這篇讀書筆記主要介紹了Objective-C底層的一些東西,比如Objective-C對象模型锅论、objc_msgSend消息發(fā)送原理循衰、方法混寫(Method Swizzling)和ISA混寫(ISA Swizzling)。
Objective-C對象模型
我們都知道Objective-C是一門動態(tài)性語言粟关,這種動態(tài)性的核心是objc提供的Objective-C運行時套菜,比如objc_msgSend就是一個核心函數(shù)亲善,每次使用[object message]語法都會調(diào)用它。我們先來了解下Objective-C對象模型逗柴。
Objective-C是一門面向?qū)ο蟮木幊陶Z言蛹头,每一個對象都是一個類的實例,在Objective-C中,每一個對象都有一個名為isa的指針掘而,指向該對象的類。每一個類描述了一系列它的實例的特點于购,包括成員變量的列表袍睡、成員函數(shù)的列表等。每一個對象都可以接受到消息肋僧,而對象能夠接受到的消息列表保存在它所對應(yīng)的類中斑胜。
注意:
每一個對象都有一個isa指針,這個指針指向的是它的類嫌吠。
類中包括成員變量止潘、成員函數(shù)列表等。
在Xcode中打開objc.h文件辫诅,會看到如下代碼:
/// Represents an instance of a class.
struct objc_object {
Class isa ?OBJC_ISA_AVAILABILITY;
};
通過注釋我們看到objc_object代表一個對象的實例凭戴,在對象實例中我們看到了isa指針,驗證了我們剛才說的話炕矮。
根據(jù)面向?qū)ο蟮脑O(shè)計原則么夫,所有事物都應(yīng)該是對象,所以在Objective-C中肤视,每一個類實際上也是一個對象档痪,每一個類也有一個名為isa的指針,每一個類也可以接收消息邢滑,例如代碼[NSObject alloc]腐螟,就是向NSObject這個類發(fā)送名為alloc的消息。
在Xcode中打開runtime.h文件困后,會看到如下代碼:
struct objc_class {
Class isa ?OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?OBJC2_UNAVAILABLE;
const char *name ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
long version ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
long info ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?OBJC2_UNAVAILABLE;
long instance_size ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists ? ? ? ? ? ? ? ? ? ?OBJC2_UNAVAILABLE;
struct objc_cache *cache ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols ? ? ? ? ? ? ? ? ? ? OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
objc_class代表一個類乐纸,從上面代碼中可以看出類中有一個isa指針的。前面說到isa指針會執(zhí)行它的類操灿,那類中的isa指針指向什么呢锯仪?因為類也是一個對象,所以它也必須是另一個類的實例趾盐,這個類就是元類(metaclass)庶喜,所以isa指針指向的是它的元類。元類保存了類的方法列表救鲤。當(dāng)一個類的方法被調(diào)用時久窟,元類會首先查找它本身是否有該類方法的實現(xiàn),如果沒有本缠,則該元類會向它的父類查找該方法斥扛,這樣可以一直找到繼承鏈的頭。
如前面所說元類也是一個對象,那么元類的isa指針指向誰呢稀颁?Objective-C為了設(shè)計上的完整芬失,所有的元類的isa指針都會指向一個根元類(root metaclass),根元類的isa指針指向自己匾灶,這樣就形成一個閉環(huán)棱烂。上面說到,一個對象能夠接收的消息列表是保存在它所對應(yīng)的類中的阶女。在實際編程中颊糜,我們幾乎不會遇到向元類發(fā)消息的情況,那它的isa指針在實際上很少用到秃踩。
再來看看繼承關(guān)系衬鱼,由于類方法的定義是保存在元類中,而方法調(diào)用的規(guī)則是憔杨,如果該類沒有一個方法的實現(xiàn)鸟赫,則向它的父類繼續(xù)查找。所以消别,為了保證父類的類方法在子類中可以被調(diào)用惯疙,所有子類的元類都會繼承父類的元類,簡單來說就是類對象和元類對象有著同樣的繼承關(guān)系妖啥。
最后用一張圖對對象模型做一個總結(jié)霉颠,如下圖:
1-1 對象模型.png
objc_msgSend
Objective-C運行時的核心就在于消息分派器objc_msgSend,消息分派器把選擇器映射為函數(shù)指針荆虱,并調(diào)用被引用的函數(shù)蒿偎。 要想理解objc_msgSend的背后原理,先來理解下NSInvocation這個類怀读。
NSInvocation是命令模式的一種傳統(tǒng)實現(xiàn)诉位,它把一個目標(biāo)、一個選擇器菜枷、一個方法簽名和所有的參數(shù)都塞進(jìn)一個對象里苍糠,這個對象可以先存儲起來,以備將來調(diào)用啤誊。當(dāng)NSInvocation被調(diào)用時岳瞭,它會發(fā)送信息,Objective-C運行時會找到正確的方法實現(xiàn)來執(zhí)行蚊锹。我們通過一個例子來理解下NSInvocation的作用瞳筏,比如[NSObject alloc],此時會發(fā)送一個alloc消息牡昆,這條消息都包含什么內(nèi)容呢姚炕?它怎么找到alloc的實現(xiàn)方法呢?這些都是通過NSInvocation來完成的,它包含了消息要傳遞的內(nèi)容柱宦,也告訴了該怎么找到對應(yīng)的方法實現(xiàn)些椒。
解釋一下什么是方法實現(xiàn)?一個方法實現(xiàn)(IMP)是一個指向具有如下簽名的C函數(shù)的函數(shù)指針掸刊,注意是指針摊沉。
id function(id self, SEL _cmd, ...)
NSInvocation包含了一個目標(biāo)和選擇器,目標(biāo)是一個可接受的對象痒给,選擇器則是被發(fā)送的消息。比如[NSObject alloc]骏全,目標(biāo)就是NSObject苍柏,選擇器就是alloc。一個選擇器大致是一個方法的名稱姜贡,之所以說是大致是因為選擇器不必精確映射到方法试吁。比如[NSString length]和[NSData length]會映射到不同方法的實現(xiàn),但他們擁有相同的選擇器楼咳。
NSInvocation還包含一個方法簽名(NSMethodSignature)熄捍,它封裝了一個方法的返回類型和參數(shù)類型,記住它不包括方法名稱母怜,只有返回類型和參數(shù)類型余耽。你可以手動創(chuàng)建一個方法簽名,如下:
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"@@:*"];
但是應(yīng)該盡可能少使用signatureWithObjCTypes:方法苹熏,獲得方法簽名常用的方法是為它請求一個類或?qū)嵗郑热缈梢允褂胢ethodSignatureForSelector:方法從實例中請求實例方法簽名,或者從類中請求類方法簽名轨域。也可以使用instanceMethodSignatureForSelector:方法從一個類中獲取實例方法簽名袱耽。兩個方法有點繞口,我們通過一個例子來看下區(qū)別:
SEL initSEL = @selector(init);
SEL allocSEL = @selector(alloc);
// 從NSString類中獲取實例方法(init)的方法簽名
NSMethodSignature *initSig = [NSString instanceMethodSignatureForSelector:initSEL];
// 從test實例中獲取實例方法(init)的方法簽名
initSig = [@"test" methodSignatureForSelector:initSEL];
// 從NSString類中獲取類方法簽名
NSMethodSignature *allocSig = [NSString methodSignatureForSelector:allocSEL];
最后干发,NSInvocation還包含了所有的參數(shù)朱巨。至此,對于[NSString length]和[NSData length]就可以通過NSInvocation對象包含的信息枉长,找到它們分別對應(yīng)的方法實現(xiàn)冀续。我們來看一個具體的例子,如下:
NSMutableSet *set = [NSMutableSet set];
NSString *stuff = @"stuff";
SEL selector = @selector(addObject:);
NSMethodSignature *sig = [set methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:set];
[invocation setSelector:selector];
[invocation setArgument:&stuff atIndex:2];
[invocation invoke];
NSLog(@"set is : %@", set);
注意必峰,第一個參數(shù)被置于索引2處沥阳,索引0是目標(biāo)(self),索引1是選擇器(_cmd)自点,NSInvocation會自動設(shè)置它們桐罕。另外,必須把參數(shù)指針傳遞給參數(shù),而不能傳遞參數(shù)本身功炮。
接下來重點要介紹下消息傳遞是如何工作的溅潜?
在Objective-C中調(diào)用方法最終會翻譯成調(diào)用方法實現(xiàn)的函數(shù)指針,并傳遞給這個方法實現(xiàn)一個對象指針薪伏、一個選擇器和一組函數(shù)參數(shù)滚澜。每個Objective-C消息表達(dá)式都會轉(zhuǎn)化為對objc_msgSend的調(diào)用,看下objc_msgSend的工作方式:
檢查接受對象是否為nil嫁怀,如果是nil设捐,調(diào)用nil處理程序。
檢查緩存中是不是已經(jīng)有方法實現(xiàn)了塘淑,有的話萝招,直接調(diào)用。
比較請求的選擇器和類中定義的選擇器存捺,如果找到了槐沼,調(diào)用方法實現(xiàn)。
比較請求的選擇器和父類中定義的選擇器捌治,然后是父類的父類岗钩,以此類推,如果找到了選擇器肖油,調(diào)用方法實現(xiàn)兼吓。
調(diào)用resolveInstanceMethod:(或resolveClassMethod)。如果它返回YES森枪,那么重新開始周蹭。這一次對象會響應(yīng)這個選擇器,一般是因為它已經(jīng)調(diào)用過class_addMethod疲恢。
調(diào)用forwardingTargetForSelector:凶朗,如果返回非nil,那就把消息發(fā)送到返回的對象上显拳,這里不要返回self棚愤,否則會形成死循環(huán)的。
調(diào)用methodSignatureForSelector:杂数,如果返回非nil宛畦,創(chuàng)建一個NSInvocation并傳給forwardInvocation:。
調(diào)用doesNotRecognizeSelector:揍移,默認(rèn)的實現(xiàn)是拋出異常次和。
先看下第5步,首先可以想到的就是用resolveInstanceMethod:和resolveClassMethod:在運行時提供實現(xiàn)那伐,這通常是@dynamic合成屬性的處理方式踏施。簡單來說石蔗,就是需要自己實現(xiàn)屬性的getter和setter方法,通過resolveInstanceMethod:方法來把setter方法和getter方法和屬性綁定在一起畅形。
如果第5步返回NO的話养距,系統(tǒng)接著會首先嘗試一次快速轉(zhuǎn)發(fā),也就是調(diào)用forwardingTargetForSelector:日熬,看其能否返回一個對象棍厌,如果有對象返回,就轉(zhuǎn)發(fā)給返回的對象竖席≡派矗快速轉(zhuǎn)發(fā)的原理其實就是先從緩存里找下是否存在對應(yīng)的選擇器。
如果快速轉(zhuǎn)發(fā)返回nil的話毕荐,接下來就進(jìn)行普通的轉(zhuǎn)發(fā)束析,調(diào)用forwardInvocation進(jìn)行普通的轉(zhuǎn)發(fā)。
objc_msgSend還有幾個相關(guān)的函數(shù):objc_msgSend_fpret东跪、objc_msgSendSuper、objc_msgSend_stret鹰溜、objc_msgSendSuper_stret虽填。
SendSuper格式的函數(shù)很明顯是把消息發(fā)送給父類,而帶stret的在返回結(jié)構(gòu)體時處理大部分情況曹动。在Intel處理器上返回浮點數(shù)時斋日,帶fpret的函數(shù)處理大部分情況。
方法混寫(Method Swizzling)與ISA混寫(Isa Swizzling)
在Objective-C中墓陈,混寫(Swizzling)是指透明地把一個東西換成另一個恶守,我們可以利用Objective-C中的運行時來實現(xiàn)混寫。我們先看下方法混寫贡必,Objective-C提供了以下API來動態(tài)替換類方法或?qū)嵗椒ǖ膶崿F(xiàn):
class_replaceMethod替換類方法的定義兔港。
method_exchangeImplementations交換兩個方法的實現(xiàn)。
method_setImplementation設(shè)置一個方法的實現(xiàn)仔拟。
我們來看下三者的區(qū)別:
class_replaceMethod衫樊,當(dāng)需要替換的方法有可能不存在時,可以考慮使用該方法利花。
method_exchangeImplementations科侈,當(dāng)需要交換兩個方法的實現(xiàn)時使用。
method_setImplementation是最簡單的用法炒事,當(dāng)僅僅需要為一個方法設(shè)置其實現(xiàn)方式時使用臀栈。
系統(tǒng)中提供的KVO使用到了isa混寫,具體是怎么實現(xiàn)的呢挠乳?當(dāng)你觀察一個對象時权薯,一個新的類會被自動創(chuàng)建姑躲,這個新類繼承自該對象的原本的類,并且重寫了被觀察屬性setter方法崭闲。重寫setter方法會負(fù)責(zé)在調(diào)用原setter方法之前和之后肋联,通知所有觀察對象:值的更改。最后通過isa混寫刁俭,把這個對象的isa指針指向這個新創(chuàng)建的子類橄仍,對象就神奇的變成了新創(chuàng)建的子類的實例。
注意一點:把isa指針指向新創(chuàng)建的子類牍戚,被觀察的對象就變成了新創(chuàng)建子類的對象實例侮繁,這是由于isa指針永遠(yuǎn)指向其對應(yīng)的類。用一張圖來說明下:
1-2 KVO.png
鍵值觀察通知依賴于NSObject的兩個方法:willChangeValueForKey:和didChangeValueForKey:如孝。在一個被觀察屬性發(fā)生改變之前宪哩,willChangeValueForKey:一定會被調(diào)用,這就會記錄舊的值第晰。而當(dāng)改變之后锁孟,didChangeValueForKey:會被調(diào)用,繼而obserValueForKey:ofObject:change:context:也會被調(diào)用茁瘦∑烦椋可以手動實現(xiàn)這些調(diào)用,但很少有人這么做甜熔。一般我們希望能控制回調(diào)的調(diào)用時機時才會這么做圆恤。
使用KVO的一個明顯的優(yōu)勢就是零開銷觀察的優(yōu)勢,如果給定的實例沒有觀察者腔稀,那么KVO不會有任何消耗盆昙,因為根本沒有KVO代碼。而即使沒有觀察者焊虏,對于委托方法和NSNotification還得工作淡喜。