iOS 中的runtime與消息轉(zhuǎn)發(fā)

在80年代初,小李和小王是異地戀的情侶,小王在改革號(hào)角的引領(lǐng)下毅然選擇了南方的一個(gè)城市去奮斗,而那個(gè)時(shí)候沒(méi)有手機(jī),他們之間的互訴相思的方式主要依靠寫(xiě)信。但是由于小王又經(jīng)常出差,居住地址會(huì)經(jīng)常變動(dòng)。所以小李每次給小王的回信,小王可能因?yàn)榈刂返淖儎?dòng)而沒(méi)有收到,他們后來(lái)想到了一個(gè)好辦法來(lái)解決這個(gè)問(wèn)題,具體的方法如下:

80年代的消息轉(zhuǎn)發(fā)


其實(shí)上面這張圖,基本上就可以表達(dá)Runtime在iOS中的作用以及iOS的消息轉(zhuǎn)發(fā)機(jī)制。Runtime的特性主要是消息(方法)傳遞,如果消息(方法)在對(duì)象中找不到,就進(jìn)行轉(zhuǎn)發(fā),具體怎么實(shí)現(xiàn)的呢。我們從下面幾個(gè)方面探尋Runtime的實(shí)現(xiàn)機(jī)制。

Runtime介紹

Objective-C 擴(kuò)展了 C 語(yǔ)言,并加入了面向?qū)ο筇匦院?Smalltalk 式的消息傳遞機(jī)制。而這個(gè)擴(kuò)展的核心是一個(gè)用 C 和 編譯語(yǔ)言 寫(xiě)的 Runtime 庫(kù)。它是 Objective-C 面向?qū)ο蠛蛣?dòng)態(tài)機(jī)制的基石。

Objective-C 是一個(gè)動(dòng)態(tài)語(yǔ)言,這意味著它不僅需要一個(gè)編譯器,也需要一個(gè)運(yùn)行時(shí)系統(tǒng)來(lái)動(dòng)態(tài)得創(chuàng)建類和對(duì)象、進(jìn)行消息傳遞和轉(zhuǎn)發(fā)。理解 Objective-C 的 Runtime 機(jī)制可以幫我們更好的了解這個(gè)語(yǔ)言,適當(dāng)?shù)臅r(shí)候還能對(duì)語(yǔ)言進(jìn)行擴(kuò)展,從系統(tǒng)層面解決項(xiàng)目中的一些設(shè)計(jì)或技術(shù)問(wèn)題。了解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。

高級(jí)編程語(yǔ)言想要成為可執(zhí)行文件需要先編譯為匯編語(yǔ)言再匯編為機(jī)器語(yǔ)言,機(jī)器語(yǔ)言也是計(jì)算機(jī)能夠識(shí)別的唯一語(yǔ)言,但是OC并不能直接編譯為匯編語(yǔ)言,而是要先轉(zhuǎn)寫(xiě)為純C語(yǔ)言再進(jìn)行編譯和匯編的操作,從OC到C語(yǔ)言的過(guò)渡就是由runtime來(lái)實(shí)現(xiàn)的。然而我們使用OC進(jìn)行面向?qū)ο箝_(kāi)發(fā),而C語(yǔ)言更多的是面向過(guò)程開(kāi)發(fā),這就需要將面向?qū)ο蟮念愞D(zhuǎn)變?yōu)槊嫦蜻^(guò)程的結(jié)構(gòu)體。

上述都是官方的文檔釋義,有些晦澀無(wú)聊,接下來(lái)我們用代碼來(lái)具體解釋一下。

Runtime消息傳遞

一個(gè)對(duì)象的方法 ?[obj test],編譯器轉(zhuǎn)成消息發(fā)送objc_msgSend(obj, test),Runtime時(shí)執(zhí)行的流程是這樣的:

1.首先,通過(guò)objisa指針找到它的class;

2.在classmethod listtest;

3.如果class中沒(méi)到test,繼續(xù)往它的superclass中找 ;

4.一旦找到test這個(gè)函數(shù),就去執(zhí)行它的實(shí)現(xiàn)IMP

當(dāng)然了,由于效率的問(wèn)題,每個(gè)消息都遍歷一次objc_method_list并不合理。所以需要把經(jīng)常被調(diào)用的函數(shù)緩存下來(lái),去提高函數(shù)查詢的效率。這也就是objc_class中另一個(gè)重要成員objc_cache做的事情 - 再找到test之后,把test的method_name作為key,method_imp作為value給存起來(lái)。當(dāng)再次收到test消息的時(shí)候,可以直接在cache里找到,避免去遍歷objc_method_list。從前面的源代碼可以看到objc_cache是存在objc_class結(jié)構(gòu)體中的。

objec_msgSend的方法:

OBJC_EXPORTidobjc_msgSend(idself, SEL op, ...)

我們看看對(duì)象(object),類(class),方法(method)這幾個(gè)的結(jié)構(gòu)體:

