聲明:當(dāng)時(shí)覺得這篇文章寫的比較好,在此做了copy,原文分為上下篇,在此合為了一篇,原文鏈接地址是:原文地址
《招聘一個(gè)靠譜的 iOS》—參考答案(上,下)
說明:面試題來源是微博@我就叫Sunny怎么了的這篇博文:《招聘一個(gè)靠譜的 iOS》,其中共55題,除第一題為糾錯(cuò)題外,其他54道均為簡(jiǎn)答題。
博文中給出了高質(zhì)量的面試題,但是未給出答案,我嘗試著總結(jié)了下答案,分兩篇發(fā):這是上篇 ,下一篇文章將發(fā)布在這里,會(huì)把剩余問題總結(jié)下,并且進(jìn)行勘誤,歡迎各位指正文中的錯(cuò)誤。請(qǐng)持續(xù)關(guān)注微博@iOS程序犭袁。(答案未經(jīng)出題者校對(duì),如有紕漏,請(qǐng)向微博@iOS程序犭袁指正。)
出題者簡(jiǎn)介: 孫源(sunnyxx),目前就職于百度,負(fù)責(zé)百度知道 iOS 客戶端的開發(fā)工作,對(duì)技術(shù)喜歡刨根問底和總結(jié)最佳實(shí)踐,熱愛分享和開源,維護(hù)一個(gè)叫 forkingdog 的開源小組。
1. 風(fēng)格糾錯(cuò)題

修改方法有很多種,現(xiàn)給出一種做示例:
下面對(duì)具體修改的地方,分兩部分做下介紹:硬傷部分和優(yōu)化部分 。因?yàn)橛矀糠譀]什么技術(shù)含量,為了節(jié)省大家時(shí)間,放在后面講,大神請(qǐng)直接看優(yōu)化部分。
優(yōu)化部分
1)enum建議使用 NS_ENUM 和 NS_OPTIONS 宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:
1 2 3 4 5 | //定義一個(gè)枚舉typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman}; |
2)age屬性的類型:應(yīng)避免使用基本類型,建議使Foundation數(shù)據(jù)類型,對(duì)應(yīng)關(guān)系如下:
1 2 3 4 | int -> NSInteger unsigned -> NSUInteger float -> CGFloat 動(dòng)畫時(shí)間 -> NSTimeInterval |
同時(shí)考慮到age的特點(diǎn),應(yīng)使用NSUInteger,而非int。 這樣做的是基于64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》。
3)如果工程項(xiàng)目非常龐大,需要拆分成不同的模塊,可以在類、typedef宏命名的時(shí)候使用前綴。
4)doLogIn方法不應(yīng)寫在該類中:雖然LogIn的命名不太清晰,但筆者猜測(cè)是login的意思,而登錄操作屬于業(yè)務(wù)邏輯,觀察類名UserModel,以及屬性的命名方式,應(yīng)該使用的是MVC模式,并非MVVM,在MVC中業(yè)務(wù)邏輯不應(yīng)當(dāng)寫在Model中。(如果是MVVM,拋開命名規(guī)范,UserModel這個(gè)類可能對(duì)應(yīng)的是用戶注冊(cè)頁面,如果有特殊的業(yè)務(wù)需求,比如:login對(duì)應(yīng)的應(yīng)當(dāng)是注冊(cè)并登錄的一個(gè)Button,出現(xiàn)login方法也可能是合理的。)
5)doLogIn方法命名不規(guī)范:添加了多余的動(dòng)詞前綴。 請(qǐng)牢記:
如果方法表示讓對(duì)象執(zhí)行一個(gè)動(dòng)作,使用動(dòng)詞打頭來命名,注意不要使用do,does這種多余的關(guān)鍵字,動(dòng)詞本身的暗示就足夠了。
6)-(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ù),比如下面的:
1 2 3 4 5 6 | //錯(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; |
7)由于字符串值可能會(huì)改變,所以要把相關(guān)屬性的“內(nèi)存管理語義”聲明為copy。(原因在下文有詳細(xì)論述:用@PRoperty聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?)
8)“性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用于設(shè)置“姓名”(Name)和“年齡”(Age)的初始值,那如何對(duì)“性別”(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的參數(shù),secondary 初始化方法是一個(gè)或多個(gè),并且提供一個(gè)或者更多的默認(rèn)參數(shù)來調(diào)用 designated 初始化方法的初始化方法。舉例說明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // .m文件 // @implementation CYLUser - (instancetype)initWithName:(NSString *)name age:(int)age sex:(CYLSex)sex { if(self = [super init]) { _name = [name copy]; _age = age; _sex = sex; } return self; } - (instancetype)initWithName:(NSString *)name age:(int)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中暴露 designated 初始化方法,是為了方便子類化 (想了解更多,請(qǐng)戳--》 《禪與 Objective-C 編程藝術(shù) (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》。)
9)按照接口設(shè)計(jì)的慣例,如果設(shè)計(jì)了“初始化方法” (initializer),也應(yīng)當(dāng)搭配一個(gè)快捷構(gòu)造方法。而快捷構(gòu)造方法的返回值,建議為instancetype,為保持一致性,init方法和快捷構(gòu)造方法的返回類型最好都用instancetype。
10)如果基于第一種修改方法:既然該類中已經(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)存管理語義”。于是可以把屬性的定義改成這樣
1 2 3 | @property (nonatomic, copy, readonly) NSString *name;@property (nonatomic, assign, readonly) NSUInter age;@property (nonatomic, assign, readonly) CYLSex sex; |
由于是只讀屬性,所以編譯器不會(huì)為其創(chuàng)建對(duì)應(yīng)的“設(shè)置方法”,即便如此,我們還是要寫上這些屬性的語義,以此表明初始化方法在設(shè)置這些屬性值時(shí)所用的方式。要是不寫明語義的話,該類的調(diào)用者就不知道初始化方法里會(huì)拷貝這些屬性,他們有可能會(huì)在調(diào)用初始化方法之前自行拷貝屬性值。這種操作多余而且低效。
11)initUserModelWithUserName如果改為initWithName會(huì)更加簡(jiǎn)潔,而且足夠清晰。
12)UserModel如果改為User會(huì)更加簡(jiǎn)潔,而且足夠清晰。
13)UserSex如果改為Sex會(huì)更加簡(jiǎn)潔,而且足夠清晰。
硬傷部分
1)在-和(void)之間應(yīng)該有一個(gè)空格
2)enum中駝峰命名法和下劃線命名法混用錯(cuò)誤:枚舉類型的命名規(guī)則和函數(shù)的命名規(guī)則相同:命名時(shí)使用駝峰命名法,勿使用下劃線命名法。
3)enum左括號(hào)前加一個(gè)空格,或者將左括號(hào)換到下一行
4)enum右括號(hào)后加一個(gè)空格
5)UserModel :NSObject 應(yīng)為UserModel : NSObject,也就是:右側(cè)少了一個(gè)空格。
6)@interface與@property屬性聲明中間應(yīng)當(dāng)間隔一行。
7)兩個(gè)方法定義之間不需要換行,有時(shí)為了區(qū)分方法的功能也可間隔一行,但示例代碼中間隔了兩行。
8)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數(shù)之間多了空格。而且- 與(id)之間少了空格。
9)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名與參數(shù)之間多了空格:(NSString*)name前多了空格。
10)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中(NSString*)name,應(yīng)為(NSString *)name,少了空格。
11)doLogIn方法命名不清晰:筆者猜測(cè)是login的意思,應(yīng)該是粗心手誤造成的。
12)第二個(gè)@property中assign和nonatomic調(diào)換位置。
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)引用,沒有必要再?gòu)?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 等)的簡(jiǎn)單賦值操作。
2)assigin 可以用非OC對(duì)象,而weak必須用于OC對(duì)象
3. 怎么用 copy 關(guān)鍵字?
用途:
1)NSString、NSArray、NSDictionary 等等經(jīng)常使用copy關(guān)鍵字,是因?yàn)樗麄冇袑?duì)應(yīng)的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
2)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此特質(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)閏opy就是復(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)鍵字?》也有論述。
第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)NSCopyiog與NSMutableCopying協(xié)議。
具體步驟:
1)需聲明該類遵從NSCopying協(xié)議
2)實(shí)現(xiàn)NSCopying協(xié)議。該協(xié)議只有一個(gè)方法:
1 | - (id)copyWithZone: (NSZone*) zone |
注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實(shí)真正需要實(shí)現(xiàn)的卻是“copyWithZone”方法。
以第一題的代碼為例:
然后實(shí)現(xiàn)協(xié)議中規(guī)定的方法:
但在實(shí)際的項(xiàng)目中,不可能這么簡(jiǎn)單,遇到更復(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)此功能所需的全部代碼:
// .m文件

