iOS進(jìn)階:Objective-C底層原理

這篇讀書筆記主要介紹了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還得工作淡喜。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市诵闭,隨后出現(xiàn)的幾起案子拆火,更是在濱河造成了極大的恐慌,老刑警劉巖涂圆,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件们镜,死亡現(xiàn)場離奇詭異,居然都是意外死亡润歉,警方通過查閱死者的電腦和手機模狭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來踩衩,“玉大人嚼鹉,你說我怎么就攤上這事贩汉。” “怎么了锚赤?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵匹舞,是天一觀的道長。 經(jīng)常有香客問我线脚,道長赐稽,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任浑侥,我火速辦了婚禮姊舵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘寓落。我一直安慰自己括丁,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布伶选。 她就那樣靜靜地躺著史飞,像睡著了一般。 火紅的嫁衣襯著肌膚如雪仰税。 梳的紋絲不亂的頭發(fā)上构资,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天,我揣著相機與錄音肖卧,去河邊找鬼蚯窥。 笑死掸鹅,一個胖子當(dāng)著我的面吹牛塞帐,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巍沙,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼葵姥,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了句携?” 一聲冷哼從身側(cè)響起榔幸,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎矮嫉,沒想到半個月后削咆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡蠢笋,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年拨齐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片昨寞。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡瞻惋,死狀恐怖厦滤,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情歼狼,我是刑警寧澤掏导,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站羽峰,受9級特大地震影響趟咆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜限寞,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一忍啸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧履植,春花似錦计雌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至庶近,卻和暖如春翁脆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背鼻种。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工反番, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人叉钥。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓罢缸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親投队。 傳聞我的和親對象是個殘疾皇子枫疆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內(nèi)容