類對(duì)象(objc_class)

Objective-C類是由Class類型來(lái)表示的,它實(shí)際上是一個(gè)指向objc_class結(jié)構(gòu)體的指針

struct objc_class結(jié)構(gòu)體定義了很多變量。結(jié)構(gòu)體里保存了指向父類的指針、類的名字、版本、實(shí)例大小、實(shí)例變量列表、方法列表、緩存、遵守的協(xié)議列表等,由此可見(jiàn),類對(duì)象就是一個(gè)結(jié)構(gòu)體struct objc_class,這個(gè)結(jié)構(gòu)體存放的數(shù)據(jù)就是元數(shù)據(jù)(metadata)。

實(shí)例(objc_object)


類對(duì)象中的元數(shù)據(jù)存儲(chǔ)的都是如何創(chuàng)建一個(gè)實(shí)例的相關(guān)信息,就是從isa指針指向的結(jié)構(gòu)體創(chuàng)建,類對(duì)象的isa指針指向的我們稱之為元類(metaclass)

元類中保存了創(chuàng)建類對(duì)象以及類方法所需的所有信息,因此整個(gè)結(jié)構(gòu)應(yīng)該如下圖所示:

實(shí)例對(duì)象、類對(duì)象與元類簡(jiǎn)圖

struct objc_object結(jié)構(gòu)體它的isa指針指向類對(duì)象;

類對(duì)象的isa指針指向了元類;

super_class指針指向了父類的類對(duì)象;

而元類的super_class指針指向了父類的元類;

有點(diǎn)繞口令的感覺(jué),那么就可以用網(wǎng)上的一個(gè)神圖來(lái)表示了:

圖6 實(shí)例對(duì)象、類對(duì)象與元類的自閉環(huán)

通過(guò)上圖我們可以看出整個(gè)體系構(gòu)成了一個(gè)自閉環(huán),如果是從NSObject中繼承而來(lái)的上圖中的Root class就是NSObject。


c1是通過(guò)一個(gè)實(shí)例對(duì)象獲取的Class,實(shí)例對(duì)象可以獲取到其類對(duì)象,類名作為消息的接受者時(shí)代表的是類對(duì)象,因此類對(duì)象獲取Class得到的是其本身。

如果我們想要獲取ISA指針的對(duì)象的話,可以用下面這兩個(gè)函數(shù)

OBJC_EXPORTBOOLclass_isMetaClass(Classcls)?OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

OBJC_EXPORTClassobject_getClass(idobj)?OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);

class_isMetaClass用于判斷Class對(duì)象是否為元類,object_getClass用于獲取對(duì)象的isa指針指向的對(duì)象。


通過(guò)代碼可以看出,一個(gè)實(shí)例對(duì)象通過(guò)class方法獲取的Class就是它的isa指針指向的類對(duì)象,而類對(duì)象不是元類,類對(duì)象的isa指針指向的對(duì)象是元類。

所以關(guān)于Runtime部分,我們總結(jié)一下就是:首先實(shí)例對(duì)象是一個(gè)結(jié)構(gòu)體,這個(gè)結(jié)構(gòu)體只有一個(gè)成員變量,指向構(gòu)造它的那個(gè)類對(duì)象,這個(gè)類對(duì)象中存儲(chǔ)了一切實(shí)例對(duì)象需要的信息包括實(shí)例變量、實(shí)例方法等,而類對(duì)象是通過(guò)元類創(chuàng)建的,元類中保存了類變量和類方法,這樣就完美解釋了整個(gè)類和實(shí)例是如何映射到結(jié)構(gòu)體的。所以理解Runtime就是理解iOS在運(yùn)行時(shí)他的數(shù)據(jù)存儲(chǔ)以及他的類、實(shí)例、類對(duì)象、元類他們之間的關(guān)系和作用。

消息轉(zhuǎn)發(fā)機(jī)制

?上述講了很多Runtime的基本理解和概念,那到底他和消息轉(zhuǎn)發(fā)有什么關(guān)系呢,以及怎么去運(yùn)用它呢?這就要講到iOS的消息轉(zhuǎn)發(fā)機(jī)制了。

歸根到底,Objective-C中所有的方法調(diào)用本質(zhì)就是向?qū)ο蟀l(fā)送消息。

1.類中創(chuàng)建方法 -(void)todoSomething;

2.iOS系統(tǒng)為這個(gè)方法創(chuàng)建一個(gè)編號(hào)即:SEL(todoSomething);并添加到方法列表中。(selector是SEL的一個(gè)實(shí)例,這點(diǎn)和IMP是不一樣的,IMP是指向最終實(shí)現(xiàn)程序的內(nèi)存地址的指針)

3.當(dāng)調(diào)用這個(gè)方法的時(shí)候:[Object todoSomething]; 系統(tǒng)去方法列表中插手這個(gè)方法編號(hào),查到就執(zhí)行。