以上做法能滿足基本的需求,但是也有缺陷:如果你所寫的對(duì)象需要深拷貝,那么可考慮新增一個(gè)專門執(zhí)行深拷貝的方法。
【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?如果改用strong關(guān)鍵字,可能造成什么問題?】
在例子中,存放朋友對(duì)象的set是用“copyWithZooe:”方法來拷貝的,這種淺拷貝方式不會(huì)逐個(gè)復(fù)制set中的元素。若需要深拷貝的話,則可像下面這樣,編寫一個(gè)專供深拷貝所用的方法:
1 2 3 4 5 6 7 8 9 | - (id)deepCopy { CYLUser *copy = [[[self copy] allocWithZone:zone] initWithName:_name age:_age sex:sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy;} |
至于如何重寫帶 copy 關(guān)鍵字的 setter這個(gè)問題,
如果拋開本例來回答的話,如下:
1 2 3 | - (void)setName:(NSString *)name { _name = [name copy];} |
如果單單就上文的代碼而言,我們不需要也不能重寫name的 setter :由于是name是只讀屬性,所以編譯器不會(huì)為其創(chuàng)建對(duì)應(yīng)的“設(shè)置方法”,用初始化方法設(shè)置好屬性值之后,就不能再改變了。( 在本例中,之所以還要聲明屬性的“內(nèi)存管理語義”--copy,是因?yàn)椋喝绻粚慶opy,該類的調(diào)用者就不知道初始化方法里會(huì)拷貝這些屬性,他們有可能會(huì)在調(diào)用初始化方法之前自行拷貝屬性值。這種操作多余而低效。)。
那如何確保name被copy?在初始化方法(initializer)中做:
1 2 3 4 5 6 7 8 9 10 11 | - (instancetype)initWithName:(NSString *)name age:(int)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è)類:
1 2 3 4 | @interface Person : NSObject @property NSString *firstName; @property NSString *lastName; @end |
上述代碼寫出來的類與下面這種寫法等效:
1 2 3 4 5 6 | @interface Person : NSObject - (NSString *)firstName; - (void)setFirstName:(NSString *)firstName; - (NSString *)lastName; - (void)setLastName:(NSString *)lastName; @end |
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í)例變量的名字.
1 2 3 4 | @implementation Person @synthesize firstName = _myFirstName; @synthesize lastName = myLastName; @end |
我為了搞清屬性是怎么實(shí)現(xiàn)的,曾經(jīng)反編譯過相關(guān)的代碼,大致生成了五個(gè)東西:
1)OBJC_IVAR_$類名$屬性名稱 :該屬性的“偏移量” (offset),這個(gè)偏移量是“硬編碼” (hardcode),表示該變量距離存放對(duì)象的內(nèi)存區(qū)域的起始地址有多遠(yuǎn)。
2)setter與getter方法對(duì)應(yīng)的實(shí)現(xiàn)函數(shù)
3)ivar_list :成員變量列表
4)method_list :方法列表
5)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
1)在protocol中使用property只會(huì)生成setter和getter方法聲明,我們使用屬性的目的,是希望遵守我協(xié)議的對(duì)象能實(shí)現(xiàn)該屬性
2)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。
我們可以設(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屬性”:
1 2 3 4 5 6 7 | // 使用偽代碼模擬:runtime如何實(shí)現(xiàn)weak屬性 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ù)。
1 2 | 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ù)。
1 | objc_storeWeak(&obj1, 0); |
前面的源代碼與下列源代碼相同。
1 2 3 4 5 6 7 8 | // 使用偽代碼模擬:runtime如何實(shí)現(xiàn)weak屬性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方法入手:
1 2 3 4 5 6 7 | - (void)setObject:(NSObject *)object{ objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }];} |
也就是有兩個(gè)步驟:
1)在setter方法中做如下設(shè)置:
1 | objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); |
2)在屬性所指的對(duì)象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。做到這點(diǎn),同樣要借助runtime:
1 2 3 4 5 6 7 8 | //要銷毀的目標(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í)行“事件”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | // .h文件// 這個(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文件// 這個(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方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // CYLNSObject+RunAtDealloc.h文件// 利用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文件// 利用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)入
1 | #import "CYLNSObject+RunAtDealloc.h" |
然后就可以使用了:
1 2 3 4 | NSObject *foo = [[NSObject alloc] init]; [foo cyl_runAtDealloc:^{ NSLog(@"正在釋放foo!"); }]; |
如果對(duì)cyl_runAtDealloc的實(shí)現(xiàn)原理有興趣,可以看下這篇博文 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(讀寫)、readooly (只讀)
內(nèi)存管理語義---assign、strong、 weak、unsafe_unretained、copy
方法名---getter=、setter=
getter=的樣式:
1 | @property (nonatomic, getter=isOn) BOOL on; |
( setter=這種不常用,也不推薦使用。故不在這里給出寫法。)
不常用的:nonnull,null_resettable,nullable
10. weak屬性需要在dealloc中置nil么?
不需要。
在ARC環(huán)境無論是強(qiáng)指針還是弱指針都無需在deallco設(shè)置為nil,ARC會(huì)自動(dòng)幫我們處理。
即便是編譯器不幫我們做這些,weak也不需要在dealloc中置nil:
正如上文的:runtime 如何實(shí)現(xiàn) weak 屬性 中提到的:
我們模擬下weak的setter方法,應(yīng)該如下:
1 2 3 4 5 6 7 | - (void)setObject:(NSObject *)object{ objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }];} |
也即:在屬性所指的對(duì)象遭到摧毀時(shí),屬性值也會(huì)清空(nil out)。
11. @synthesize和@dynamic分別有什么作用?
1)@property有兩個(gè)對(duì)應(yīng)的詞,一個(gè)是@synthesize,一個(gè)是@dynamic。如果@synthesize和@dynamic都沒寫,那么默認(rèn)的就是@syntheszie var = _var;
2)@synthesize的語義是如果你沒有手動(dòng)實(shí)現(xiàn)setter方法和getter方法,那么編譯器會(huì)自動(dòng)為你加上這兩個(gè)方法。
3)@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
對(duì)于普通的OC對(duì)象
atomic,readwrite,strong
參考鏈接:
13. 用@property聲明的NSString(或NSArray,NSDictionary)經(jīng)常使用copy關(guān)鍵字,為什么?如果改用strong關(guān)鍵字,可能造成什么問題?
1)因?yàn)楦割愔羔樋梢灾赶蜃宇悓?duì)象,使用copy的目的是為了讓本對(duì)象的屬性不受外界影響,使用copy無論給我傳入是一個(gè)可變對(duì)象還是不可對(duì)象,我本身持有的就是一個(gè)不可變的副本.
2)如果我們使用是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í)拷貝一份。
為了理解這種做法,首先要知道,對(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ù)制。用代碼簡(jiǎn)單表示如下:
[immutableObject copy] // 淺復(fù)制
[immutableObject mutableCopy] //深復(fù)制
[mutableObject copy] //深復(fù)制
[mutableObject mutableCopy] //深復(fù)制
比如以下代碼:
1 2 | NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copyNSString *stringCopy = [string copy]; |
查看內(nèi)存,會(huì)發(fā)現(xiàn) string、stringCopy 內(nèi)存地址都不一樣,說明此時(shí)都是做內(nèi)容拷貝、深拷貝。即使你進(jìn)行如下操作:
1 | [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í)拷貝一份。
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í)例變量的名字。
1 2 3 4 | @interface CYLPerson : NSObject @property NSString *firstName; @property NSString *lastName; @end |
在上例中,會(huì)生成兩個(gè)實(shí)例變量,其名稱分別為 _firstName與_lastName。也可以在類的實(shí)現(xiàn)代碼里通過@synthesize語法來指定實(shí)例變量的名字:
1 2 3 4 | @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):
1)如果指定了成員變量的名稱,會(huì)生成一個(gè)指定的名稱的成員變量,
2)如果這個(gè)成員已經(jīng)存在了就不再生成了.
3)如果是 @synthesize foo; 還會(huì)生成一個(gè)名稱為foo的成員變量,也就是說:如果沒有指定成員變量的名稱會(huì)自動(dòng)生成一個(gè)屬性同名的成員變量。
4)如果是 @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 中定義的所有屬性
重載的屬性
當(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)橛辛薬utosynthesis(自動(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í)例變量的名字:
1 2 3 4 | @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)景:
結(jié)果編譯器報(bào)錯(cuò):

