我們都知道OC是一門動(dòng)態(tài)語言,那么什么是動(dòng)態(tài)語言呢?動(dòng)態(tài)語言,是指程序在運(yùn)行時(shí)可以改變其結(jié)構(gòu):新的函數(shù)可以被引進(jìn),已有的函數(shù)可以被刪除,類型的檢查在運(yùn)行時(shí)進(jìn)行。學(xué)習(xí)了OC的動(dòng)態(tài)特性以后,我們就會(huì)知道,為什么會(huì)說OC是一門動(dòng)態(tài)語言。
OC具有相當(dāng)多的動(dòng)態(tài)特性,經(jīng)常被提到的有:動(dòng)態(tài)類型,動(dòng)態(tài)識(shí)別,動(dòng)態(tài)綁定,動(dòng)態(tài)加載。
動(dòng)態(tài)類型
動(dòng)態(tài)類型特性能使程序直到執(zhí)行時(shí)才確定對象所屬類型。比如說id類型,id類型即通用的對象類型,可以指向任何對象。結(jié)合isKindOfClass方法,在確定對象為某個(gè)類的成員后,再安全的進(jìn)行強(qiáng)制轉(zhuǎn)換,執(zhí)行對應(yīng)的代碼。
動(dòng)態(tài)綁定
基于動(dòng)態(tài)類型,在某個(gè)實(shí)例對象的類型被確定后,該對象對應(yīng)的屬性和響應(yīng)的消息也被完全確定。這樣就使得程序直到執(zhí)行時(shí)才確定對象要調(diào)用的實(shí)際方法。在繼續(xù)之前,需要明確OC中消息的概念。由于OC的動(dòng)態(tài)特性,在OC中其實(shí)很少提及“函數(shù)”概念,傳統(tǒng)的函數(shù)一般在編譯時(shí)就已經(jīng)把參數(shù)信息和函數(shù)實(shí)現(xiàn)打包到編譯后的源碼中了,而在OC中最常用的是消息機(jī)制。
OC中的消息發(fā)送機(jī)制(補(bǔ)充)
在C語言中,我們調(diào)用函數(shù)時(shí),必須先聲明函數(shù)(或者自上而下),而實(shí)際上,聲明函數(shù)的過程就是獲取函數(shù)地址,調(diào)用函數(shù)的過程就是直接跳到地址執(zhí)行,代碼在被編譯器解析、優(yōu)化后,就成為了一堆匯編代碼,然后連接各種庫,完了生成可執(zhí)行的代碼(即是靜態(tài)的)。
在OC中,消息發(fā)送機(jī)制則是Runtime通過selector快速查找IMP的過程,有了函數(shù)指針就可以執(zhí)行對應(yīng)的方法實(shí)現(xiàn)。所有的[object message]都會(huì)轉(zhuǎn)換為objc_msgSend(object, @selector(message))(如果是有參數(shù)的方法,可以在后面用“,”分隔對參數(shù)進(jìn)行拼接),objc_msgSend方法又會(huì)調(diào)用class_getMethodImplementation獲取IMP。
這里需要普及一下什么是selector和IMP。
selector:OC在編譯時(shí),會(huì)根據(jù)方法的名字生成一個(gè)用來區(qū)分這個(gè)方法的唯一的一個(gè)ID,本質(zhì)上就是一個(gè)字符串。只要方法名稱相同,就算參數(shù)的類型不同,它們的ID也是相同的。
IMP:實(shí)際上就是一個(gè)函數(shù)指針,指向方法實(shí)現(xiàn)的首地址。
運(yùn)行時(shí)類中的方法可能會(huì)增加,需要先做讀操作加鎖,使得方法查找和緩存填充成為原子操作。添加category會(huì)刷新緩存,之后如果舊數(shù)據(jù)又被重新填充到緩存中,category添加操作就會(huì)被忽略掉。
獲取IMP的邏輯整理和流程:
- 檢查selector是否需要忽略,如果selector是需要被忽略的垃圾回收用到的方法,則將IMP結(jié)果設(shè)為_objc_ignored_method,這是個(gè)匯編程序入口,可以理解為一個(gè)標(biāo)記。對此種情況進(jìn)行緩存填充操作后,跳到第8步,否則執(zhí)行下一步。
- 檢查target是否為nil,如果是nil就直接cleanup,然后return。
- 查找當(dāng)前類中的緩存,如果命中緩存獲取到了IMP,則直接跳到第8步,否則執(zhí)行下一步。
- 在當(dāng)前類中的方法列表(method list)中進(jìn)行查找,也就是根據(jù)selector查找到Method后,獲取Method中的IMP,并填充到緩存中。查找過程比較復(fù)雜,會(huì)針對已經(jīng)排序的列表使用二分法查找,未排序的列表則是線性遍歷(順序查找)。如果成功查找到Method對象,則直接跳到第8步,否則執(zhí)行下一步。
- 在繼承層級(jí)中遞歸向父類中查找,情況跟上一步類似,也是先查找緩存,緩存中沒有就查找方法列表。查找成功,進(jìn)入第8步,查找失敗,進(jìn)入下一步。
- 進(jìn)入動(dòng)態(tài)方法解析,這是消息轉(zhuǎn)發(fā)前的最后一次機(jī)會(huì)。此時(shí)釋放讀入鎖,接著間接的發(fā)送+resolveInstanceMethod(如果查找的是實(shí)例方法,調(diào)用該方法)或+resolveClassMethod(如果查找的是類方法,調(diào)用該方法)消息。這相當(dāng)于告訴程序員,趕緊用runtime給類里這個(gè)selector弄個(gè)對應(yīng)的IMP吧,因?yàn)榇藭r(shí)鎖已經(jīng)unlock了所以不會(huì)緩存結(jié)果。這些工作都是在非線程安全下進(jìn)行的,完成后需要回到第1步再次查找IMP。
- 此時(shí)不僅沒查到IMP,動(dòng)態(tài)方法解析也不奏效,即將進(jìn)入消息轉(zhuǎn)發(fā)。
- 讀操作解鎖,并將之前找到的IMP返回。
查找類方法的IMP的流程和查找實(shí)例方法的一樣,不過實(shí)例方法的IMP是在類的緩存和方法列表里面查找,類方法的IMP是在元類的緩存和方法列表里面查找。關(guān)于元類的定義,建議大家參考這篇文章,寫的很詳細(xì)。
OC中的消息轉(zhuǎn)發(fā)機(jī)制(補(bǔ)充)
當(dāng)發(fā)送的消息在類的緩存和方法列表里面查找不到時(shí),為了防止crash,還有3種補(bǔ)救方法:
-
動(dòng)態(tài)方法解析,就是上面的第6步操作。對應(yīng)的具體方法是+(BOOL)resolveInstanceMethod:(SEL)sel和+(BOOL)resolveClassMethod:(SEL)sel,當(dāng)查找的方法是實(shí)例方法時(shí)調(diào)用前者,當(dāng)查找的方法是類方法時(shí),調(diào)用后者。
、、、 void eat(id target, SEL sel, NSString *name) { } + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(name)) { class_addMethod(self, sel, (IMP)eat, "v@:@"); return YES; } return [super resolveInstanceMethod:sel]; } 、、、
這里需要說明一下class_addMethod的第四個(gè)參數(shù),它是一個(gè)const char * _Nullable types類型,Nullable代表這個(gè)參數(shù)可以為空,char *表示這是一個(gè)char型指針,所以不要試圖用@“”。看我們傳進(jìn)去的參數(shù)v@:@,有人可能有點(diǎn)迷糊,但其實(shí)很好理解:v代表這是一個(gè)返回值為void的函數(shù),@代表這是一個(gè)參數(shù),:代表這是一個(gè)SEL類型,所以跟我們的eat方法對應(yīng)起來就是v@:@。關(guān)于這些類型的詳細(xì)對應(yīng)關(guān)系,大家可以在官方文檔中查找。
-
轉(zhuǎn)移消息的接受者。通過方法-(id)forwardingTargetForSelector:(SEL)aSelector可以轉(zhuǎn)移消息的接收者,當(dāng)返回非self/非nil時(shí),消息被轉(zhuǎn)給新對象執(zhí)行。如果該對象實(shí)現(xiàn)了該方法(不管該方法是私有還是公有,其實(shí)OC是沒有嚴(yán)格意義上的私有方法的,因?yàn)橛衦untime的存在,你想要調(diào)用總可以辦到的),就直接讓那個(gè)對象來處理消息。這種方式只是轉(zhuǎn)移消息的接收者,不能對傳遞的消息做修改。
、、、 - (id)forwardingTargetForSelector:(SEL)aSelector{ if(selector == @selector(name)){ Student *s = [[Student alloc] init]; if([s respondsToSelector:@selector(name)]){ return s; } } return[super forwardingTargetForSelector:aSelector]; } 、、、
-
完整消息轉(zhuǎn)發(fā)。如果上述方式?jīng)]有對轉(zhuǎn)發(fā)的消息做處理,那么系統(tǒng)就會(huì)走完整的轉(zhuǎn)發(fā)流程。系統(tǒng)會(huì)先通過-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法,找到轉(zhuǎn)發(fā)的消息的方法簽名,然后再通過-(void)forwardInvocation:(NSInvovation *)anInvocation來轉(zhuǎn)發(fā)消息。注意,前面一個(gè)方法不能返回nil,如果返回nil,第二個(gè)方法是不會(huì)執(zhí)行的。通過這種方式轉(zhuǎn)發(fā)的消息,我們不僅可以轉(zhuǎn)換消息的接收者,轉(zhuǎn)發(fā)給多個(gè)對象,還可以對轉(zhuǎn)發(fā)的消息做相應(yīng)的修改。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{ if (aSelector == @selector(eatChicken:)) { // 這個(gè)方法的參數(shù)對應(yīng)的type和上面提到的TypeEncoding的對應(yīng)關(guān)系是一樣的。signatureWithObjCTypes的參數(shù)應(yīng)該與aSelector的返回值和參數(shù)的類型和個(gè)數(shù)相對應(yīng),比如eatNothing是一個(gè)無返回值無參數(shù)的方法,所以type就對應(yīng)為v@:(可能有的童鞋把type寫成v@:@也能成功運(yùn)行,但是成功運(yùn)行不代表就正確。如果你在forwardInvocation中對NSInvocation的returnvalue和argument進(jìn)行操作的話,就會(huì)報(bào)錯(cuò)。有可能會(huì)報(bào)參數(shù)越界,也有可能會(huì)什么錯(cuò)誤信息都沒有。如果出現(xiàn)這種情況的童鞋不妨檢查一下自己的type和selector是否一一對應(yīng))。 NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 為了防止上面的錯(cuò)誤出現(xiàn),我們也可以采取這種寫法,EatFood是聲明了aSelector方法的類。 // sign = [[EatFood alloc] init] methodSignatureForSelector:aSelector]; return sign; } NSMethodSignature *sign = [super methodSignatureForSelector:aSelector]; return sign; } - (void)forwardInvocation:(NSInvocation *)anInvocation{ NSNumber *number = @3; // 修改轉(zhuǎn)發(fā)消息的參數(shù),注意因?yàn)橛袃蓚€(gè)隱藏參數(shù)的存在,所以index應(yīng)該從2開始算起 [anInvocation setArgument:&number atIndex:2]; // 修改轉(zhuǎn)發(fā)的消息 anInvocation.selector = @selector(eatNothing); // 把消息轉(zhuǎn)發(fā)給多個(gè)對象 if ([EatChicken instancesRespondToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:[[EatChicken alloc] init]]; } if ([EatFood instancesRespondToSelector:anInvocation.selector]) { [anInvocation invokeWithTarget:[[EatFood alloc] init]]; } } 、、、
如果上述3個(gè)方法都沒有來處理這個(gè)消息,就會(huì)進(jìn)入NSObject的-(void)doesNotRecognizeSelector:(SEL)aSelector方法中,拋出異常。
如果想要通過運(yùn)行時(shí)為類添加方法,使用第一種方案;如果想要把消息轉(zhuǎn)發(fā)給另外另外一個(gè)類的對象時(shí),使用第二種方案;如果想要把消息轉(zhuǎn)發(fā)給多個(gè)類的對象(OC的多繼承也可以通過該方法實(shí)現(xiàn))時(shí),使用第三種方案,該方案還可以對消息的selector和參數(shù)進(jìn)行修改。步驟越往后,處理消息的代價(jià)就越大。
那么說了那么多,消息轉(zhuǎn)發(fā)在實(shí)際開發(fā)中究竟有什么應(yīng)用呢?相信大家在調(diào)試接口的時(shí)候都遇到過這種問題,后臺(tái)開發(fā)接口的時(shí)候,大家說好的沒有數(shù)據(jù)返回空數(shù)組,空字典或者空字符串,但是他啪給你返回來一個(gè)NSNull,然后在你開開心心去用dic[@"happy"]去取值的時(shí)候給你來一個(gè)大大的崩潰,很心酸有沒有。這時(shí)候我們可以怎么做呢,給NSNull添加一個(gè)category,然后重寫methodSignatureForSelector和forwardInvocation來處理異常情況。 (PS:實(shí)現(xiàn)這兩個(gè)方法的時(shí)候會(huì)報(bào)一個(gè)警告:Category is implementing a method which will also be implemented by its primary class。意思是類目里面重寫了基類里面的方法,蘋果是不建議在類目里面重寫類的方法的,因?yàn)轭惸坷镏貙懙姆椒ㄗ饔糜蚴侨值模锌赡軙?huì)導(dǎo)致一些未知的錯(cuò)誤,坑隊(duì)友啊。而且有一些框架里面也有可能會(huì)重寫這個(gè)方法,這樣哪個(gè)類目里的方法會(huì)被執(zhí)行就是未知的。在類目里面聲明一些方法的時(shí)候,盡量帶上前綴,以防止跟系統(tǒng)或者三方框架里面的方法重名。另外,重寫方法的話盡量使用繼承的方式,繼承的作用域只在于子類,不會(huì)對父類產(chǎn)生影響。)
、、、
@implementation NSNull (Forward)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *sign = [super methodSignatureForSelector:aSelector];
if (sign == nil) {
sign = [NSMethodSignature signatureWithObjCTypes:"@^v^c"];
}
return sign;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([self respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self];
}
}
@end
、、、
通過運(yùn)行時(shí)避免動(dòng)態(tài)綁定來優(yōu)化方法調(diào)用的一個(gè)小tips
如果需要頻繁對一個(gè)消息進(jìn)行多次調(diào)用,而且我們希望節(jié)省每次調(diào)用方法都要發(fā)送消息的開銷時(shí),我們可以通過取得方法的地址,并且直接像調(diào)用函數(shù)一樣調(diào)用該方法來達(dá)到目的。
利用NSObject類中的methodForSelector:方法,我們可以獲得一個(gè)指向方法實(shí)現(xiàn)的指針,并且可以使用該指針直接調(diào)用方法實(shí)現(xiàn)。methodForSelector:返回的指針和賦值的變量類型必須完全一致,包括方法的參數(shù)類型和返回值類型都在類型識(shí)別的考慮范圍中。
下面的例子展示了怎么使用指針來調(diào)用setFilled:方法的實(shí)現(xiàn):
void (*setter) (id, SEL, BOOL);
int i;
setter = (void (*) (id, SEL, BOOL))[target methodForSelector:@selector(setFilled)];
for (int i = 0; i < 1000, i++){
setter(targetList[i], @selector(setFilled:), YES);
}
、、、
方法指針的第一個(gè)參數(shù)是接收消息的對象,第二個(gè)參數(shù)是方法的SEL,這兩個(gè)參數(shù)在方法中是隱藏參數(shù),但使用函數(shù)的形式來調(diào)用方法時(shí)必須顯式的給出。使用methodForSelector:來避免動(dòng)態(tài)綁定將減少大部分消息的開銷,但是這只有在指定的消息被重復(fù)發(fā)送給多個(gè)不同類的對象(如果是多次發(fā)送給同一個(gè)對象,效果不是很明顯,因?yàn)橥ㄟ^消息的發(fā)送機(jī)制我們知道,調(diào)用過的method會(huì)被添加到類的方法的cache列表里,從cache列表查詢和直接通過指針調(diào)用區(qū)別并不是很大,不過開銷肯定是減少了的)很多次時(shí)才有意義,例如上面的for循環(huán)。
動(dòng)態(tài)識(shí)別
動(dòng)態(tài)識(shí)別常用的幾個(gè)方法:
- (BOOL)isKindOfClass:(__unsafe_unretained Class)是否是某個(gè)類或者這個(gè)類的子類。
- (BOOL)isMemberOfClass::(__unsafe_unretained Class)是否是一個(gè)類的實(shí)例。
- (BOOL)respondsToSelector:selector類中是否有這個(gè)方法。
動(dòng)態(tài)加載
OC程序可以在運(yùn)行時(shí)鏈接和載入新的類和范疇類。新載入的類和在程序啟動(dòng)時(shí)載入的類并沒有區(qū)別。動(dòng)態(tài)加載可以用在很多地方。例如,系統(tǒng)配置中的模塊就是被動(dòng)態(tài)加載的。在Cocoa環(huán)境中,動(dòng)態(tài)加載一般被用來對應(yīng)用程序進(jìn)行定制。您的程序可以在運(yùn)行時(shí)加載其它程序員編寫的模塊--和Interface Build載入定制的調(diào)色板以及系統(tǒng)配置程序載入定制的模塊的類似。這些模塊通過您許可的方式擴(kuò)展了您的程序,而您無需自己來定義或?qū)崿F(xiàn)。您提供了框架,而其它的程序員提供了實(shí)現(xiàn)。
OC可以動(dòng)態(tài)的創(chuàng)建一個(gè)類嗎?
答案:可以。
- 使用objc_allocateClassPair函數(shù)為新的類分配存儲(chǔ)空間,參數(shù)分別是你要繼承的類,新的類的名稱和你要分配的內(nèi)存大小。
- 使用class_addMethod為類增加方法,使用class_addIvar為類增加實(shí)例變量。
- 用objc_registerClassPair函數(shù)注冊這個(gè)類,以便它能被別人使用。