注意:我們?cè)趯?xiě)C代碼的時(shí)候,經(jīng)常會(huì)用到函數(shù)重載,就是函數(shù)名相同,參數(shù)不同,但是這在Objective-C中是行不通的,因?yàn)閟elector只記了method的name,沒(méi)有參數(shù),所以沒(méi)法區(qū)分不同的method。

所以如果調(diào)用了一個(gè)方法,就會(huì)進(jìn)行一次發(fā)送消息會(huì)在相關(guān)的類對(duì)象中搜索方法列表,如果找不到則會(huì)沿著繼承樹(shù)向上一直搜索知道繼承樹(shù)根部(通常為NSObject),如果還是找不到并且消息轉(zhuǎn)發(fā)都失敗了就回執(zhí)行doesNotRecognizeSelector:方法報(bào)unrecognized selector錯(cuò)。那么消息轉(zhuǎn)發(fā)到底是什么呢?接下來(lái)將會(huì)逐一介紹最后的三次機(jī)會(huì)。

1.動(dòng)態(tài)方法解析

Objective-C運(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ā)送的過(guò)程。如下圖實(shí)例

打印出了“Doing foo”

可以看到雖然沒(méi)有實(shí)現(xiàn)foo:這個(gè)函數(shù),但是我們通過(guò)class_addMethod動(dòng)態(tài)添加fooMethod函數(shù),并執(zhí)行fooMethod這個(gè)函數(shù)的IMP。從打印結(jié)果看,成功實(shí)現(xiàn)了。

如果resolve方法返回?NO?,運(yùn)行時(shí)就會(huì)移到下一步:forwardingTargetForSelector。

備用接收者

如果目標(biāo)對(duì)象實(shí)現(xiàn)了-forwardingTargetForSelector:,Runtime?這時(shí)就會(huì)調(diào)用這個(gè)方法,給你把這個(gè)消息轉(zhuǎn)發(fā)給其他對(duì)象的機(jī)會(huì)。

實(shí)現(xiàn)一個(gè)備用接收者的例子如下:

可以看到我們通過(guò)forwardingTargetForSelector把當(dāng)前ViewController的方法轉(zhuǎn)發(fā)給了Person去執(zhí)行了。打印結(jié)果也證明我們成功實(shí)現(xiàn)了轉(zhuǎn)發(fā)。

完整消息轉(zhuǎn)發(fā)

如果在上一步還不能處理未知消息,則唯一能做的就是啟用完整的消息轉(zhuǎn)發(fā)機(jī)制了。

首先它會(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ì)象。

也打印出“Doing foo”

這就是Runtime的三次轉(zhuǎn)發(fā)流程。下面我們講講Runtime的實(shí)際應(yīng)用


當(dāng)系統(tǒng)自帶的方法功能不夠,可以給系統(tǒng)自帶的方法擴(kuò)展一些功能,并保持原有的功能。例如我想知道當(dāng)前的URL是否為空如果每次都判斷一下的話會(huì)很麻煩,如果我創(chuàng)建擴(kuò)展來(lái)寫(xiě),又不知道內(nèi)部是如何實(shí)現(xiàn)的.

一、可以使用runtime交換方法。

二、也可以動(dòng)態(tài)添加方法

三、?給分類添加屬性


四、KVO實(shí)現(xiàn)

KVO的實(shí)現(xiàn)依賴于?Objective-C?強(qiáng)大的?Runtime,當(dāng)觀察某對(duì)象?A?時(shí),KVO?機(jī)制動(dòng)態(tài)創(chuàng)建一個(gè)對(duì)象A當(dāng)前類的子類,并為這個(gè)新的子類重寫(xiě)了被觀察屬性?keyPath?的?setter?方法。setter?方法隨后負(fù)責(zé)通知觀察對(duì)象屬性的改變狀況。

五、消息轉(zhuǎn)發(fā)(熱更新)解決Bug(JSPatch)

關(guān)于消息轉(zhuǎn)發(fā),消息轉(zhuǎn)發(fā)分為三級(jí),我們可以在每級(jí)實(shí)現(xiàn)替換功能,實(shí)現(xiàn)消息轉(zhuǎn)發(fā),從而不會(huì)造成崩潰。JSPatch不僅能夠?qū)崿F(xiàn)消息轉(zhuǎn)發(fā),還可以實(shí)現(xiàn)方法添加、替換能一系列功能

六、實(shí)現(xiàn)NSCoding的自動(dòng)歸檔和自動(dòng)解檔

原理描述:用runtime提供的函數(shù)遍歷Model自身所有屬性,并對(duì)屬性進(jìn)行encode和decode操作。

核心方法:在Model的基類中重寫(xiě)方法:


總結(jié):在整個(gè)Objective-C運(yùn)行中,所有的方法調(diào)用都是消息的發(fā)送或轉(zhuǎn)發(fā)的過(guò)程,最后可以把的第一個(gè)圖大致變成下面這樣的,方便理解

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容