當(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)。例如:
1 | Person * motherInlaw = [[aPerson spouse] mother]; |
如果spouse對(duì)象為nil,那么發(fā)送給nil的消息mother也將返回nil。
1)如果方法返回值為指針類型,其指針大小為小于或者等于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。
3)如果方法的返回值不是上述提到的幾種情況,那么發(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的源代碼:
1 2 3 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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)用.如果我沒有記錯(cuò)的大概是這樣的:
1 | ((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo")); |
也就是說:
[obj foo];在objc動(dòng)態(tài)編譯時(shí),會(huì)被轉(zhuǎn)意為:objc_msgSend(obj, @selector(foo));。
18. 什么時(shí)候會(huì)報(bào)unrecognized selector的異常?
簡(jiǎn)單來說:當(dāng)該對(duì)象上某個(gè)方法,而該對(duì)象上沒有實(shí)現(xiàn)這個(gè)方法的時(shí)候, 可以通過“消息轉(zhuǎn)發(fā)”進(jìn)行解決。
簡(jiǎ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ù)并返回 YES,那運(yùn)行時(shí)系統(tǒng)就會(huì)重新啟動(dòng)一次消息發(fā)送的過程,如果 resolve 方法返回 NO ,運(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)。
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ì)象。
19. 一個(gè)objc對(duì)象如何進(jìn)行內(nèi)存布局?(考慮有父類的情況)
所有父類的成員變量和自己的成員變量都會(huì)存放在該對(duì)象所對(duì)應(yīng)的存儲(chǔ)空間中.
每一個(gè)對(duì)象內(nèi)部都有一個(gè)isa指針,指向他的類對(duì)象,類對(duì)象中存放著本對(duì)象的
1)對(duì)象方法列表(對(duì)象能夠接收的消息列表,保存在它所對(duì)應(yīng)的類對(duì)象中)
2)成員變量的列表
3)屬性列表
它內(nèi)部也有一個(gè)isa指針指向元對(duì)象(meta class),元對(duì)象內(nèi)部存放的是類方法列表,類對(duì)象內(nèi)部還有一個(gè)superclass的指針,指向他的父類對(duì)象。

1)根對(duì)象就是NSobject,它的superclass指針指向nil。
2)類對(duì)象既然稱為對(duì)象,那它也是一個(gè)實(shí)例。類對(duì)象中也有一個(gè)isa指針指向它的元類(meta class),即類對(duì)象是元類的實(shí)例。元類內(nèi)部存放的是類方法列表,根元類的isa指針指向自己,superclass指針指向NSObject類。
如圖:

