1. 風(fēng)格糾錯(cuò)題
修改完的代碼:修改方法有很多種,現(xiàn)給出一種做示例:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代碼身辨,這是第一種修改方法丐谋,后面會(huì)給出第二種修改方法
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
下面對(duì)具體修改的地方,分兩部分做下介紹:硬傷部分 和 優(yōu)化部分 煌珊。因?yàn)?strong>硬傷部分沒什么技術(shù)含量号俐,為了節(jié)省大家時(shí)間,放在后面講定庵,大神請(qǐng)直接看優(yōu)化部分吏饿。
優(yōu)化部分
- enum 建議使用
NS_ENUM
和NS_OPTIONS
宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:
//定義一個(gè)枚舉
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
(僅僅讓性別包含男和女可能并不嚴(yán)謹(jǐn)蔬浙,最嚴(yán)謹(jǐn)?shù)淖龇梢詤⒖?這里 猪落。)
- age 屬性的類型:應(yīng)避免使用基本類型,建議使用 Foundation 數(shù)據(jù)類型敛滋,對(duì)應(yīng)關(guān)系如下:
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
動(dòng)畫時(shí)間 -> NSTimeInterval
同時(shí)考慮到 age 的特點(diǎn),應(yīng)使用 NSUInteger 兴革,而非 int 绎晃。 這樣做的是基于64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》杂曲。
如果工程項(xiàng)目非常龐大庶艾,需要拆分成不同的模塊,可以在類擎勘、typedef宏命名的時(shí)候使用前綴咱揍。
-
doLogIn方法不應(yīng)寫在該類中:
雖然LogIn
的命名不太清晰,但筆者猜測(cè)是login的意思棚饵, (勘誤:Login是名詞煤裙,LogIn 是動(dòng)詞掩完,都表示登陸的意思。見: Log in vs. login )登錄操作屬于業(yè)務(wù)邏輯硼砰,觀察類名 UserModel 且蓬,以及屬性的命名方式,該類應(yīng)該是一個(gè) Model 而不是一個(gè)“ MVVM 模式下的 ViewModel ”:
無論是 MVC 模式還是 MVVM 模式题翰,業(yè)務(wù)邏輯都不應(yīng)當(dāng)寫在 Model 里:MVC 應(yīng)在 C恶阴,MVVM 應(yīng)在 VM。
(如果拋開命名規(guī)范豹障,假設(shè)該類真的是 MVVM 模式里的 ViewModel 冯事,那么 UserModel 這個(gè)類可能對(duì)應(yīng)的是用戶注冊(cè)頁面,如果有特殊的業(yè)務(wù)需求血公,比如: -logIn
對(duì)應(yīng)的應(yīng)當(dāng)是注冊(cè)并登錄的一個(gè) Button 昵仅,出現(xiàn) -logIn
方法也可能是合理的。)
- doLogIn 方法命名不規(guī)范:添加了多余的動(dòng)詞前綴坞笙。 請(qǐng)牢記:
如果方法表示讓對(duì)象執(zhí)行一個(gè)動(dòng)作岩饼,使用動(dòng)詞打頭來命名,注意不要使用
do
薛夜,does
這種多余的關(guān)鍵字籍茧,動(dòng)詞本身的暗示就足夠了。
應(yīng)為 -logIn
(注意: Login
是名詞梯澜, LogIn
是動(dòng)詞寞冯,都表示登陸。 見 Log in vs. login )
-
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中不要用with
來連接兩個(gè)參數(shù):withAge:
應(yīng)當(dāng)換為age:
晚伙,age:
已經(jīng)足以清晰說明參數(shù)的作用吮龄,也不建議用andAge:
:通常情況下,即使有類似withA:withB:
的命名需求咆疗,也通常是使用withA:andB:
這種命名漓帚,用來表示方法執(zhí)行了兩個(gè)相對(duì)獨(dú)立的操作(從設(shè)計(jì)上來說,這時(shí)候也可以拆分成兩個(gè)獨(dú)立的方法)午磁,它不應(yīng)該用作闡明有多個(gè)參數(shù)尝抖,比如下面的:
//錯(cuò)誤,不要使用"and"來連接參數(shù)
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//錯(cuò)誤迅皇,不要使用"and"來闡明有多個(gè)參數(shù)
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正確昧辽,使用"and"來表示兩個(gè)相對(duì)獨(dú)立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
- 由于字符串值可能會(huì)改變,所以要把相關(guān)屬性的“內(nèi)存管理語義”聲明為 copy 登颓。(原因在下文有詳細(xì)論述:用@property聲明的NSString(或NSArray搅荞,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?)
- “性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用于設(shè)置“姓名”(Name)和“年齡”(Age)的初始值咕痛,那如何對(duì)“性別”(Sex)初始化痢甘?
Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的參數(shù)暇检,secondary 初始化方法是一個(gè)或多個(gè)产阱,并且提供一個(gè)或者更多的默認(rèn)參數(shù)來調(diào)用 designated 初始化方法的初始化方法。舉例說明:
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//
@implementation CYLUser
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
}
return self;
}
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age {
return [self initWithName:name age:age sex:nil];
}
@end
上面的代碼中initWithName:age:sex: 就是 designated 初始化方法块仆,另外的是 secondary 初始化方法构蹬。因?yàn)閮H僅是調(diào)用類實(shí)現(xiàn)的 designated 初始化方法。
因?yàn)槌鲱}者沒有給出 .m
文件悔据,所以有兩種猜測(cè):1:本來打算只設(shè)計(jì)一個(gè) designated 初始化方法庄敛,但漏掉了“性別”(sex)屬性。那么最終的修改代碼就是上文給出的第一種修改方法科汗。2:不打算初始時(shí)初始化“性別”(sex)屬性藻烤,打算后期再修改,如果是這種情況头滔,那么應(yīng)該把“性別”(sex)屬性設(shè)為 readwrite 屬性怖亭,最終給出的修改代碼應(yīng)該是:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 第二種修改方法(基于第一種修改方法的基礎(chǔ)上)
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readwrite, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
.h
中暴露 designated 初始化方法,是為了方便子類化 (想了解更多坤检,請(qǐng)戳--》 《禪與 Objective-C 編程藝術(shù) (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》兴猩。)
- 按照接口設(shè)計(jì)的慣例,如果設(shè)計(jì)了“初始化方法” (initializer)早歇,也應(yīng)當(dāng)搭配一個(gè)快捷構(gòu)造方法倾芝。而快捷構(gòu)造方法的返回值,建議為 instancetype箭跳,為保持一致性晨另,init 方法和快捷構(gòu)造方法的返回類型最好都用 instancetype。
- 如果基于第一種修改方法:既然該類中已經(jīng)有一個(gè)“初始化方法” (initializer)谱姓,用于設(shè)置“姓名”(Name)借尿、“年齡”(Age)和“性別”(Sex)的初始值: 那么在設(shè)計(jì)對(duì)應(yīng)
@property
時(shí)就應(yīng)該盡量使用不可變的對(duì)象:其三個(gè)屬性都應(yīng)該設(shè)為“只讀”立润。用初始化方法設(shè)置好屬性值之后钢悲,就不能再改變了。在本例中泽谨,仍需聲明屬性的“內(nèi)存管理語義”奶躯。于是可以把屬性的定義改成這樣
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
-
initUserModelWithUserName
如果改為initWithName
會(huì)更加簡潔帚桩,而且足夠清晰亿驾。 -
UserModel
如果改為User
會(huì)更加簡潔嘹黔,而且足夠清晰。 -
UserSex
如果改為Sex
會(huì)更加簡潔,而且足夠清晰儡蔓。 - 第二個(gè)
@property
中 assign 和 nonatomic 調(diào)換位置郭蕉。 推薦按照下面的格式來定義屬性
@property (nonatomic, readwrite, copy) NSString *name;
屬性的參數(shù)應(yīng)該按照下面的順序排列: 原子性,讀寫 和 內(nèi)存管理喂江。 這樣做你的屬性更容易修改正確召锈,并且更好閱讀。這在《禪與Objective-C編程藝術(shù) >》里有介紹获询。而且習(xí)慣上修改某個(gè)屬性的修飾符時(shí)涨岁,一般從屬性名從右向左搜索需要修動(dòng)的修飾符。最可能從最右邊開始修改這些屬性的修飾符吉嚣,根據(jù)經(jīng)驗(yàn)這些修飾符被修改的可能性從高到底應(yīng)為:內(nèi)存管理 > 讀寫權(quán)限 >原子操作梢薪。
硬傷部分
- 在-和(void)之間應(yīng)該有一個(gè)空格
- enum 中駝峰命名法和下劃線命名法混用錯(cuò)誤:枚舉類型的命名規(guī)則和函數(shù)的命名規(guī)則相同:命名時(shí)使用駝峰命名法,勿使用下劃線命名法尝哆。
- enum 左括號(hào)前加一個(gè)空格秉撇,或者將左括號(hào)換到下一行
- enum 右括號(hào)后加一個(gè)空格
-
UserModel :NSObject
應(yīng)為UserModel : NSObject
,也就是:
右側(cè)少了一個(gè)空格秋泄。 -
@interface
與@property
屬性聲明中間應(yīng)當(dāng)間隔一行琐馆。 - 兩個(gè)方法定義之間不需要換行,有時(shí)為了區(qū)分方法的功能也可間隔一行恒序,但示例代碼中間隔了兩行瘦麸。
-
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中方法名與參數(shù)之間多了空格。而且-
與(id)
之間少了空格奸焙。 -
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中方法名與參數(shù)之間多了空格:(NSString*)name
前多了空格瞎暑。 -
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中(NSString*)name
,應(yīng)為(NSString *)name
,少了空格与帆。 doLogIn方法中的LogIn
命名不清晰:筆者猜測(cè)是login的意思了赌,應(yīng)該是粗心手誤造成的。
(勘誤: Login
是名詞玄糟, LogIn
是動(dòng)詞勿她,都表示登陸的意思。見: Log in vs. login )
2. 什么情況使用 weak 關(guān)鍵字阵翎,相比 assign 有什么不同逢并?
什么情況使用 weak 關(guān)鍵字?
1郭卫、在 ARC 中,在有可能出現(xiàn)循環(huán)引用的時(shí)候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性
2砍聊、自身已經(jīng)對(duì)它進(jìn)行一次強(qiáng)引用,沒有必要再強(qiáng)引用一次,此時(shí)也會(huì)使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當(dāng)然贰军,也可以使用strong玻蝌。在下文也有論述:《IBOutlet連出來的視圖屬性為什么可以被設(shè)置成weak?》
不同點(diǎn):
1、weak 此特質(zhì)表明該屬性定義了一種“非擁有關(guān)系” (nonowning relationship)。為這種屬性設(shè)置新值時(shí)俯树,設(shè)置方法既不保留新值帘腹,也不釋放舊值。此特質(zhì)同assign類似许饿, 然而在屬性所指的對(duì)象遭到摧毀時(shí)阳欲,屬性值也會(huì)清空(nil out)。 而 assign 的“設(shè)置方法”只會(huì)執(zhí)行針對(duì)“純量類型” (scalar type陋率,例如 CGFloat 或 NSlnteger 等)的簡單賦值操作球化。
2、assign 可以用非 OC 對(duì)象,而 weak 必須用于 OC 對(duì)象
3. 怎么用 copy 關(guān)鍵字瓦糟?
用途:
- NSString赊窥、NSArray、NSDictionary 等等經(jīng)常使用copy關(guān)鍵字狸页,是因?yàn)樗麄冇袑?duì)應(yīng)的可變類型:NSMutableString锨能、NSMutableArray、NSMutableDictionary芍耘;
- block 也經(jīng)常使用 copy 關(guān)鍵字址遇,具體原因見官方文檔:Objects Use Properties to Keep Track of Blocks:
block 使用 copy 是從 MRC 遺留下來的“傳統(tǒng)”,在 MRC 中,方法內(nèi)部的 block 是在棧區(qū)的,使用 copy 可以把它放到堆區(qū).在 ARC 中寫不寫都行:對(duì)于 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅斋竞,還能時(shí)刻提醒我們:編譯器自動(dòng)對(duì) block 進(jìn)行了 copy 操作倔约。如果不寫 copy ,該類的調(diào)用者有可能會(huì)忘記或者根本不知道“編譯器會(huì)自動(dòng)對(duì) block 進(jìn)行了 copy 操作”坝初,他們有可能會(huì)在調(diào)用之前自行拷貝屬性值浸剩。這種操作多余而低效。你也許會(huì)感覺我這種做法有些怪異鳄袍,不需要寫依然寫绢要。如果你這樣想,其實(shí)是你“日用而不知”拗小,你平時(shí)開發(fā)中是經(jīng)常在用我說的這種做法的重罪,比如下面的屬性不寫copy也行,但是你會(huì)選擇寫還是不寫呢哀九?
下面做下解釋: copy 此特質(zhì)所表達(dá)的所屬關(guān)系與 strong 類似剿配。然而設(shè)置方法并不保留新值,而是將其“拷貝” (copy)阅束。 當(dāng)屬性類型為 NSString 時(shí)呼胚,經(jīng)常用此特質(zhì)來保護(hù)其封裝性,因?yàn)閭鬟f給設(shè)置方法的新值有可能指向一個(gè) NSMutableString 類的實(shí)例息裸。這個(gè)類是 NSString 的子類蝇更,表示一種可修改其值的字符串琢融,此時(shí)若是不拷貝字符串,那么設(shè)置完屬性之后簿寂,字符串的值就可能會(huì)在對(duì)象不知情的情況下遭人更改。所以宿亡,這時(shí)就要拷貝一份“不可變” (immutable)的字符串常遂,確保對(duì)象中的字符串值不會(huì)無意間變動(dòng)。只要實(shí)現(xiàn)屬性所用的對(duì)象是“可變的” (mutable)挽荠,就應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份克胳。
用
@property
聲明 NSString、NSArray圈匆、NSDictionary 經(jīng)常使用 copy 關(guān)鍵字漠另,是因?yàn)樗麄冇袑?duì)應(yīng)的可變類型:NSMutableString、NSMutableArray跃赚、NSMutableDictionary笆搓,他們之間可能進(jìn)行賦值操作,為確保對(duì)象中的字符串值不會(huì)無意間變動(dòng)纬傲,應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份满败。
該問題在下文中也有論述:用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字叹括,為什么算墨?如果改用strong關(guān)鍵字,可能造成什么問題汁雷?
4. 這個(gè)寫法會(huì)出什么問題: @property (copy) NSMutableArray *array;
兩個(gè)問題:1净嘀、添加,刪除,修改數(shù)組內(nèi)的元素的時(shí)候,程序會(huì)因?yàn)檎也坏綄?duì)應(yīng)的方法而崩潰.因?yàn)?copy 就是復(fù)制一個(gè)不可變 NSArray 的對(duì)象;2侠讯、使用了 atomic 屬性會(huì)嚴(yán)重影響性能 挖藏;
第1條的相關(guān)原因在下文中有論述《用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用 copy 關(guān)鍵字厢漩,為什么熬苍?如果改用strong關(guān)鍵字,可能造成什么問題袁翁?》 以及上文《怎么用 copy 關(guān)鍵字柴底?》也有論述。
比如下面的代碼就會(huì)發(fā)生崩潰
// .h文件
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
接下來就會(huì)奔潰:
-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
第2條原因粱胜,如下:
該屬性使用了自旋鎖柄驻,會(huì)在創(chuàng)建時(shí)生成一些額外的代碼用于幫助編寫多線程程序,這會(huì)帶來性能問題焙压,通過聲明 nonatomic 可以節(jié)省這些雖然很小但是不必要額外開銷鸿脓。
在默認(rèn)情況下抑钟,由編譯器所合成的方法會(huì)通過鎖定機(jī)制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質(zhì)野哭,則不使用自旋鎖在塔。請(qǐng)注意,盡管沒有名為“atomic”的特質(zhì)(如果某屬性不具備 nonatomic 特質(zhì)拨黔,那它就是“原子的”(atomic))蛔溃。
在iOS開發(fā)中,你會(huì)發(fā)現(xiàn)篱蝇,幾乎所有屬性都聲明為 nonatomic贺待。
一般情況下并不要求屬性必須是“原子的”,因?yàn)檫@并不能保證“線程安全” ( thread safety)零截,若要實(shí)現(xiàn)“線程安全”的操作麸塞,還需采用更為深層的鎖定機(jī)制才行。例如涧衙,一個(gè)線程在連續(xù)多次讀取某屬性值的過程中有別的線程在同時(shí)改寫該值哪工,那么即便將屬性聲明為 atomic,也還是會(huì)讀到不同的屬性值弧哎。
因此正勒,開發(fā)iOS程序時(shí)一般都會(huì)使用 nonatomic 屬性。但是在開發(fā) Mac OS X 程序時(shí)傻铣,使用 atomic 屬性通常都不會(huì)有性能瓶頸章贞。
5. 如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關(guān)鍵字的 setter非洲?
若想令自己所寫的對(duì)象具有拷貝功能鸭限,則需實(shí)現(xiàn) NSCopying 協(xié)議。如果自定義的對(duì)象分為可變版本與不可變版本两踏,那么就要同時(shí)實(shí)現(xiàn)
NSCopying
與NSMutableCopying
協(xié)議败京。
具體步驟:
- 需聲明該類遵從 NSCopying 協(xié)議
- 實(shí)現(xiàn) NSCopying 協(xié)議。該協(xié)議只有一個(gè)方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到讓自己的類用 copy 修飾符梦染,我們總是想覆寫copy方法赡麦,其實(shí)真正需要實(shí)現(xiàn)的卻是 “copyWithZone” 方法。
以第一題的代碼為例:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代碼
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
然后實(shí)現(xiàn)協(xié)議中規(guī)定的方法:
- (id)copyWithZone:(NSZone *)zone {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
return copy;
}
但在實(shí)際的項(xiàng)目中帕识,不可能這么簡單泛粹,遇到更復(fù)雜一點(diǎn),比如類對(duì)象中的數(shù)據(jù)結(jié)構(gòu)可能并未在初始化方法中設(shè)置好肮疗,需要另行設(shè)置晶姊。舉個(gè)例子,假如 CYLUser 中含有一個(gè)數(shù)組伪货,與其他 CYLUser 對(duì)象建立或解除朋友關(guān)系的那些方法都需要操作這個(gè)數(shù)組们衙。那么在這種情況下钾怔,你得把這個(gè)包含朋友對(duì)象的數(shù)組也一并拷貝過來。下面列出了實(shí)現(xiàn)此功能所需的全部代碼:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 以第一題《風(fēng)格糾錯(cuò)題》里的代碼為例
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (void)addFriend:(CYLUser *)user;
- (void)removeFriend:(CYLUser *)user;
@end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//
@implementation CYLUser {
NSMutableSet *_friends;
}
- (void)setName:(NSString *)name {
_name = [name copy];
}
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
_friends = [[NSMutableSet alloc] init];
}
return self;
}
- (void)addFriend:(CYLUser *)user {
[_friends addObject:user];
}
- (void)removeFriend:(CYLUser *)user {
[_friends removeObject:user];
}
- (id)copyWithZone:(NSZone *)zone {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [_friends mutableCopy];
return copy;
}
- (id)deepCopy {
CYLUser *copy = [[[self class] alloc]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
@end
以上做法能滿足基本的需求蒙挑,但是也有缺陷:
如果你所寫的對(duì)象需要深拷貝宗侦,那么可考慮新增一個(gè)專門執(zhí)行深拷貝的方法。
【注:深淺拷貝的概念忆蚀,在下文中有介紹矾利,詳見下文的:用@property聲明的 NSString(或NSArray,NSDictionary)經(jīng)常使用 copy 關(guān)鍵字蜓谋,為什么?如果改用 strong 關(guān)鍵字炭分,可能造成什么問題桃焕?】
在例子中,存放朋友對(duì)象的 set 是用 “copyWithZone:” 方法來拷貝的捧毛,這種淺拷貝方式不會(huì)逐個(gè)復(fù)制 set 中的元素观堂。若需要深拷貝的話,則可像下面這樣呀忧,編寫一個(gè)專供深拷貝所用的方法:
- (id)deepCopy {
CYLUser *copy = [[[self class] alloc]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
至于如何重寫帶 copy 關(guān)鍵字的 setter這個(gè)問題师痕,
如果拋開本例來回答的話,如下:
- (void)setName:(NSString *)name {
//[_name release];
_name = [name copy];
}
不過也有爭議而账,有人說“蘋果如果像下面這樣干胰坟,是不是效率會(huì)高一些?”
- (void)setName:(NSString *)name {
if (_name != name) {
//[_name release];//MRC
_name = [name copy];
}
}
你可能會(huì)說:
之所以在這里做if判斷
這個(gè)操作:是因?yàn)橐粋€(gè) if 可能避免一個(gè)耗時(shí)的copy泞辐,還是很劃算的笔横。 (在剛剛講的:《如何讓自己的類用 copy 修飾符?》里的那種復(fù)雜的copy咐吼,我們可以稱之為 “耗時(shí)的copy”吹缔,但是對(duì) NSString 的 copy 還稱不上。)
但是你有沒有考慮過代價(jià):
你每次調(diào)用
setX:
都會(huì)做 if 判斷锯茄,這會(huì)讓setX:
變慢厢塘,如果你在setX:
寫了一串復(fù)雜的if+elseif+elseif+...
判斷,將會(huì)更慢肌幽。
要回答“哪個(gè)效率會(huì)高一些晚碾?”這個(gè)問題,不能脫離實(shí)際開發(fā)喂急,就算 copy 操作十分耗時(shí)迄薄,if 判斷也不見得一定會(huì)更快,除非你把一個(gè)“ @property他當(dāng)前的值 ”賦給了他自己煮岁,代碼看起來就像:
[a setX:x1];
[a setX:x1]; //你確定你要這么干讥蔽?與其在setter中判斷涣易,為什么不把代碼寫好?
或者
[a setX:[a x]]; //隊(duì)友咆哮道:你在干嘛冶伞?P轮ⅰ!
不要在 setter 里進(jìn)行像
if(_obj != newObj)
這樣的判斷响禽。(該觀點(diǎn)參考鏈接: How To Write Cocoa Object Setters: Principle 3: Only Optimize After You Measure )
什么情況會(huì)在 copy setter 里做 if 判斷徒爹? 例如,車速可能就有最高速的限制芋类,車速也不可能出現(xiàn)負(fù)值隆嗅,如果車子的最高速為300,則 setter 的方法就要改寫成這樣:
-(void)setSpeed:(int)speed {
if(speed < 0) speed = 0;
if(speed > 300) speed = 300;
_speed = speed;
}
回到這個(gè)題目侯繁,如果單單就上文的代碼而言胖喳,我們不需要也不能重寫 name 的 setter :由于是 name 是只讀屬性,所以編譯器不會(huì)為其創(chuàng)建對(duì)應(yīng)的“設(shè)置方法”贮竟,用初始化方法設(shè)置好屬性值之后丽焊,就不能再改變了。( 在本例中咕别,之所以還要聲明屬性的“內(nèi)存管理語義”--copy技健,是因?yàn)椋喝绻粚?copy,該類的調(diào)用者就不知道初始化方法里會(huì)拷貝這些屬性惰拱,他們有可能會(huì)在調(diào)用初始化方法之前自行拷貝屬性值雌贱。這種操作多余而低效)。
那如何確保 name 被 copy偿短?在初始化方法(initializer)中做:
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
_friends = [[NSMutableSet alloc] init];
}
return self;
}
6. @property 的本質(zhì)是什么帽芽?ivar、getter翔冀、setter 是如何生成并添加到這個(gè)類中的
@property 的本質(zhì)是什么导街?
@property = ivar + getter + setter;
下面解釋下:
“屬性” (property)有兩大概念:ivar(實(shí)例變量)、存取方法(access method = getter + setter)纤子。
“屬性” (property)作為 Objective-C 的一項(xiàng)特性搬瑰,主要的作用就在于封裝對(duì)象中的數(shù)據(jù)。 Objective-C 對(duì)象通常會(huì)把其所需要的數(shù)據(jù)保存為各種實(shí)例變量控硼。實(shí)例變量一般通過“存取方法”(access method)來訪問泽论。其中,“獲取方法” (getter)用于讀取變量值卡乾,而“設(shè)置方法” (setter)用于寫入變量值翼悴。這個(gè)概念已經(jīng)定型,并且經(jīng)由“屬性”這一特性而成為 Objective-C 2.0
的一部分幔妨。 而在正規(guī)的 Objective-C 編碼風(fēng)格中鹦赎,存取方法有著嚴(yán)格的命名規(guī)范谍椅。 正因?yàn)橛辛诉@種嚴(yán)格的命名規(guī)范,所以 Objective-C 這門語言才能根據(jù)名稱自動(dòng)創(chuàng)建出存取方法古话。其實(shí)也可以把屬性當(dāng)做一種關(guān)鍵字雏吭,其表示:
編譯器會(huì)自動(dòng)寫出一套存取方法,用以訪問給定類型中具有給定名稱的變量陪踩。 所以你也可以這么說:
@property = getter + setter;
例如下面這個(gè)類:
@interface Person : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
上述代碼寫出來的類與下面這種寫法等效:
@interface Person : NSObject
- (NSString *)firstName;
- (void)setFirstName:(NSString *)firstName;
- (NSString *)lastName;
- (void)setLastName:(NSString *)lastName;
@end
更新:
property在runtime中是objc_property_t
定義如下:
typedef struct objc_property *objc_property_t;
而objc_property
是一個(gè)結(jié)構(gòu)體杖们,包括name和attributes,定義如下:
struct property_t {
const char *name;
const char *attributes;
};
而attributes本質(zhì)是objc_property_attribute_t
肩狂,定義了property的一些屬性摘完,定義如下:
/// Defines a property attribute
typedef struct {
const char *name; /**< The name of the attribute */
const char *value; /**< The value of the attribute (usually empty) */
} objc_property_attribute_t;
而attributes的具體內(nèi)容是什么呢?其實(shí)傻谁,包括:類型孝治,原子性,內(nèi)存語義和對(duì)應(yīng)的實(shí)例變量栅螟。
例如:我們定義一個(gè)string的property@property (nonatomic, copy) NSString *string;
荆秦,通過 property_getAttributes(property)
獲取到attributes并打印出來之后的結(jié)果為T@"NSString",C,N,V_string
其中T就代表類型篱竭,可參閱Type Encodings力图,C就代表Copy,N代表nonatomic掺逼,V就代表對(duì)應(yīng)的實(shí)例變量吃媒。
ivar、getter吕喘、setter 是如何生成并添加到這個(gè)類中的?
“自動(dòng)合成”( autosynthesis)
完成屬性定義后赘那,編譯器會(huì)自動(dòng)編寫訪問這些屬性所需的方法,此過程叫做“自動(dòng)合成”(autosynthesis)氯质。需要強(qiáng)調(diào)的是募舟,這個(gè)過程由編譯 器在編譯期執(zhí)行,所以編輯器里看不到這些“合成方法”(synthesized method)的源代碼闻察。除了生成方法代碼 getter拱礁、setter 之外,編譯器還要自動(dòng)向類中添加適當(dāng)類型的實(shí)例變量辕漂,并且在屬性名前面加下劃線呢灶,以此作為實(shí)例變量的名字。在前例中钉嘹,會(huì)生成兩個(gè)實(shí)例變量鸯乃,其名稱分別為 _firstName
與 _lastName
。也可以在類的實(shí)現(xiàn)代碼里通過@synthesize
語法來指定實(shí)例變量的名字.
@implementation Person
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
我為了搞清屬性是怎么實(shí)現(xiàn)的,曾經(jīng)反編譯過相關(guān)的代碼,他大致生成了五個(gè)東西
-
OBJC_IVAR_$類名$屬性名稱
:該屬性的“偏移量” (offset)跋涣,這個(gè)偏移量是“硬編碼” (hardcode)缨睡,表示該變量距離存放對(duì)象的內(nèi)存區(qū)域的起始地址有多遠(yuǎn)鸟悴。 - setter 與 getter 方法對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)
-
ivar_list
:成員變量列表 -
method_list
:方法列表 -
prop_list
:屬性列表
也就是說我們每次在增加一個(gè)屬性,系統(tǒng)都會(huì)在 ivar_list
中添加一個(gè)成員變量的描述,在 method_list
中增加 setter 與 getter 方法的描述,在屬性列表中增加一個(gè)屬性的描述,然后計(jì)算該屬性在對(duì)象中的偏移量,然后給出 setter 與 getter 方法對(duì)應(yīng)的實(shí)現(xiàn),在 setter 方法中從偏移量的位置開始賦值,在 getter 方法中從偏移量開始取值,為了能夠讀取正確字節(jié)數(shù),系統(tǒng)對(duì)象偏移量的指針類型進(jìn)行了類型強(qiáng)轉(zhuǎn).
7. @protocol 和 category 中如何使用 @property
在 protocol 中使用 property 只會(huì)生成 setter 和 getter 方法聲明,我們使用屬性的目的,是希望遵守我協(xié)議的對(duì)象能實(shí)現(xiàn)該屬性
category 使用 @property 也是只會(huì)生成 setter 和 getter 方法的聲明,如果我們真的需要給 category 增加屬性的實(shí)現(xiàn),需要借助于運(yùn)行時(shí)的兩個(gè)函數(shù):
objc_setAssociatedObject
objc_getAssociatedObject
8. runtime 如何實(shí)現(xiàn) weak 屬性
要實(shí)現(xiàn) weak 屬性,首先要搞清楚 weak 屬性的特點(diǎn):
weak 此特質(zhì)表明該屬性定義了一種“非擁有關(guān)系” (nonowning relationship)宏蛉。為這種屬性設(shè)置新值時(shí)遣臼,設(shè)置方法既不保留新值,也不釋放舊值拾并。此特質(zhì)同 assign 類似揍堰, 然而在屬性所指的對(duì)象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。
那么 runtime 如何實(shí)現(xiàn) weak 變量的自動(dòng)置nil?
runtime 對(duì)注冊(cè)的類票腰, 會(huì)進(jìn)行布局纱昧,對(duì)于 weak 對(duì)象會(huì)放入一個(gè) hash 表中。 用 weak 指向的對(duì)象內(nèi)存地址作為 key愧沟,當(dāng)此對(duì)象的引用計(jì)數(shù)為0的時(shí)候會(huì) dealloc,假如 weak 指向的對(duì)象內(nèi)存地址是a,那么就會(huì)以a為鍵幽纷, 在這個(gè) weak 表中搜索,找到所有以a為鍵的 weak 對(duì)象博敬,從而設(shè)置為 nil友浸。
(注:在下文的《使用runtime Associate方法關(guān)聯(lián)的對(duì)象,需要在主對(duì)象dealloc的時(shí)候釋放么偏窝?》里給出的“對(duì)象的內(nèi)存銷毀時(shí)間表”也提到__weak
引用的解除時(shí)間收恢。)
先看下 runtime 里源碼的實(shí)現(xiàn):
/**
* The internal structure stored in the weak references table.
* It maintains and stores
* a hash set of weak references pointing to an object.
* If out_of_line==0, the set is instead a small inline array.
*/
#define WEAK_INLINE_COUNT 4
struct weak_entry_t {
DisguisedPtr<objc_object> referent;
union {
struct {
weak_referrer_t *referrers;
uintptr_t out_of_line : 1;
uintptr_t num_refs : PTR_MINUS_1;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
struct {
// out_of_line=0 is LSB of one of these (don't care which)
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};
};
/**
* The global weak references table. Stores object ids as keys,
* and weak_entry_t structs as their values.
*/
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};
具體完整實(shí)現(xiàn)參照 objc/objc-weak.h 。
我們可以設(shè)計(jì)一個(gè)函數(shù)(偽代碼)來表示上述機(jī)制:
objc_storeWeak(&a, b)
函數(shù):
objc_storeWeak
函數(shù)把第二個(gè)參數(shù)--賦值對(duì)象(b)的內(nèi)存地址作為鍵值key祭往,將第一個(gè)參數(shù)--weak修飾的屬性變量(a)的內(nèi)存地址(&a)作為value伦意,注冊(cè)到 weak 表中。如果第二個(gè)參數(shù)(b)為0(nil)硼补,那么把變量(a)的內(nèi)存地址(&a)從weak表中刪除驮肉,
你可以把objc_storeWeak(&a, b)
理解為:objc_storeWeak(value, key)
,并且當(dāng)key變nil已骇,將value置nil离钝。
在b非nil時(shí),a和b指向同一個(gè)內(nèi)存地址疾捍,在b變nil時(shí)奈辰,a變nil。此時(shí)向a發(fā)送消息不會(huì)崩潰:在Objective-C中向nil發(fā)送消息是安全的乱豆。
而如果a是由 assign 修飾的奖恰,則: 在 b 非 nil 時(shí),a 和 b 指向同一個(gè)內(nèi)存地址,在 b 變 nil 時(shí)瑟啃,a 還是指向該內(nèi)存地址论泛,變野指針。此時(shí)向 a 發(fā)送消息極易崩潰蛹屿。
下面我們將基于objc_storeWeak(&a, b)
函數(shù)屁奏,使用偽代碼模擬“runtime如何實(shí)現(xiàn)weak屬性”:
// 使用偽代碼模擬:runtime如何實(shí)現(xiàn)weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
objc_initWeak(&obj1, obj);
/*obj引用計(jì)數(shù)變?yōu)?,變量作用域結(jié)束*/
objc_destroyWeak(&obj1);
下面對(duì)用到的兩個(gè)方法objc_initWeak
和objc_destroyWeak
做下解釋:
總體說來错负,作用是: 通過objc_initWeak
函數(shù)初始化“附有weak修飾符的變量(obj1)”坟瓢,在變量作用域結(jié)束時(shí)通過objc_destoryWeak
函數(shù)釋放該變量(obj1)。
下面分別介紹下方法的內(nèi)部實(shí)現(xiàn):
objc_initWeak
函數(shù)的實(shí)現(xiàn)是這樣的:在將“附有weak修飾符的變量(obj1)”初始化為0(nil)后犹撒,會(huì)將“賦值對(duì)象”(obj)作為參數(shù)折联,調(diào)用objc_storeWeak
函數(shù)。
obj1 = 0识颊;
obj_storeWeak(&obj1, obj);
也就是說:
weak 修飾的指針默認(rèn)值是 nil (在Objective-C中向nil發(fā)送消息是安全的)
然后obj_destroyWeak
函數(shù)將0(nil)作為參數(shù)诚镰,調(diào)用objc_storeWeak
函數(shù)。
objc_storeWeak(&obj1, 0);
前面的源代碼與下列源代碼相同祥款。
// 使用偽代碼模擬:runtime如何實(shí)現(xiàn)weak屬性
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
id obj1;
obj1 = 0;
objc_storeWeak(&obj1, obj);
/* ... obj的引用計(jì)數(shù)變?yōu)?清笨,被置nil ... */
objc_storeWeak(&obj1, 0);
objc_storeWeak
函數(shù)把第二個(gè)參數(shù)--賦值對(duì)象(obj)的內(nèi)存地址作為鍵值,將第一個(gè)參數(shù)--weak修飾的屬性變量(obj1)的內(nèi)存地址注冊(cè)到 weak 表中刃跛。如果第二個(gè)參數(shù)(obj)為0(nil)抠艾,那么把變量(obj1)的地址從 weak 表中刪除,在后面的相關(guān)一題會(huì)詳解奠伪。
使用偽代碼是為了方便理解跌帐,下面我們“真槍實(shí)彈”地實(shí)現(xiàn)下:
如何讓不使用weak修飾的@property首懈,擁有weak的效果绊率。
我們從setter方法入手:
(注意以下的 cyl_runAtDealloc
方法實(shí)現(xiàn)僅僅用于模擬原理,如果想用于項(xiàng)目中究履,還需要考慮更復(fù)雜的場(chǎng)景滤否,想在實(shí)際項(xiàng)目使用的話,可以使用我寫的一個(gè)小庫最仑,可以使用 CocoaPods 在項(xiàng)目中使用: CYLDeallocBlockExecutor )
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
也就是有兩個(gè)步驟:
- 在setter方法中做如下設(shè)置:
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
- 在屬性所指的對(duì)象遭到摧毀時(shí)藐俺,屬性值也會(huì)清空(nil out)。做到這點(diǎn)泥彤,同樣要借助 runtime:
//要銷毀的目標(biāo)對(duì)象
id objectToBeDeallocated;
//可以理解為一個(gè)“事件”:當(dāng)上面的目標(biāo)對(duì)象銷毀時(shí)欲芹,同時(shí)要發(fā)生的“事件”。
id objectWeWantToBeReleasedWhenThatHappens;
objc_setAssociatedObject(objectToBeDeallocted,
someUniqueKey,
objectWeWantToBeReleasedWhenThatHappens,
OBJC_ASSOCIATION_RETAIN);
知道了思路吟吝,我們就開始實(shí)現(xiàn) cyl_runAtDealloc
方法菱父,實(shí)現(xiàn)過程分兩部分:
第一部分:創(chuàng)建一個(gè)類,可以理解為一個(gè)“事件”:當(dāng)目標(biāo)對(duì)象銷毀時(shí),同時(shí)要發(fā)生的“事件”浙宜。借助 block 執(zhí)行“事件”官辽。
// .h文件
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 這個(gè)類,可以理解為一個(gè)“事件”:當(dāng)目標(biāo)對(duì)象銷毀時(shí)粟瞬,同時(shí)要發(fā)生的“事件”同仆。借助block執(zhí)行“事件”。
typedef void (^voidBlock)(void);
@interface CYLBlockExecutor : NSObject
- (id)initWithBlock:(voidBlock)block;
@end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 這個(gè)類裙品,可以理解為一個(gè)“事件”:當(dāng)目標(biāo)對(duì)象銷毀時(shí)俗批,同時(shí)要發(fā)生的“事件”。借助block執(zhí)行“事件”市怎。
#import "CYLBlockExecutor.h"
@interface CYLBlockExecutor() {
voidBlock _block;
}
@implementation CYLBlockExecutor
- (id)initWithBlock:(voidBlock)aBlock
{
self = [super init];
if (self) {
_block = [aBlock copy];
}
return self;
}
- (void)dealloc
{
_block ? _block() : nil;
}
@end
第二部分:核心代碼:利用runtime實(shí)現(xiàn)cyl_runAtDealloc
方法
// CYLNSObject+RunAtDealloc.h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 利用runtime實(shí)現(xiàn)cyl_runAtDealloc方法
#import "CYLBlockExecutor.h"
const void *runAtDeallocBlockKey = &runAtDeallocBlockKey;
@interface NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block;
@end
// CYLNSObject+RunAtDealloc.m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 利用runtime實(shí)現(xiàn)cyl_runAtDealloc方法
#import "CYLNSObject+RunAtDealloc.h"
#import "CYLBlockExecutor.h"
@implementation NSObject (CYLRunAtDealloc)
- (void)cyl_runAtDealloc:(voidBlock)block
{
if (block) {
CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block];
objc_setAssociatedObject(self,
runAtDeallocBlockKey,
executor,
OBJC_ASSOCIATION_RETAIN);
}
}
@end
使用方法: 導(dǎo)入
#import "CYLNSObject+RunAtDealloc.h"
然后就可以使用了:
NSObject *foo = [[NSObject alloc] init];
[foo cyl_runAtDealloc:^{
NSLog(@"正在釋放foo!");
}];
如果對(duì) cyl_runAtDealloc
的實(shí)現(xiàn)原理有興趣扶镀,可以看下我寫的一個(gè)小庫,可以使用 CocoaPods 在項(xiàng)目中使用: CYLDeallocBlockExecutor
參考博文: Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object
9. @property中有哪些屬性關(guān)鍵字焰轻?/ @property 后面可以有哪些修飾符臭觉?
屬性可以擁有的特質(zhì)分為四類:
-
原子性---
nonatomic
特質(zhì)在默認(rèn)情況下,由編譯器合成的方法會(huì)通過鎖定機(jī)制確保其原子性(atomicity)辱志。如果屬性具備 nonatomic 特質(zhì)蝠筑,則不使用自旋鎖。請(qǐng)注意揩懒,盡管沒有名為“atomic”的特質(zhì)(如果某屬性不具備 nonatomic 特質(zhì)什乙,那它就是“原子的” ( atomic) ),但是仍然可以在屬性特質(zhì)中寫明這一點(diǎn)已球,編譯器不會(huì)報(bào)錯(cuò)臣镣。若是自己定義存取方法,那么就應(yīng)該遵從與屬性特質(zhì)相符的原子性智亮。
讀/寫權(quán)限---
readwrite(讀寫)
忆某、readonly (只讀)
內(nèi)存管理語義---
assign
、strong
阔蛉、weak
弃舒、unsafe_unretained
、copy
方法名---
getter=<name>
状原、setter=<name>
getter=<name>
的樣式:
@property (nonatomic, getter=isOn) BOOL on;
( setter=
這種不常用聋呢,也不推薦使用。故不在這里給出寫法颠区。)
setter=<name>
一般用在特殊的情境下削锰,比如:
在數(shù)據(jù)反序列化、轉(zhuǎn)模型的過程中毕莱,服務(wù)器返回的字段如果以 init
開頭器贩,所以你需要定義一個(gè) init
開頭的屬性测暗,但默認(rèn)生成的 setter
與 getter
方法也會(huì)以 init
開頭,而編譯器會(huì)把所有以 init
開頭的方法當(dāng)成初始化方法磨澡,而初始化方法只能返回 self 類型碗啄,因此編譯器會(huì)報(bào)錯(cuò)。
這時(shí)你就可以使用下面的方式來避免編譯器報(bào)錯(cuò):
@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;
另外也可以用關(guān)鍵字進(jìn)行特殊說明稳摄,來避免編譯器報(bào)錯(cuò):
@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy __attribute__((objc_method_family(none)));
- 不常用的:
nonnull
,null_resettable
,nullable
注意:很多人會(huì)認(rèn)為如果屬性具備 nonatomic 特質(zhì)稚字,則不使用 “同步鎖”。其實(shí)在屬性設(shè)置方法中使用的是自旋鎖厦酬,自旋鎖相關(guān)代碼如下:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
10. weak屬性需要在dealloc中置nil么胆描?
不需要。
在ARC環(huán)境無論是強(qiáng)指針還是弱指針都無需在 dealloc 設(shè)置為 nil 仗阅, ARC 會(huì)自動(dòng)幫我們處理
即便是編譯器不幫我們做這些昌讲,weak也不需要在 dealloc 中置nil:
正如上文的:runtime 如何實(shí)現(xiàn) weak 屬性 中提到的:
我們模擬下 weak 的 setter 方法,應(yīng)該如下:
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
如果對(duì) cyl_runAtDealloc
的實(shí)現(xiàn)原理有興趣减噪,可以看下我寫的一個(gè)小庫短绸,可以使用 CocoaPods 在項(xiàng)目中使用: CYLDeallocBlockExecutor
也即:
在屬性所指的對(duì)象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)筹裕。
11. @synthesize和@dynamic分別有什么作用醋闭?
- @property有兩個(gè)對(duì)應(yīng)的詞,一個(gè)是 @synthesize朝卒,一個(gè)是 @dynamic证逻。如果 @synthesize和 @dynamic都沒寫,那么默認(rèn)的就是
@syntheszie var = _var;
- @synthesize 的語義是如果你沒有手動(dòng)實(shí)現(xiàn) setter 方法和 getter 方法抗斤,那么編譯器會(huì)自動(dòng)為你加上這兩個(gè)方法囚企。
- @dynamic 告訴編譯器:屬性的 setter 與 getter 方法由用戶自己實(shí)現(xiàn),不自動(dòng)生成瑞眼。(當(dāng)然對(duì)于 readonly 的屬性只需提供 getter 即可)龙宏。假如一個(gè)屬性被聲明為 @dynamic var,然后你沒有提供 @setter方法和 @getter 方法负拟,編譯的時(shí)候沒問題烦衣,但是當(dāng)程序運(yùn)行到
instance.var = someVar
歹河,由于缺 setter 方法會(huì)導(dǎo)致程序崩潰掩浙;或者當(dāng)運(yùn)行到someVar = var
時(shí),由于缺 getter 方法同樣會(huì)導(dǎo)致崩潰秸歧。編譯時(shí)沒問題厨姚,運(yùn)行時(shí)才執(zhí)行相應(yīng)的方法,這就是所謂的動(dòng)態(tài)綁定键菱。
12. ARC下谬墙,不顯式指定任何屬性關(guān)鍵字時(shí)今布,默認(rèn)的關(guān)鍵字都有哪些?
- 對(duì)應(yīng)基本數(shù)據(jù)類型默認(rèn)關(guān)鍵字是
atomic,readwrite,assign 2. 對(duì)于普通的 Objective-C 對(duì)象
atomic,readwrite,strong
參考鏈接:
13. 用@property聲明的NSString(或NSArray拭抬,NSDictionary)經(jīng)常使用copy關(guān)鍵字部默,為什么?如果改用strong關(guān)鍵字造虎,可能造成什么問題傅蹂?
- 因?yàn)楦割愔羔樋梢灾赶蜃宇悓?duì)象,使用 copy 的目的是為了讓本對(duì)象的屬性不受外界影響,使用 copy 無論給我傳入是一個(gè)可變對(duì)象還是不可對(duì)象,我本身持有的就是一個(gè)不可變的副本.
- 如果我們使用是 strong ,那么這個(gè)屬性就有可能指向一個(gè)可變對(duì)象,如果這個(gè)可變對(duì)象在外部被修改了,那么會(huì)影響該屬性.
copy 此特質(zhì)所表達(dá)的所屬關(guān)系與 strong 類似。然而設(shè)置方法并不保留新值算凿,而是將其“拷貝” (copy)份蝴。 當(dāng)屬性類型為 NSString 時(shí),經(jīng)常用此特質(zhì)來保護(hù)其封裝性氓轰,因?yàn)閭鬟f給設(shè)置方法的新值有可能指向一個(gè) NSMutableString 類的實(shí)例婚夫。這個(gè)類是 NSString 的子類,表示一種可修改其值的字符串署鸡,此時(shí)若是不拷貝字符串案糙,那么設(shè)置完屬性之后,字符串的值就可能會(huì)在對(duì)象不知情的情況下遭人更改靴庆。所以侍筛,這時(shí)就要拷貝一份“不可變” (immutable)的字符串,確保對(duì)象中的字符串值不會(huì)無意間變動(dòng)撒穷。只要實(shí)現(xiàn)屬性所用的對(duì)象是“可變的” (mutable)匣椰,就應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份。
舉例說明:
定義一個(gè)以 strong 修飾的 array:
@property (nonatomic ,readwrite, strong) NSArray *array;
然后進(jìn)行下面的操作:
NSArray *array = @[ @1, @2, @3, @4 ];
NSMutableArray *mutableArray = [NSMutableArray arrayWithArray:array];
self.array = mutableArray;
[mutableArray removeAllObjects];;
NSLog(@"%@",self.array);
[mutableArray addObjectsFromArray:array];
self.array = [mutableArray copy];
[mutableArray removeAllObjects];;
NSLog(@"%@",self.array);
打印結(jié)果如下所示:
2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] (
)
2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] (
1,
2,
3,
4
)
(詳見倉庫內(nèi)附錄的 Demo端礼。)
為了理解這種做法禽笑,首先要知道,兩種情況:
- 對(duì)非集合類對(duì)象的 copy 與 mutableCopy 操作蛤奥;
- 對(duì)集合類對(duì)象的 copy 與 mutableCopy 操作佳镜。
1. 對(duì)非集合類對(duì)象的copy操作:
在非集合類對(duì)象中:對(duì) immutable 對(duì)象進(jìn)行 copy 操作,是指針復(fù)制凡桥,mutableCopy 操作時(shí)內(nèi)容復(fù)制蟀伸;對(duì) mutable 對(duì)象進(jìn)行 copy 和 mutableCopy 都是內(nèi)容復(fù)制。用代碼簡單表示如下:
- [immutableObject copy] // 淺復(fù)制
- [immutableObject mutableCopy] //深復(fù)制
- [mutableObject copy] //深復(fù)制
- [mutableObject mutableCopy] //深復(fù)制
比如以下代碼:
NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy
NSString *stringCopy = [string copy];
查看內(nèi)存缅刽,會(huì)發(fā)現(xiàn) string啊掏、stringCopy 內(nèi)存地址都不一樣,說明此時(shí)都是做內(nèi)容拷貝衰猛、深拷貝迟蜜。即使你進(jìn)行如下操作:
[string appendString:@"origion!"]
stringCopy 的值也不會(huì)因此改變,但是如果不使用 copy啡省,stringCopy 的值就會(huì)被改變娜睛。 集合類對(duì)象以此類推髓霞。 所以,
用 @property 聲明 NSString畦戒、NSArray方库、NSDictionary 經(jīng)常使用 copy 關(guān)鍵字,是因?yàn)樗麄冇袑?duì)應(yīng)的可變類型:NSMutableString障斋、NSMutableArray薪捍、NSMutableDictionary,他們之間可能進(jìn)行賦值操作配喳,為確保對(duì)象中的字符串值不會(huì)無意間變動(dòng)酪穿,應(yīng)該在設(shè)置新屬性值時(shí)拷貝一份。
2晴裹、集合類對(duì)象的copy與mutableCopy
集合類對(duì)象是指 NSArray被济、NSDictionary、NSSet ... 之類的對(duì)象涧团。下面先看集合類immutable對(duì)象使用 copy 和 mutableCopy 的一個(gè)例子:
NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
查看內(nèi)容只磷,可以看到 copyArray 和 array 的地址是一樣的,而 mCopyArray 和 array 的地址是不同的泌绣。說明 copy 操作進(jìn)行了指針拷貝钮追,mutableCopy 進(jìn)行了內(nèi)容拷貝。但需要強(qiáng)調(diào)的是:此處的內(nèi)容拷貝阿迈,僅僅是拷貝 array 這個(gè)對(duì)象元媚,array 集合內(nèi)部的元素仍然是指針拷貝。這和上面的非集合 immutable 對(duì)象的拷貝還是挺相似的苗沧,那么mutable對(duì)象的拷貝會(huì)不會(huì)類似呢刊棕?我們繼續(xù)往下,看 mutable 對(duì)象拷貝的例子:
NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
查看內(nèi)存待逞,如我們所料甥角,copyArray、mCopyArray和 array 的內(nèi)存地址都不一樣识樱,說明 copyArray嗤无、mCopyArray 都對(duì) array 進(jìn)行了內(nèi)容拷貝。同樣怜庸,我們可以得出結(jié)論:
在集合類對(duì)象中当犯,對(duì) immutable 對(duì)象進(jìn)行 copy,是指針復(fù)制休雌, mutableCopy 是內(nèi)容復(fù)制灶壶;對(duì) mutable 對(duì)象進(jìn)行 copy 和 mutableCopy 都是內(nèi)容復(fù)制。但是:集合對(duì)象的內(nèi)容復(fù)制僅限于對(duì)象本身杈曲,對(duì)象元素仍然是指針復(fù)制驰凛。用代碼簡單表示如下:
[immutableObject copy] // 淺復(fù)制
[immutableObject mutableCopy] //單層深復(fù)制
[mutableObject copy] //單層深復(fù)制
[mutableObject mutableCopy] //單層深復(fù)制
這個(gè)代碼結(jié)論和非集合類的非常相似。
14. @synthesize合成實(shí)例變量的規(guī)則是什么担扑?假如property名為foo恰响,存在一個(gè)名為_foo
的實(shí)例變量,那么還會(huì)自動(dòng)合成新變量么涌献?
在回答之前先說明下一個(gè)概念:
實(shí)例變量 = 成員變量 = ivar
這些說法胚宦,筆者下文中,可能都會(huì)用到燕垃,指的是一個(gè)東西枢劝。
正如 Apple官方文檔 You Can Customize Synthesized Instance Variable Names 所說:
如果使用了屬性的話,那么編譯器就會(huì)自動(dòng)編寫訪問屬性所需的方法卜壕,此過程叫做“自動(dòng)合成”( auto synthesis)您旁。需要強(qiáng)調(diào)的是,這個(gè)過程由編譯器在編譯期執(zhí)行轴捎,所以編輯器里看不到這些“合成方法” (synthesized method)的源代碼鹤盒。除了生成方法代碼之外,編譯器還要自動(dòng)向類中添加適當(dāng)類型的實(shí)例變量侦副,并且在屬性名前面加下劃線侦锯,以此作為實(shí)例變量的名字。
@interface CYLPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
在上例中秦驯,會(huì)生成兩個(gè)實(shí)例變量尺碰,其名稱分別為 _firstName
與 _lastName
。也可以在類的實(shí)現(xiàn)代碼里通過 @synthesize
語法來指定實(shí)例變量的名字:
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
上述語法會(huì)將生成的實(shí)例變量命名為 _myFirstName
與 _myLastName
译隘,而不再使用默認(rèn)的名字葱蝗。一般情況下無須修改默認(rèn)的實(shí)例變量名,但是如果你不喜歡以下劃線來命名實(shí)例變量细燎,那么可以用這個(gè)辦法將其改為自己想要的名字两曼。筆者還是推薦使用默認(rèn)的命名方案,因?yàn)槿绻腥硕紙?jiān)持這套方案玻驻,那么寫出來的代碼大家都能看得懂悼凑。
總結(jié)下 @synthesize 合成實(shí)例變量的規(guī)則,有以下幾點(diǎn):
如果指定了成員變量的名稱,會(huì)生成一個(gè)指定的名稱的成員變量,
如果這個(gè)成員已經(jīng)存在了就不再生成了.
如果是
@synthesize foo;
還會(huì)生成一個(gè)名稱為foo的成員變量璧瞬,也就是說:
如果沒有指定成員變量的名稱會(huì)自動(dòng)生成一個(gè)屬性同名的成員變量,
- 如果是
@synthesize foo = _foo;
就不會(huì)生成成員變量了.
假如 property 名為 foo户辫,存在一個(gè)名為 _foo
的實(shí)例變量,那么還會(huì)自動(dòng)合成新變量么嗤锉? 不會(huì)渔欢。如下圖:
15. 在有了自動(dòng)合成屬性實(shí)例變量之后,@synthesize還有哪些使用場(chǎng)景瘟忱?
回答這個(gè)問題前奥额,我們要搞清楚一個(gè)問題苫幢,什么情況下不會(huì)autosynthesis(自動(dòng)合成)?
- 同時(shí)重寫了 setter 和 getter 時(shí)
- 重寫了只讀屬性的 getter 時(shí)
- 使用了 @dynamic 時(shí)
- 在 @protocol 中定義的所有屬性
- 在 category 中定義的所有屬性
- 重寫(overridden)的屬性
當(dāng)你在子類中重載了父類中的屬性垫挨,你必須 使用 @synthesize
來手動(dòng)合成ivar韩肝。
除了后三條,對(duì)其他幾個(gè)我們可以總結(jié)出一個(gè)規(guī)律:當(dāng)你想手動(dòng)管理 @property 的所有內(nèi)容時(shí)九榔,你就會(huì)嘗試通過實(shí)現(xiàn) @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic
來達(dá)到這個(gè)目的哀峻,這時(shí)編譯器就會(huì)認(rèn)為你打算手動(dòng)管理 @property,于是編譯器就禁用了 autosynthesis(自動(dòng)合成)哲泊。
因?yàn)橛辛?autosynthesis(自動(dòng)合成)剩蟀,大部分開發(fā)者已經(jīng)習(xí)慣不去手動(dòng)定義ivar,而是依賴于 autosynthesis(自動(dòng)合成)切威,但是一旦你需要使用ivar育特,而 autosynthesis(自動(dòng)合成)又失效了,如果不去手動(dòng)定義ivar牢屋,那么你就得借助 @synthesize
來手動(dòng)合成 ivar且预。
其實(shí),@synthesize
語法還有一個(gè)應(yīng)用場(chǎng)景烙无,但是不太建議大家使用:
可以在類的實(shí)現(xiàn)代碼里通過 @synthesize
語法來指定實(shí)例變量的名字:
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
上述語法會(huì)將生成的實(shí)例變量命名為 _myFirstName
與 _myLastName
锋谐,而不再使用默認(rèn)的名字。一般情況下無須修改默認(rèn)的實(shí)例變量名截酷,但是如果你不喜歡以下劃線來命名實(shí)例變量涮拗,那么可以用這個(gè)辦法將其改為自己想要的名字。筆者還是推薦使用默認(rèn)的命名方案迂苛,因?yàn)槿绻腥硕紙?jiān)持這套方案三热,那么寫出來的代碼大家都能看得懂。
舉例說明:應(yīng)用場(chǎng)景:
//
// .m文件
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 打開第14行和第17行中任意一行三幻,就可編譯成功
@import Foundation;
@interface CYLObject : NSObject
@property (nonatomic, copy) NSString *title;
@end
@implementation CYLObject {
// NSString *_title;
}
//@synthesize title = _title;
- (instancetype)init
{
self = [super init];
if (self) {
_title = @"微博@iOS程序犭袁";
}
return self;
}
- (NSString *)title {
return _title;
}
- (void)setTitle:(NSString *)title {
_title = [title copy];
}
@end
當(dāng)你同時(shí)重寫了 setter 和 getter 時(shí)就漾,系統(tǒng)就不會(huì)生成 ivar(實(shí)例變量/成員變量)。這時(shí)候有兩種選擇:
- 要么如第14行:手動(dòng)創(chuàng)建 ivar
- 要么如第17行:使用
@synthesize foo = _foo;
念搬,關(guān)聯(lián) @property 與 ivar抑堡。
更多信息,請(qǐng)戳- 》 When should I use @synthesize explicitly?
16. objc中向一個(gè)nil對(duì)象發(fā)送消息將會(huì)發(fā)生什么朗徊?
在 Objective-C 中向 nil 發(fā)送消息是完全有效的——只是在運(yùn)行時(shí)不會(huì)有任何作用:
- 如果一個(gè)方法返回值是一個(gè)對(duì)象首妖,那么發(fā)送給nil的消息將返回0(nil)。例如:
Person * motherInlaw = [[aPerson spouse] mother];
如果 spouse 對(duì)象為 nil爷恳,那么發(fā)送給 nil 的消息 mother 也將返回 nil有缆。 2. 如果方法返回值為指針類型,其指針大小為小于或者等于sizeof(void*),float棚壁,double杯矩,long double 或者 long long 的整型標(biāo)量,發(fā)送給 nil 的消息將返回0灌曙。 2. 如果方法返回值為結(jié)構(gòu)體,發(fā)送給 nil 的消息將返回0菊碟。結(jié)構(gòu)體中各個(gè)字段的值將都是0节芥。 2. 如果方法的返回值不是上述提到的幾種情況在刺,那么發(fā)送給 nil 的消息的返回值將是未定義的。
具體原因如下:
objc是動(dòng)態(tài)語言头镊,每個(gè)方法在運(yùn)行時(shí)會(huì)被動(dòng)態(tài)轉(zhuǎn)為消息發(fā)送蚣驼,即:objc_msgSend(receiver, selector)。
那么相艇,為了方便理解這個(gè)內(nèi)容颖杏,還是貼一個(gè)objc的源代碼:
// runtime.h(類在runtime中的定義)
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指針指向Meta Class,因?yàn)镺bjc的類的本身也是一個(gè)Object坛芽,為了處理這個(gè)關(guān)系留储,runtime就創(chuàng)造了Meta Class,當(dāng)給類發(fā)送[NSObject alloc]這樣消息時(shí)咙轩,實(shí)際上是把這個(gè)消息發(fā)給了Class Object
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父類
const char *name OBJC2_UNAVAILABLE; // 類名
long version OBJC2_UNAVAILABLE; // 類的版本信息获讳,默認(rèn)為0
long info OBJC2_UNAVAILABLE; // 類信息,供運(yùn)行期使用的一些位標(biāo)識(shí)
long instance_size OBJC2_UNAVAILABLE; // 該類的實(shí)例變量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 該類的成員變量鏈表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定義的鏈表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法緩存活喊,對(duì)象接到一個(gè)消息會(huì)根據(jù)isa指針查找消息對(duì)象丐膝,這時(shí)會(huì)在method Lists中遍歷,如果cache了钾菊,常用的方法調(diào)用時(shí)就能夠提高調(diào)用的效率帅矗。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 協(xié)議鏈表
#endif
} OBJC2_UNAVAILABLE;
objc在向一個(gè)對(duì)象發(fā)送消息時(shí),runtime庫會(huì)根據(jù)對(duì)象的isa指針找到該對(duì)象實(shí)際所屬的類煞烫,然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行浑此,然后在發(fā)送消息的時(shí)候,objc_msgSend方法不會(huì)返回值滞详,所謂的返回內(nèi)容都是具體調(diào)用時(shí)執(zhí)行的莽使。 那么,回到本題做盅,如果向一個(gè)nil對(duì)象發(fā)送消息匣缘,首先在尋找對(duì)象的isa指針時(shí)就是0地址返回了,所以不會(huì)出現(xiàn)任何錯(cuò)誤稀火。
17. objc中向一個(gè)對(duì)象發(fā)送消息[obj foo]和objc_msgSend()
函數(shù)之間有什么關(guān)系暖哨?
具體原因同上題:該方法編譯之后就是objc_msgSend()
函數(shù)調(diào)用.
我們用 clang 分析下,clang 提供一個(gè)命令,可以將Objective-C的源碼改寫成C++語言篇裁,借此可以研究下[obj foo]和objc_msgSend()
函數(shù)之間有什么關(guān)系沛慢。
以下面的代碼為例,由于 clang 后的代碼達(dá)到了10萬多行达布,為了便于區(qū)分团甲,添加了一個(gè)叫 iOSinit 方法,
//
// main.m
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.
//
#import "CYLTest.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
CYLTest *test = [[CYLTest alloc] init];
[test performSelector:(@selector(iOSinit))];
return 0;
}
}
在終端中輸入
clang -rewrite-objc main.m
就可以生成一個(gè)main.cpp
的文件黍聂,在最低端(10萬4千行左右)
我們可以看到大概是這樣的:
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));
也就是說:
[obj foo];在objc編譯時(shí)躺苦,會(huì)被轉(zhuǎn)意為:
objc_msgSend(obj, @selector(foo));
。
18. 什么時(shí)候會(huì)報(bào)unrecognized selector的異常产还?
簡單來說:
當(dāng)調(diào)用該對(duì)象上某個(gè)方法,而該對(duì)象上沒有實(shí)現(xiàn)這個(gè)方法的時(shí)候匹厘, 可以通過“消息轉(zhuǎn)發(fā)”進(jìn)行解決。
簡單的流程如下脐区,在上一題中也提到過:
objc是動(dòng)態(tài)語言愈诚,每個(gè)方法在運(yùn)行時(shí)會(huì)被動(dòng)態(tài)轉(zhuǎn)為消息發(fā)送,即:objc_msgSend(receiver, selector)牛隅。
objc在向一個(gè)對(duì)象發(fā)送消息時(shí)炕柔,runtime庫會(huì)根據(jù)對(duì)象的isa指針找到該對(duì)象實(shí)際所屬的類,然后在該類中的方法列表以及其父類方法列表中尋找方法運(yùn)行媒佣,如果匕累,在最頂層的父類中依然找不到相應(yīng)的方法時(shí),程序在運(yùn)行時(shí)會(huì)掛掉并拋出異常unrecognized selector sent to XXX 丈攒。但是在這之前哩罪,objc的運(yùn)行時(shí)會(huì)給出三次拯救程序崩潰的機(jī)會(huì):
- Method resolution
objc運(yùn)行時(shí)會(huì)調(diào)用+resolveInstanceMethod:
或者 +resolveClassMethod:
,讓你有機(jī)會(huì)提供一個(gè)函數(shù)實(shí)現(xiàn)巡验。如果你添加了函數(shù)际插,那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過程,否則 显设,運(yùn)行時(shí)就會(huì)移到下一步框弛,消息轉(zhuǎn)發(fā)(Message Forwarding)。
- Fast forwarding
如果目標(biāo)對(duì)象實(shí)現(xiàn)了-forwardingTargetForSelector:
捕捂,Runtime 這時(shí)就會(huì)調(diào)用這個(gè)方法瑟枫,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他對(duì)象的機(jī)會(huì)。 只要這個(gè)方法返回的不是nil和self指攒,整個(gè)消息發(fā)送的過程就會(huì)被重啟慷妙,當(dāng)然發(fā)送的對(duì)象會(huì)變成你返回的那個(gè)對(duì)象。否則允悦,就會(huì)繼續(xù)Normal Fowarding膝擂。 這里叫Fast,只是為了區(qū)別下一步的轉(zhuǎn)發(fā)機(jī)制。因?yàn)檫@一步不會(huì)創(chuàng)建任何新的對(duì)象架馋,但下一步轉(zhuǎn)發(fā)會(huì)創(chuàng)建一個(gè)NSInvocation對(duì)象狞山,所以相對(duì)更快點(diǎn)。 3. Normal forwarding
這一步是Runtime最后一次給你挽救的機(jī)會(huì)叉寂。首先它會(huì)發(fā)送-methodSignatureForSelector:
消息獲得函數(shù)的參數(shù)和返回值類型萍启。如果-methodSignatureForSelector:
返回nil,Runtime則會(huì)發(fā)出-doesNotRecognizeSelector:
消息屏鳍,程序這時(shí)也就掛掉了勘纯。如果返回了一個(gè)函數(shù)簽名,Runtime就會(huì)創(chuàng)建一個(gè)NSInvocation對(duì)象并發(fā)送-forwardInvocation:
消息給目標(biāo)對(duì)象孕蝉。
為了能更清晰地理解這些方法的作用屡律,git倉庫里也給出了一個(gè)Demo腌逢,名稱叫“ _objc_msgForward_demo
”,可運(yùn)行起來看看降淮。
19. 一個(gè)objc對(duì)象如何進(jìn)行內(nèi)存布局?(考慮有父類的情況)
- 所有父類的成員變量和自己的成員變量都會(huì)存放在該對(duì)象所對(duì)應(yīng)的存儲(chǔ)空間中.
- 每一個(gè)對(duì)象內(nèi)部都有一個(gè)isa指針,指向他的類對(duì)象,類對(duì)象中存放著本對(duì)象的
- 對(duì)象方法列表(對(duì)象能夠接收的消息列表搏讶,保存在它所對(duì)應(yīng)的類對(duì)象中)
- 成員變量的列表,
- 屬性列表,
它內(nèi)部也有一個(gè)isa指針指向元對(duì)象(meta class),元對(duì)象內(nèi)部存放的是類方法列表,類對(duì)象內(nèi)部還有一個(gè)superclass的指針,指向他的父類對(duì)象佳鳖。
每個(gè) Objective-C 對(duì)象都有相同的結(jié)構(gòu),如下圖所示:
翻譯過來就是
Objective-C 對(duì)象的結(jié)構(gòu)圖 |
---|
ISA指針 |
根類的實(shí)例變量 |
倒數(shù)第二層父類的實(shí)例變量 |
... |
父類的實(shí)例變量 |
類的實(shí)例變量 |
根對(duì)象就是NSObject媒惕,它的superclass指針指向nil
類對(duì)象既然稱為對(duì)象系吩,那它也是一個(gè)實(shí)例。類對(duì)象中也有一個(gè)isa指針指向它的元類(meta class)妒蔚,即類對(duì)象是元類的實(shí)例穿挨。元類內(nèi)部存放的是類方法列表,根元類的isa指針指向自己肴盏,superclass指針指向NSObject類科盛。
20. 一個(gè)objc對(duì)象的isa的指針指向什么?有什么作用菜皂?
指向他的類對(duì)象,從而可以找到對(duì)象上的方法
21. 下面的代碼輸出什么贞绵?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:
都輸出 Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
這個(gè)題目主要是考察關(guān)于 Objective-C 中對(duì) self 和 super 的理解。
我們都知道:self 是類的隱藏參數(shù)恍飘,指向當(dāng)前調(diào)用方法的這個(gè)類的實(shí)例榨崩。那 super 呢?
很多人會(huì)想當(dāng)然的認(rèn)為“ super 和 self 類似章母,應(yīng)該是指向父類的指針吧母蛛!”。這是很普遍的一個(gè)誤區(qū)乳怎。其實(shí) super 是一個(gè) Magic Keyword彩郊, 它本質(zhì)是一個(gè)編譯器標(biāo)示符,和 self 是指向的同一個(gè)消息接受者!他們兩個(gè)的不同點(diǎn)在于:super 會(huì)告訴編譯器焦辅,調(diào)用 class 這個(gè)方法時(shí)博杖,要去父類的方法,而不是本類里的筷登。
上面的例子不管調(diào)用[self class]
還是[super class]
剃根,接受消息的對(duì)象都是當(dāng)前 Son *xxx
這個(gè)對(duì)象。
當(dāng)使用 self 調(diào)用方法時(shí)前方,會(huì)從當(dāng)前類的方法列表中開始找狈醉,如果沒有,就從父類中再找惠险;而當(dāng)使用 super 時(shí)苗傅,則從父類的方法列表中開始找。然后調(diào)用父類的這個(gè)方法班巩。
這也就是為什么說“不推薦在 init 方法中使用點(diǎn)語法”渣慕,如果想訪問實(shí)例變量 iVar 應(yīng)該使用下劃線( _iVar
),而非點(diǎn)語法( self.iVar
)抱慌。
點(diǎn)語法( self.iVar
)的壞處就是子類有可能覆寫 setter 逊桦。假設(shè) Person 有一個(gè)子類叫 ChenPerson,這個(gè)子類專門表示那些姓“陳”的人抑进。該子類可能會(huì)覆寫 lastName 屬性所對(duì)應(yīng)的設(shè)置方法:
//
// ChenPerson.m
//
//
// Created by https://github.com/ChenYilong on 15/8/30.
// Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.
//
#import "ChenPerson.h"
@implementation ChenPerson
@synthesize lastName = _lastName;
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"??類名與方法名:%s(在第%d行)强经,描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
NSLog(@"??類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([super class]));
}
return self;
}
- (void)setLastName:(NSString*)lastName
{
//設(shè)置方法一:如果setter采用是這種方式寺渗,就可能引起崩潰
// if (![lastName isEqualToString:@"陳"])
// {
// [NSException raise:NSInvalidArgumentException format:@"姓不是陳"];
// }
// _lastName = lastName;
//設(shè)置方法二:如果setter采用是這種方式匿情,就可能引起崩潰
_lastName = @"陳";
NSLog(@"??類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, @"會(huì)調(diào)用這個(gè)方法,想一下為什么信殊?");
}
@end
在基類 Person 的默認(rèn)初始化方法中炬称,可能會(huì)將姓氏設(shè)為空字符串。此時(shí)若使用點(diǎn)語法( self.lastName
)也即 setter 設(shè)置方法鸡号,那么調(diào)用將會(huì)是子類的設(shè)置方法转砖,如果在剛剛的 setter 代碼中采用設(shè)置方法一,那么就會(huì)拋出異常鲸伴,
為了方便采用打印的方式展示府蔗,究竟發(fā)生了什么,我們使用設(shè)置方法二汞窗。
如果基類的代碼是這樣的:
//
// Person.m
// nil對(duì)象調(diào)用點(diǎn)語法
//
// Created by https://github.com/ChenYilong on 15/8/29.
// Copyright (c) 2015年 http://weibo.com/luohanchenyilong/ 微博@iOS程序犭袁. All rights reserved.
//
#import "Person.h"
@implementation Person
- (instancetype)init
{
self = [super init];
if (self) {
self.lastName = @"";
//NSLog(@"??類名與方法名:%s(在第%d行)姓赤,描述:%@", __PRETTY_FUNCTION__, __LINE__, NSStringFromClass([self class]));
//NSLog(@"??類名與方法名:%s(在第%d行),描述:%@", __PRETTY_FUNCTION__, __LINE__, self.lastName);
}
return self;
}
- (void)setLastName:(NSString*)lastName
{
NSLog(@"??類名與方法名:%s(在第%d行)仲吏,描述:%@", __PRETTY_FUNCTION__, __LINE__, @"根本不會(huì)調(diào)用這個(gè)方法");
_lastName = @"炎黃";
}
@end
那么打印結(jié)果將會(huì)是這樣的:
??類名與方法名:-[ChenPerson setLastName:](在第36行)不铆,描述:會(huì)調(diào)用這個(gè)方法,想一下為什么蝌焚?
??類名與方法名:-[ChenPerson init](在第19行),描述:ChenPerson
??類名與方法名:-[ChenPerson init](在第20行)誓斥,描述:ChenPerson
我在倉庫里也給出了一個(gè)相應(yīng)的 Demo(名字叫:Demo_21題_下面的代碼輸出什么)只洒。有興趣可以跑起來看一下,主要看下他是怎么打印的劳坑,思考下為什么這么打印毕谴。
接下來讓我們利用 runtime 的相關(guān)知識(shí)來驗(yàn)證一下 super 關(guān)鍵字的本質(zhì),使用clang重寫命令:
$ clang -rewrite-objc test.m
將這道題目中給出的代碼被轉(zhuǎn)化為:
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass("Son")) }, sel_registerName("class"))));
從上面的代碼中距芬,我們可以發(fā)現(xiàn)在調(diào)用 [self class] 時(shí)涝开,會(huì)轉(zhuǎn)化成 objc_msgSend
函數(shù)】蜃校看下函數(shù)定義:
id objc_msgSend(id self, SEL op, ...)
我們把 self 做為第一個(gè)參數(shù)傳遞進(jìn)去舀武。
而在調(diào)用 [super class]時(shí),會(huì)轉(zhuǎn)化成 objc_msgSendSuper
函數(shù)离斩∫眨看下函數(shù)定義:
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一個(gè)參數(shù)是 objc_super
這樣一個(gè)結(jié)構(gòu)體,其定義如下:
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
結(jié)構(gòu)體有兩個(gè)成員捐腿,第一個(gè)成員是 receiver, 類似于上面的 objc_msgSend
函數(shù)第一個(gè)參數(shù)self 纵朋。第二個(gè)成員是記錄當(dāng)前類的父類是什么。
所以茄袖,當(dāng)調(diào)用 [self class] 時(shí),實(shí)際先調(diào)用的是 objc_msgSend
函數(shù)嘁锯,第一個(gè)參數(shù)是 Son當(dāng)前的這個(gè)實(shí)例宪祥,然后在 Son 這個(gè)類里面去找 - (Class)class這個(gè)方法,沒有家乘,去父類 Father里找蝗羊,也沒有,最后在 NSObject類中發(fā)現(xiàn)這個(gè)方法仁锯。而 - (Class)class的實(shí)現(xiàn)就是返回self的類別耀找,故上述輸出結(jié)果為 Son。
objc Runtime開源代碼對(duì)- (Class)class方法的實(shí)現(xiàn):
- (Class)class {
return object_getClass(self);
}
而當(dāng)調(diào)用 [super class]
時(shí)业崖,會(huì)轉(zhuǎn)換成objc_msgSendSuper函數(shù)
野芒。第一步先構(gòu)造 objc_super
結(jié)構(gòu)體,結(jié)構(gòu)體第一個(gè)成員就是 self
双炕。 第二個(gè)成員是 (id)class_getSuperclass(objc_getClass(“Son”))
, 實(shí)際該函數(shù)輸出結(jié)果為 Father狞悲。
第二步是去 Father這個(gè)類里去找 - (Class)class
,沒有妇斤,然后去NSObject類去找摇锋,找到了丹拯。最后內(nèi)部是使用 objc_msgSend(objc_super->receiver, @selector(class))
去調(diào)用,
此時(shí)已經(jīng)和[self class]
調(diào)用相同了荸恕,故上述輸出結(jié)果仍然返回 Son乖酬。
參考鏈接:微博@Chun_iOS的博文刨根問底Objective-C Runtime(1)- Self & Super
22. runtime如何通過selector找到對(duì)應(yīng)的IMP地址?(分別考慮類方法和實(shí)例方法)
每一個(gè)類對(duì)象中都一個(gè)方法列表,方法列表中記錄著方法的名稱,方法實(shí)現(xiàn),以及參數(shù)類型,其實(shí)selector本質(zhì)就是方法名稱,通過這個(gè)方法名稱就可以在方法列表中找到對(duì)應(yīng)的方法實(shí)現(xiàn).
23. 使用runtime Associate方法關(guān)聯(lián)的對(duì)象融求,需要在主對(duì)象dealloc的時(shí)候釋放么剑刑?
在ARC下不需要。
-
在MRC中,對(duì)于使用retain或copy策略的需要 双肤。在MRC下也不需要
無論在MRC下還是ARC下均不需要施掏。
2011年版本的Apple API 官方文檔 - Associative References 一節(jié)中有一個(gè)MRC環(huán)境下的例子:
// 在MRC下,使用runtime Associate方法關(guān)聯(lián)的對(duì)象茅糜,不需要在主對(duì)象dealloc的時(shí)候釋放
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 摘自2011年版本的Apple API 官方文檔 - Associative References
static char overviewKey;
NSArray *array =
[[NSArray alloc] initWithObjects:@"One", @"Two", @"Three", nil];
// For the purposes of illustration, use initWithFormat: to ensure
// the string can be deallocated
NSString *overview =
[[NSString alloc] initWithFormat:@"%@", @"First three numbers"];
objc_setAssociatedObject (
array,
&overviewKey,
overview,
OBJC_ASSOCIATION_RETAIN
);
[overview release];
// (1) overview valid
[array release];
// (2) overview invalid
文檔指出
At point 1, the string
overview
is still valid because theOBJC_ASSOCIATION_RETAIN
policy specifies that the array retains the associated object. When the array is deallocated, however (at point 2),overview
is released and so in this case also deallocated.
我們可以看到七芭,在[array release];
之后,overview就會(huì)被release釋放掉了蔑赘。
既然會(huì)被銷毀狸驳,那么具體在什么時(shí)間點(diǎn)?
根據(jù) WWDC 2011, Session 322 (第36分22秒) 中發(fā)布的內(nèi)存銷毀時(shí)間表缩赛,被關(guān)聯(lián)的對(duì)象在生命周期內(nèi)要比對(duì)象本身釋放的晚很多耙箍。它們會(huì)在被 NSObject -dealloc 調(diào)用的 object_dispose() 方法中釋放。
對(duì)象的內(nèi)存銷毀時(shí)間表酥馍,分四個(gè)步驟:
// 對(duì)象的內(nèi)存銷毀時(shí)間表
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 根據(jù) WWDC 2011, Session 322 (36分22秒)中發(fā)布的內(nèi)存銷毀時(shí)間表
1. 調(diào)用 -release :引用計(jì)數(shù)變?yōu)榱? * 對(duì)象正在被銷毀辩昆,生命周期即將結(jié)束.
* 不能再有新的 __weak 弱引用, 否則將指向 nil.
* 調(diào)用 [self dealloc]
2. 子類 調(diào)用 -dealloc
* 繼承關(guān)系中最底層的子類 在調(diào)用 -dealloc
* 如果是 MRC 代碼 則會(huì)手動(dòng)釋放實(shí)例變量們(iVars)
* 繼承關(guān)系中每一層的父類 都在調(diào)用 -dealloc
3. NSObject 調(diào) -dealloc
* 只做一件事:調(diào)用 Objective-C runtime 中的 object_dispose() 方法
4. 調(diào)用 object_dispose()
* 為 C++ 的實(shí)例變量們(iVars)調(diào)用 destructors
* 為 ARC 狀態(tài)下的 實(shí)例變量們(iVars) 調(diào)用 -release
* 解除所有使用 runtime Associate方法關(guān)聯(lián)的對(duì)象
* 解除所有 __weak 引用
* 調(diào)用 free()
對(duì)象的內(nèi)存銷毀時(shí)間表:參考鏈接旨袒。
24. objc中的類方法和實(shí)例方法有什么本質(zhì)區(qū)別和聯(lián)系汁针?
類方法:
- 類方法是屬于類對(duì)象的
- 類方法只能通過類對(duì)象調(diào)用
- 類方法中的self是類對(duì)象
- 類方法可以調(diào)用其他的類方法
- 類方法中不能訪問成員變量
- 類方法中不能直接調(diào)用對(duì)象方法
實(shí)例方法:
- 實(shí)例方法是屬于實(shí)例對(duì)象的
- 實(shí)例方法只能通過實(shí)例對(duì)象調(diào)用
- 實(shí)例方法中的self是實(shí)例對(duì)象
- 實(shí)例方法中可以訪問成員變量
- 實(shí)例方法中直接調(diào)用實(shí)例方法
- 實(shí)例方法中也可以調(diào)用類方法(通過類名)