20. 一個(gè)objc對(duì)象的isa的指針指向什么?有什么作用?
指向他的類對(duì)象,從而可以找到對(duì)象上的方法
21. 下面的代碼輸出什么?
1 2 3 4 5 6 7 8 9 10 11 | @implementation Son : Father- (id)init{ self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self;}@end |
答案:
都輸出 Son
1 2 | NSStringFromClass([self class]) = SonNSStringFromClass([super class]) = Son |
解惑:
(以下解惑部分摘自微博@Chun_iOS的博文刨根問底Objective-C Runtime(1)- Self & Super)
這個(gè)題目主要是考察關(guān)于objc中對(duì) self 和 super 的理解。
self 是類的隱藏參數(shù),指向當(dāng)前調(diào)用方法的這個(gè)類的實(shí)例。而 super 是一個(gè) Magic KeyWord, 它本質(zhì)是一個(gè)編譯器標(biāo)示符,和 self 是指向的同一個(gè)消息接受者。
上面的例子不管調(diào)用[self class]還是[super class],接受消息的對(duì)象都是當(dāng)前 Son *xxx 這個(gè)對(duì)象。而不同的是,super是告訴編譯器,調(diào)用 class 這個(gè)方法時(shí),要去父類的方法,而不是本類里的。
當(dāng)使用 self 調(diào)用方法時(shí),會(huì)從當(dāng)前類的方法列表中開始找,如果沒有,就從父類中再找;而當(dāng)使用 super 時(shí),則從父類的方法列表中開始找。然后調(diào)用父類的這個(gè)方法。
真的是這樣嗎?繼續(xù)看:
使用clang重寫命令:
1 | $ clang -rewrite-objc test.m |
發(fā)現(xiàn)上述代碼被轉(zhuǎn)化為:
1 2 | 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ù)定義:
1 | id objc_msgSend(id self, SEL op, ...) |
我們把 self 做為第一個(gè)參數(shù)傳遞進(jìn)去。
而在調(diào)用 [super class]時(shí),會(huì)轉(zhuǎn)化成 objc_msgSendSuper函數(shù)。看下函數(shù)定義:
1 | id objc_msgSendSuper(struct objc_super *super, SEL op, ...) |
第一個(gè)參數(shù)是 objc_super 這樣一個(gè)結(jié)構(gòu)體,其定義如下:
1 2 3 4 | 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):
1 2 3 | - (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。
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下還是ARC下均不需要
2011年版本的Apple API 官方文檔 - Associative References 一節(jié)中有一個(gè)MRC環(huán)境下的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | // 在MRC下,使用runtime Associate方法關(guān)聯(lián)的對(duì)象,不需要在主對(duì)象dealloc的時(shí)候釋放// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)// 摘自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 deallocatedNSString *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 the OBJC_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è)步驟:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // 對(duì)象的內(nèi)存銷毀時(shí)間表// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)// 根據(jù) WWDC 2011, Session 322 (36分22秒)中發(fā)布的內(nèi)存銷毀時(shí)間表 1. 調(diào)用 -release :引用計(jì)數(shù)變?yōu)榱?/code> * 對(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)用類方法(通過類名)
_objc_msgForward函數(shù)是做什么的,直接調(diào)用它將會(huì)發(fā)生什么?
_objc_msgForward是 IMP 類型,用于消息轉(zhuǎn)發(fā)的:當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒有實(shí)現(xiàn)的時(shí)候,_objc_msgForward會(huì)嘗試做消息轉(zhuǎn)發(fā)。
我們可以這樣創(chuàng)建一個(gè)_objc_msgForward對(duì)象:
IMP msgForwardIMP = _objc_msgForward;在上篇中的《objc中向一個(gè)對(duì)象發(fā)送消息[obj foo]和objc_msgSend()函數(shù)之間有什么關(guān)系?》曾提到objc_msgSend在“消息傳遞”中的作用。在“消息傳遞”過程中,objc_msgSend的動(dòng)作比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),如果沒找到,則向父類的 Class 查找。如果一直查找到根類仍舊沒有實(shí)現(xiàn),則用_objc_msgForward函數(shù)指針代替 IMP 。最后,執(zhí)行這個(gè) IMP 。
Objective-C運(yùn)行時(shí)是開源的,所以我們可以看到它的實(shí)現(xiàn)。打開 Apple Open Source 里Mac代碼里的obj包 下載一個(gè)最新版本,找到 objc-runtime-new.mm,進(jìn)入之后搜索_objc_msgForward。
里面有對(duì)_objc_msgForward的功能解釋:
/************************************************************************ lookUpImpOrForward.* The standard IMP lookup. * initialize==NO tries to avoid +initialize (but sometimes fails)* cache==NO skips optimistic unlocked lookup (but uses cache elsewhere)* Most callers should use initialize==YES and cache==YES.* inst is an instance of cls or a subclass thereof, or nil if none is known. * If cls is an un-initialized metaclass then a non-nil inst is faster.* May return _objc_msgForward_impcache. IMPs destined for external use * must be converted to _objc_msgForward or _objc_msgForward_stret.* If you don't want forwarding at all, use lookUpImpOrNil() instead.**********************************************************************/對(duì) objc-runtime-new.mm文件里與_objc_msgForward有關(guān)的三個(gè)函數(shù)使用偽代碼展示下:
// objc-runtime-new.mm 文件里與 _objc_msgForward 有關(guān)的三個(gè)函數(shù)使用偽代碼展示// Created by https://github.com/ChenYilong// Copyright (c) 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/). All rights reserved.// 同時(shí),這也是 obj_msgSend 的實(shí)現(xiàn)過程id objc_msgSend(id self, SEL op, ...) { if (!self) return nil; IMP imp = class_getMethodImplementation(self->isa, SEL op); imp(self, op, ...); //調(diào)用這個(gè)函數(shù),偽代碼...}//查找IMPIMP class_getMethodImplementation(Class cls, SEL sel) { if (!cls || !sel) return nil; IMP imp = lookUpImpOrNil(cls, sel); if (!imp) return _objc_msgForward; //_objc_msgForward 用于消息轉(zhuǎn)發(fā) return imp;}IMP lookUpImpOrNil(Class cls, SEL sel) { if (!cls->initialize()) { _class_initialize(cls); } Class curClass = cls; IMP imp = nil; do { //先查緩存,緩存沒有時(shí)重建,仍舊沒有則向父類查詢 if (!curClass) break; if (!curClass->cache) fill_cache(cls, curClass); imp = cache_getImp(curClass, sel); if (imp) break; } while (curClass = curClass->superclass); return imp;}雖然Apple沒有公開_objc_msgForward的實(shí)現(xiàn)源碼,但是我們還是能得出結(jié)論:
_objc_msgForward是一個(gè)函數(shù)指針(和 IMP 的類型一樣),是用于消息轉(zhuǎn)發(fā)的:當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒有實(shí)現(xiàn)的時(shí)候,_objc_msgForward會(huì)嘗試做消息轉(zhuǎn)發(fā)。在上篇中的《objc中向一個(gè)對(duì)象發(fā)送消息
[obj foo]和objc_msgSend()函數(shù)之間有什么關(guān)系?》曾提到objc_msgSend在“消息傳遞”中的作用。在“消息傳遞”過程中,objc_msgSend的動(dòng)作比較清晰:首先在 Class 中的緩存查找 IMP (沒緩存則初始化緩存),如果沒找到,則向父類的 Class 查找。如果一直查找到根類仍舊沒有實(shí)現(xiàn),則用_objc_msgForward函數(shù)指針代替 IMP 。最后,執(zhí)行這個(gè) IMP 。
為了展示消息轉(zhuǎn)發(fā)的具體動(dòng)作,這里嘗試向一個(gè)對(duì)象發(fā)送一條錯(cuò)誤的消息,并查看一下_objc_msgForward是如何進(jìn)行轉(zhuǎn)發(fā)的。
首先開啟調(diào)試模式、打印出所有運(yùn)行時(shí)發(fā)送的消息: 可以在代碼里執(zhí)行下面的方法:
(void)instrumentObjcMessageSends(YES);或者斷點(diǎn)暫停程序運(yùn)行,并在 gdb 中輸入下面的命令:
call (void)instrumentObjcMessageSends(YES)以第二種為例,操作如下所示:
之后,運(yùn)行時(shí)發(fā)送的所有消息都會(huì)打印到/tmp/msgSend-xxxx文件里了。
終端中輸入命令前往:
open /private/tmp可能看到有多條,找到最新生成的,雙擊打開
在模擬器上執(zhí)行執(zhí)行以下語句(這一套調(diào)試方案僅適用于模擬器,真機(jī)不可用,關(guān)于該調(diào)試方案的拓展鏈接: Can the messages sent to an object in Objective-C be monitored or printed out? ),向一個(gè)對(duì)象發(fā)送一條錯(cuò)誤的消息:
//// main.m// CYLObjcMsgForwardTest//// Created by http://weibo.com/luohanchenyilong/.// Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.//#import <UIKit/UIKit.h>#import "AppDelegate.h"#import "CYLTest.h"int main(int argc, char * argv[]) { @autoreleasepool { CYLTest *test = [[CYLTest alloc] init]; [test performSelector:(@selector(iOS程序犭袁))]; return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}你可以在/tmp/msgSend-xxxx(我這一次是/tmp/msgSend-9805)文件里,看到打印出來:
+ CYLTest NSObject initialize+ CYLTest NSObject alloc- CYLTest NSObject init- CYLTest NSObject performSelector:+ CYLTest NSObject resolveInstanceMethod:+ CYLTest NSObject resolveInstanceMethod:- CYLTest NSObject forwardingTargetForSelector:- CYLTest NSObject forwardingTargetForSelector:- CYLTest NSObject methodSignatureForSelector:- CYLTest NSObject methodSignatureForSelector:- CYLTest NSObject class- CYLTest NSObject doesNotRecognizeSelector:- CYLTest NSObject doesNotRecognizeSelector:- CYLTest NSObject class結(jié)合《NSObject官方文檔》,排除掉 NSObject 做的事,剩下的就是_objc_msgForward消息轉(zhuǎn)發(fā)做的幾件事:
調(diào)用resolveInstanceMethod:方法 (或 resolveClassMethod:)。允許用戶在此時(shí)為該 Class 動(dòng)態(tài)添加實(shí)現(xiàn)。如果有實(shí)現(xiàn)了,則調(diào)用并返回YES,那么重新開始objc_msgSend流程。這一次對(duì)象會(huì)響應(yīng)這個(gè)選擇器,一般是因?yàn)樗呀?jīng)調(diào)用過class_addMethod。如果仍沒實(shí)現(xiàn),繼續(xù)下面的動(dòng)作。
調(diào)用forwardingTargetForSelector:方法,嘗試找到一個(gè)能響應(yīng)該消息的對(duì)象。如果獲取到,則直接把消息轉(zhuǎn)發(fā)給它,返回非 nil 對(duì)象。否則返回 nil ,繼續(xù)下面的動(dòng)作。注意,這里不要返回 self ,否則會(huì)形成死循環(huán)。
調(diào)用methodSignatureForSelector:方法,嘗試獲得一個(gè)方法簽名。如果獲取不到,則直接調(diào)用doesNotRecognizeSelector拋出異常。如果能獲取,則返回非nil:創(chuàng)建一個(gè) NSlnvocation 并傳給forwardInvocation:。
調(diào)用forwardInvocation:方法,將第3步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這里面了,并返回非ni。
調(diào)用doesNotRecognizeSelector: ,默認(rèn)的實(shí)現(xiàn)是拋出異常。如果第3步?jīng)]能獲得一個(gè)方法簽名,執(zhí)行該步驟。
上面前4個(gè)方法均是模板方法,開發(fā)者可以override,由 runtime 來調(diào)用。最常見的實(shí)現(xiàn)消息轉(zhuǎn)發(fā):就是重寫方法3和4,吞掉一個(gè)消息或者代理給其他對(duì)象都是沒問題的
也就是說_objc_msgForward在進(jìn)行消息轉(zhuǎn)發(fā)的過程中會(huì)涉及以下這幾個(gè)方法:
resolveInstanceMethod:方法 (或 resolveClassMethod:)。
forwardingTargetForSelector:方法
methodSignatureForSelector:方法
forwardInvocation:方法
doesNotRecognizeSelector: 方法
為了能更清晰地理解這些方法的作用,git倉庫里也給出了一個(gè)Demo,名稱叫“ _objc_msgForward_demo ”,可運(yùn)行起來看看。
下面回答下第二個(gè)問題“直接_objc_msgForward調(diào)用它將會(huì)發(fā)生什么?”
直接調(diào)用_objc_msgForward是非常危險(xiǎn)的事,如果用不好會(huì)直接導(dǎo)致程序Crash,但是如果用得好,能做很多非常酷的事。
就好像跑酷,干得好,叫“耍酷”,干不好就叫“作死”。
正如前文所說:
_objc_msgForward是 IMP 類型,用于消息轉(zhuǎn)發(fā)的:當(dāng)向一個(gè)對(duì)象發(fā)送一條消息,但它并沒有實(shí)現(xiàn)的時(shí)候,_objc_msgForward會(huì)嘗試做消息轉(zhuǎn)發(fā)。
如何調(diào)用_objc_msgForward? _objc_msgForward隸屬 C 語言,有三個(gè)參數(shù) :
| -- | _objc_msgForward參數(shù) | 類型 |
|---|---|---|
| 1. | 所屬對(duì)象 | id類型 |
| 2. | 方法名 | SEL類型 |
| 3. | 可變參數(shù) | 可變參數(shù)類型 |
首先了解下如何調(diào)用 IMP 類型的方法,IMP類型是如下格式:
為了直觀,我們可以通過如下方式定義一個(gè) IMP類型 :
typedef void (*voidIMP)(id, SEL, ...)一旦調(diào)用_objc_msgForward,將跳過查找 IMP 的過程,直接觸發(fā)“消息轉(zhuǎn)發(fā)”,
如果調(diào)用了_objc_msgForward,即使這個(gè)對(duì)象確實(shí)已經(jīng)實(shí)現(xiàn)了這個(gè)方法,你也會(huì)告訴objc_msgSend:
“我沒有在這個(gè)對(duì)象里找到這個(gè)方法的實(shí)現(xiàn)”
想象下objc_msgSend會(huì)怎么做?通常情況下,下面這張圖就是你正常走objc_msgSend過程,和直接調(diào)用_objc_msgForward的前后差別:
有哪些場(chǎng)景需要直接調(diào)用_objc_msgForward?最常見的場(chǎng)景是:你想獲取某方法所對(duì)應(yīng)的NSInvocation對(duì)象。舉例說明:
jspatch (Github 鏈接)就是直接調(diào)用_objc_msgForward來實(shí)現(xiàn)其核心功能的:
JSPatch 以小巧的體積做到了讓JS調(diào)用/替換任意OC方法,讓iOS APP具備熱更新的能力。
作者的博文《JSPatch實(shí)現(xiàn)原理詳解》詳細(xì)記錄了實(shí)現(xiàn)原理,有興趣可以看下。
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 如何實(shí)現(xiàn) weak 屬性》有論述。(注:在上篇的《使用runtime Associate方法關(guān)聯(lián)的對(duì)象,需要在主對(duì)象dealloc的時(shí)候釋放么?》里給出的“對(duì)象的內(nèi)存銷毀時(shí)間表”也提到__weak引用的解除時(shí)間。)
我們可以設(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/ChenYilongid 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表中刪除。
解釋下:
因?yàn)榫幾g后的類已經(jīng)注冊(cè)在 runtime 中,類結(jié)構(gòu)體中的 objc_ivar_list 實(shí)例變量的鏈表 和 instance_size 實(shí)例變量的內(nèi)存大小已經(jīng)確定,同時(shí)runtime 會(huì)調(diào)用 class_setIvarLayout 或 class_setWeakIvarLayout 來處理 strong weak 引用。所以不能向存在的類中添加實(shí)例變量;
運(yùn)行時(shí)創(chuàng)建的類是可以添加實(shí)例變量,調(diào)用 class_addIvar 函數(shù)。但是得在調(diào)用 objc_allocateClassPair 之后,objc_registerClassPair 之前,原因同上。
總的說來,Run loop,正如其名,loop表示某種循環(huán),和run放在一起就表示一直在運(yùn)行著的循環(huán)。實(shí)際上,run loop和線程是緊密相連的,可以這樣說run loop是為了線程而生,沒有線程,它就沒有存在的必要。Run loops是線程的基礎(chǔ)架構(gòu)部分, Cocoa 和 CoreFundation 都提供了 run loop 對(duì)象方便配置和管理線程的 run loop (以下都以 Cocoa 為例)。每個(gè)線程,包括程序的主線程( main thread )都有與之相應(yīng)的 run loop 對(duì)象。
runloop 和線程的關(guān)系:
主線程的run loop默認(rèn)是啟動(dòng)的。
iOS的應(yīng)用程序里面,程序啟動(dòng)后會(huì)有一個(gè)如下的main()函數(shù)
int main(int argc, char * argv[]) {@autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));}}重點(diǎn)是UIApplicationMain()函數(shù),這個(gè)方法會(huì)為main thread設(shè)置一個(gè)NSRunLoop對(duì)象,這就解釋了:為什么我們的應(yīng)用可以在無人操作的時(shí)候休息,需要讓它干活的時(shí)候又能立馬響應(yīng)。
對(duì)其它線程來說,run loop默認(rèn)是沒有啟動(dòng)的,如果你需要更多的線程交互則可以手動(dòng)配置和啟動(dòng),如果線程只是去執(zhí)行一個(gè)長(zhǎng)時(shí)間的已確定的任務(wù)則不需要。
在任何一個(gè) Cocoa 程序的線程中,都可以通過以下代碼來獲取到當(dāng)前線程的 run loop 。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];參考鏈接:《Objective-C之run loop詳解》。
model 主要是用來指定事件在運(yùn)行循環(huán)中的優(yōu)先級(jí)的,分為:
蘋果公開提供的 Mode 有兩個(gè):
RunLoop只能運(yùn)行在一種mode下,如果要換mode,當(dāng)前的loop也需要停下重啟成新的。利用這個(gè)機(jī)制,ScrollView滾動(dòng)過程中 NSDefaultRunLoopMode(kCFRunLoopDefaultMode)的mode會(huì)切換到 UITrackingRunLoopMode來保證ScrollView的流暢滑動(dòng):只能在NSDefaultRunLoopMode模式下處理的事件會(huì) 影響scrllView的滑動(dòng)。
如果我們把一個(gè)NSTimer對(duì)象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主運(yùn)行循環(huán)中的時(shí)候, ScrollView滾動(dòng)過程中會(huì)因?yàn)閙ode的切換,而導(dǎo)致NSTimer將不再被調(diào)度。
同時(shí)因?yàn)閙ode還是可定制的,所以:
Timer計(jì)時(shí)會(huì)被scrollView的滑動(dòng)影響的問題可以通過將timer添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)來解決。代碼如下:
// // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)// https://github.com/ChenYilong//將timer添加到NSDefaultRunLoopMode中[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];//然后再添加到NSRunLoopCommonModes里NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTick:) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];一般來講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出,通常的代碼邏輯 是這樣的:
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit);}或使用偽代碼來展示下:
// // http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)// https://github.com/ChenYilongint main(int argc, char * argv[]) { //程序一直運(yùn)行狀態(tài) while (AppIsRunning) { //睡眠狀態(tài),等待喚醒事件 id whoWakesMe = SleepForWakingUp(); //得到喚醒事件 id event = GetEvent(whoWakesMe); //開始處理事件 HandleEvent(event); } return 0;}參考鏈接:
通過 retainCount 的機(jī)制來決定對(duì)象是否需要釋放。 每次 runloop 的時(shí)候,都會(huì)檢查對(duì)象的 retainCount,如果retainCount 為 0,說明該對(duì)象沒有地方需要繼續(xù)使用了,可以釋放掉了。
編譯時(shí)根據(jù)代碼上下文,插入 retain/release
ARC相對(duì)于MRC,不是在編譯時(shí)添加retain/release/autorelease這么簡(jiǎn)單。應(yīng)該是編譯期和運(yùn)行期兩部分共同幫助開發(fā)者管理內(nèi)存。
在編譯期,ARC用的是更底層的C接口實(shí)現(xiàn)的retain/release/autorelease,這樣做性能更好,也是為什么不能在ARC環(huán)境 下手動(dòng)retain/release/autorelease,同時(shí)對(duì)同一上下文的同一對(duì)象的成對(duì)retain/release操作進(jìn)行優(yōu)化(即忽略掉不 必要的操作);ARC也包含運(yùn)行期組件,這個(gè)地方做的優(yōu)化比較復(fù)雜,但也不能被忽略。【TODO:后續(xù)更新會(huì)詳細(xì)描述下】
分兩種情況:手動(dòng)干預(yù)釋放時(shí)機(jī)、系統(tǒng)自動(dòng)去釋放。
系統(tǒng)自動(dòng)去釋放--不手動(dòng)指定autoreleasepool
Autorelease對(duì)象出了作用域之后,會(huì)被添加到最近一次創(chuàng)建的自動(dòng)釋放池中,并會(huì)在當(dāng)前的 runloop 迭代結(jié)束時(shí)釋放。
釋放的時(shí)機(jī)總結(jié)起來,可以用下圖來表示:
下面對(duì)這張圖進(jìn)行詳細(xì)的解釋:
從程序啟動(dòng)到加載完成是一個(gè)完整的運(yùn)行循環(huán),然后會(huì)停下來,等待用戶交互,用戶的每一次交互都會(huì)啟動(dòng)一次運(yùn)行循環(huán),來處理用戶所有的點(diǎn)擊事件、觸摸事件。
我們都是知道: 所有 autorelease 的對(duì)象,在出了作用域之后,會(huì)被自動(dòng)添加到最近創(chuàng)建的自動(dòng)釋放池中。
但是如果每次都放進(jìn)應(yīng)用程序的 main.m 中的 autoreleasepool 中,遲早有被撐滿的一刻。這個(gè)過程中必定有一個(gè)釋放的動(dòng)作。何時(shí)?
在一次完整的運(yùn)行循環(huán)結(jié)束之前,會(huì)被銷毀。
那什么時(shí)間會(huì)創(chuàng)建自動(dòng)釋放池?運(yùn)行循環(huán)檢測(cè)到事件并啟動(dòng)后,就會(huì)創(chuàng)建自動(dòng)釋放池。
子線程的 runloop 默認(rèn)是不工作,無法主動(dòng)創(chuàng)建,必須手動(dòng)創(chuàng)建。
自定義的 NSOperation 和 NSThread 需要手動(dòng)創(chuàng)建自動(dòng)釋放池。比如: 自定義的 NSOperation 類中的 main 方法里就必須添加自動(dòng)釋放池。否則出了作用域后,自動(dòng)釋放對(duì)象會(huì)因?yàn)闆]有自動(dòng)釋放池去處理它,而造成內(nèi)存泄露。
但對(duì)于 blockOperation 和 invocationOperation 這種默認(rèn)的Operation ,系統(tǒng)已經(jīng)幫我們封裝好了,不需要手動(dòng)創(chuàng)建自動(dòng)釋放池。
@autoreleasepool 當(dāng)自動(dòng)釋放池被銷毀或者耗盡時(shí),會(huì)向自動(dòng)釋放池中的所有對(duì)象發(fā)送 release 消息,釋放自動(dòng)釋放池中的所有對(duì)象。
如果在一個(gè)vc的viewDidLoad中創(chuàng)建一個(gè) Autorelease對(duì)象,那么該對(duì)象會(huì)在 viewDidAppear 方法執(zhí)行前就被銷毀了。
參考鏈接:《黑幕背后的Autorelease》
訪問了野指針,比如對(duì)一個(gè)已經(jīng)釋放的對(duì)象執(zhí)行了release、訪問已經(jīng)釋放對(duì)象的成員變量或者發(fā)消息。 死循環(huán)
autoreleasepool 以一個(gè)隊(duì)列數(shù)組的形式實(shí)現(xiàn),主要通過下列三個(gè)函數(shù)完成.
objc_autoreleasepoolPushobjc_autoreleasepoolPopobjc_autorelease看函數(shù)名就可以知道,對(duì) autorelease 分別執(zhí)行 push,和 pop 操作。銷毀對(duì)象時(shí)執(zhí)行release操作。
舉例說明:我們都知道用類方法創(chuàng)建的對(duì)象都是 Autorelease 的,那么一旦 Person 出了作用域,當(dāng)在 Person 的 dealloc 方法中打上斷點(diǎn),我們就可以看到這樣的調(diào)用堆棧信息:
一個(gè)對(duì)象中強(qiáng)引用了block,在block中又使用了該對(duì)象,就會(huì)發(fā)射循環(huán)引用。 解決方法是將該對(duì)象使用__weak或者_(dá)_block修飾符修飾之后再在block中使用。
默認(rèn)情況下,在block中訪問的外部變量是復(fù)制過去的,即:寫操作不對(duì)原變量生效。但是你可以加上__block來讓其寫操作生效,示例代碼如下:
__block int a = 0;void (^foo)(void) = ^{ a = 1; }f00(); //這里,a的值被修改為1參考鏈接:微博@唐巧_boy的著作《iOS開發(fā)進(jìn)階》中的第11.2.3章節(jié)
系統(tǒng)的某些block api中,UIView的block版本寫動(dòng)畫時(shí)不需要考慮,但也有一些api 需要考慮:
所謂“引用循環(huán)”是指雙向的強(qiáng)引用,所以那些“單向的強(qiáng)引用”(block 強(qiáng)引用 self )沒有問題,比如這些:
[UIView animateWithDuration:duration animations:^{ [self.superview layoutIfNeeded]; }]; [[NSOperationQueue mainQueue] addOperationWithBlock:^{ self.someProperty = xyz; }]; [[NSNotificationCenter defaultCenter] addObserverForName:@"someNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * notification) { self.someProperty = xyz; }]; 這些情況不需要考慮“引用循環(huán)”。
但如果你使用一些參數(shù)中可能含有 ivar 的系統(tǒng) api ,如 GCD 、NSNotificationCenter就要小心一點(diǎn):比如GCD 內(nèi)部如果引用了 self,而且 GCD 的其他參數(shù)是 ivar,則要考慮到循環(huán)引用:
__weak __typeof__(self) weakSelf = self;dispatch_group_async(_operationsGroup, _operationsQueue, ^{__typeof__(self) strongSelf = weakSelf;[strongSelf doSomething];[strongSelf doSomethingElse];} );類似的:
__weak __typeof__(self) weakSelf = self; _observer = [[NSNotificationCenter defaultCenter] addObserverForName:@"testKey" object:nil queue:nil usingBlock:^(NSNotification *note) { __typeof__(self) strongSelf = weakSelf; [strongSelf dismissModalViewControllerAnimated:YES]; }];self --> _observer --> block --> self 顯然這也是一個(gè)循環(huán)引用。
dispatch_queue_t)分哪兩種類型?使用Dispatch Group追加block到Global Group Queue,這些block如果全部執(zhí)行完畢,就會(huì)執(zhí)行Main Dispatch Queue中的結(jié)束處理的block。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);dispatch_group_t group = dispatch_group_create();dispatch_group_async(group, queue, ^{ /*加載圖片1 */ });dispatch_group_async(group, queue, ^{ /*加載圖片2 */ });dispatch_group_async(group, queue, ^{ /*加載圖片3 */ }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 合并圖片});dispatch_barrier_async的作用是什么?在并行隊(duì)列中,為了保持某些任務(wù)的順序,需要等待一些任務(wù)完成后才能繼續(xù)進(jìn)行,使用 barrier 來等待之前任務(wù)完成,避免數(shù)據(jù)競(jìng)爭(zhēng)等問題。 dispatch_barrier_async 函數(shù)會(huì)等待追加到Concurrent Dispatch Queue并行隊(duì)列中的操作全部執(zhí)行完之后,然后再執(zhí)行 dispatch_barrier_async 函數(shù)追加的處理,等 dispatch_barrier_async 追加的處理執(zhí)行結(jié)束之后,Concurrent Dispatch Queue才恢復(fù)之前的動(dòng)作繼續(xù)執(zhí)行。
打個(gè)比方:比如你們公司周末跟團(tuán)旅游,高速休息站上,司機(jī)說:大家都去上廁所,速戰(zhàn)速?zèng)Q,上完廁所就上高速。超大的公共廁所,大家同時(shí)去,程序猿很快就結(jié)束了,但程序媛就可能會(huì)慢一些,即使你第一個(gè)回來,司機(jī)也不會(huì)出發(fā),司機(jī)要等待所有人都回來后,才能出發(fā)。 dispatch_barrier_async 函數(shù)追加的內(nèi)容就如同 “上完廁所就上高速”這個(gè)動(dòng)作。
(注意:使用 dispatch_barrier_async ,該函數(shù)只能搭配自定義并行隊(duì)列 dispatch_queue_t 使用。不能使用: dispatch_get_global_queue ,否則 dispatch_barrier_async 的作用會(huì)和 dispatch_async 的作用一模一樣。 )
dispatch_get_current_queue?dispatch_get_current_queue容易造成死鎖
- (void)viewDidLoad{ [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); }); NSLog(@"3");}只輸出:1 。發(fā)生主線程鎖死。
// 添加鍵值觀察/*1 觀察者,負(fù)責(zé)處理監(jiān)聽事件的對(duì)象2 觀察的屬性3 觀察的選項(xiàng)4 上下文*/[self.person addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:@"Person Name"];observer中需要實(shí)現(xiàn)一下方法:
// 所有的 kvo 監(jiān)聽到事件,都會(huì)調(diào)用此方法/* 1. 觀察的屬性 2. 觀察的對(duì)象 3. change 屬性變化字典(新/舊) 4. 上下文,與監(jiān)聽的時(shí)候傳遞的一致 */- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;所謂的“手動(dòng)觸發(fā)”是區(qū)別于“自動(dòng)觸發(fā)”:
自動(dòng)觸發(fā)是指類似這種場(chǎng)景:在注冊(cè) KVO 之前設(shè)置一個(gè)初始值,注冊(cè)之后,設(shè)置一個(gè)不一樣的值,就可以觸發(fā)了。
想知道如何手動(dòng)觸發(fā),必須知道自動(dòng)觸發(fā) KVO 的原理:
鍵值觀察通知依賴于 NSObject 的兩個(gè)方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一個(gè)被觀察屬性發(fā)生改變之前, willChangeValueForKey: 一定會(huì)被調(diào)用,這就 會(huì)記錄舊的值。而當(dāng)改變發(fā)生后, didChangeValueForKey: 會(huì)被調(diào)用,繼而 observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用。如果可以手動(dòng)實(shí)現(xiàn)這些調(diào)用,就可以實(shí)現(xiàn)“手動(dòng)觸發(fā)”了。
那么“手動(dòng)觸發(fā)”的使用場(chǎng)景是什么?一般我們只在希望能控制“回調(diào)的調(diào)用時(shí)機(jī)”時(shí)才會(huì)這么做。
具體做法如下:
如果這個(gè) value 是 表示時(shí)間的 self.now ,那么代碼如下:最后兩行代碼缺一不可。
// .m文件// Created by https://github.com/ChenYilong// 微博@iOS程序犭袁(http://weibo.com/luohanchenyilong/).// 手動(dòng)觸發(fā) value 的KVO,最后兩行代碼缺一不可。//@property (nonatomic, strong) NSDate *now;- (void)viewDidLoad{ [super viewDidLoad]; [self willChangeValueForKey:@"now"]; // “手動(dòng)觸發(fā)self.now的KVO”,必寫。 [self didChangeValueForKey:@"now"]; // “手動(dòng)觸發(fā)self.now的KVO”,必寫。}但是平時(shí)我們一般不會(huì)這么干,我們都是等系統(tǒng)去“自動(dòng)觸發(fā)”。“自動(dòng)觸發(fā)”的實(shí)現(xiàn)原理:
比如調(diào)用
setNow:時(shí),系統(tǒng)還會(huì)以某種方式在中間插入wilChangeValueForKey:、didChangeValueForKey:和observeValueForKeyPath:ofObject:change:context:的調(diào)用。
大家可能以為這是因?yàn)?setNow: 是合成方法,有時(shí)候我們也能看到人們這么寫代碼:
- (void)setNow:(NSDate *)aDate { [self willChangeValueForKey:@"now"]; // 沒有必要 _now = aDate; [self didChangeValueForKey:@"now"];// 沒有必要}這是完全沒有必要的代碼,不要這么做,這樣的話,KVO代碼會(huì)被調(diào)用兩次。KVO在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey: ,之后總是調(diào)用 didChangeValueForkey: 。怎么做到的呢?答案是通過 isa 混寫(isa-swizzling)。下文《apple用什么方式實(shí)現(xiàn)對(duì)一個(gè)對(duì)象的KVO?》會(huì)有詳述。
參考鏈接: Manual Change Notification---Apple 官方文檔
NSString *_foo ,調(diào)用setValue:forKey:時(shí),可以以foo還是 _foo 作為key?都可以。
KVO支持實(shí)例變量
請(qǐng)參考:《如何自己動(dòng)手實(shí)現(xiàn) KVO》
Apple 的文檔對(duì) KVO 實(shí)現(xiàn)的描述:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
從Apple 的文檔可以看出:Apple 并不希望過多暴露 KVO 的實(shí)現(xiàn)細(xì)節(jié)。不過,要是借助 runtime 提供的方法去深入挖掘,所有被掩蓋的細(xì)節(jié)都會(huì)原形畢露:
當(dāng)你觀察一個(gè)對(duì)象時(shí),一個(gè)新的類會(huì)被動(dòng)態(tài)創(chuàng)建。這個(gè)類繼承自該對(duì)象的原本的類,并重寫了被觀察屬性的 setter 方法。重寫的 setter 方法會(huì)負(fù)責(zé)在調(diào)用原 setter 方法之前和之后,通知所有觀察對(duì)象:值的更改。最后通過
isa 混寫(isa-swizzling)把這個(gè)對(duì)象的 isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個(gè)對(duì)象的類是什么 ) 指向這個(gè)新創(chuàng)建的子類,對(duì)象就神奇的變成了新創(chuàng)建的子類的實(shí)例。我畫了一張示意圖,如下所示:
KVO 確實(shí)有點(diǎn)黑魔法:
Apple 使用了
isa 混寫(isa-swizzling)來實(shí)現(xiàn) KVO 。
下面做下詳細(xì)解釋:
鍵值觀察通知依賴于 NSObject 的兩個(gè)方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一個(gè)被觀察屬性發(fā)生改變之前, willChangeValueForKey: 一定會(huì)被調(diào)用,這就會(huì)記錄舊的值。而當(dāng)改變發(fā)生后, didChangeValueForKey: 會(huì)被調(diào)用,繼而 observeValueForKey:ofObject:change:context: 也會(huì)被調(diào)用。可以手動(dòng)實(shí)現(xiàn)這些調(diào)用,但很少有人這么做。一般我們只在希望能控制回調(diào)的調(diào)用時(shí)機(jī)時(shí)才會(huì)這么做。大部分情況下,改變通知會(huì)自動(dòng)調(diào)用。
比如調(diào)用 setNow: 時(shí),系統(tǒng)還會(huì)以某種方式在中間插入 wilChangeValueForKey: 、 didChangeValueForKey: 和 observeValueForKeyPath:ofObject:change:context: 的調(diào)用。大家可能以為這是因?yàn)?setNow: 是合成方法,有時(shí)候我們也能看到人們這么寫代碼:
- (void)setNow:(NSDate *)aDate { [self willChangeValueForKey:@"now"]; // 沒有必要 _now = aDate; [self didChangeValueForKey:@"now"];// 沒有必要}這是完全沒有必要的代碼,不要這么做,這樣的話,KVO代碼會(huì)被調(diào)用兩次。KVO在調(diào)用存取方法之前總是調(diào)用 willChangeValueForKey: ,之后總是調(diào)用 didChangeValueForkey: 。怎么做到的呢?答案是通過 isa 混寫(isa-swizzling)。第一次對(duì)一個(gè)對(duì)象調(diào)用 addObserver:forKeyPath:options:context: 時(shí),框架會(huì)創(chuàng)建這個(gè)類的新的 KVO 子類,并將被觀察對(duì)象轉(zhuǎn)換為新子類的對(duì)象。在這個(gè) KVO 特殊子類中, Cocoa 創(chuàng)建觀察屬性的 setter ,大致工作原理如下:
- (void)setNow:(NSDate *)aDate { [self willChangeValueForKey:@"now"]; [super setValue:aDate forKey:@"now"]; [self didChangeValueForKey:@"now"];}這種繼承和方法注入是在運(yùn)行時(shí)而不是編譯時(shí)實(shí)現(xiàn)的。這就是正確命名如此重要的原因。只有在使用KVC命名約定時(shí),KVO才能做到這一點(diǎn)。
KVO 在實(shí)現(xiàn)中通過 isa 混寫(isa-swizzling) 把這個(gè)對(duì)象的 isa 指針 ( isa 指針告訴 Runtime 系統(tǒng)這個(gè)對(duì)象的類是什么 ) 指向這個(gè)新創(chuàng)建的子類,對(duì)象就神奇的變成了新創(chuàng)建的子類的實(shí)例。這在Apple 的文檔可以得到印證:
Automatic key-value observing is implemented using a technique called isa-swizzling... When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class ...
然而 KVO 在實(shí)現(xiàn)中使用了 isa 混寫( isa-swizzling) ,這個(gè)的確不是很容易發(fā)現(xiàn):Apple 還重寫、覆蓋了 -class 方法并返回原來的類。 企圖欺騙我們:這個(gè)類沒有變,就是原本那個(gè)類。。。
但是,假設(shè)“被監(jiān)聽的對(duì)象”的類對(duì)象是 MYClass ,有時(shí)候我們能看到對(duì) NSKVONotifying_MYClass 的引用而不是對(duì) MYClass 的引用。借此我們得以知道 Apple 使用了 isa 混寫(isa-swizzling)。具體探究過程可參考 這篇博文 。
參考鏈接: Should IBOutlets be strong or weak under ARC?
文章告訴我們:
因?yàn)榧热挥型怄溎敲匆晥D在xib或者storyboard中肯定存在,視圖已經(jīng)對(duì)它有一個(gè)強(qiáng)引用了。
不過這個(gè)回答漏了個(gè)重要知識(shí),使用storyboard(xib不行)創(chuàng)建的vc,會(huì)有一個(gè)叫 _topLevelObjectsToKeepAliveFromStoryboard的私有數(shù)組強(qiáng)引用所有top level的對(duì)象,所以這時(shí)即便outlet聲明成weak也沒關(guān)系
它能夠通過KVC的方式配置一些你在interface builder 中不能配置的屬性。當(dāng)你希望在IB中作盡可能多得事情,這個(gè)特性能夠幫助你編寫更加輕量級(jí)的viewcontroller
設(shè)置全局?jǐn)帱c(diǎn)快速定位問題代碼所在行

更多 lldb(gdb) 調(diào)試命令可查看
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注