iOS 中的runtime與消息轉發

在80年代初,小李和小王是異地戀的情侶,小王在改革號角的引領下毅然選擇了南方的一個城市去奮鬥,而那個時候沒有手機,他們之間的互訴相思的方式主要依靠寫信。但是由於小王又經常出差,居住地址會經常變動。所以小李每次給小王的回信,小王可能因爲地址的變動而沒有收到,他們後來想到了一個好辦法來解決這個問題,具體的方法如下:

80年代的消息轉發

 

其實上面這張圖,基本上就可以表達Runtime在iOS中的作用以及iOS的消息轉發機制。Runtime的特性主要是消息(方法)傳遞,如果消息(方法)在對象中找不到,就進行轉發,具體怎麼實現的呢。我們從下面幾個方面探尋Runtime的實現機制。

Runtime介紹

Objective-C 擴展了 C 語言,並加入了面向對象特性和 Smalltalk 式的消息傳遞機制。而這個擴展的核心是一個用 C 和 編譯語言 寫的 Runtime 庫。它是 Objective-C 面向對象和動態機制的基石。

Objective-C 是一個動態語言,這意味着它不僅需要一個編譯器,也需要一個運行時系統來動態得創建類和對象、進行消息傳遞和轉發。理解 Objective-C 的 Runtime 機制可以幫我們更好的瞭解這個語言,適當的時候還能對語言進行擴展,從系統層面解決項目中的一些設計或技術問題。瞭解 Runtime ,要先了解它的核心 - 消息傳遞 (Messaging)。

高級編程語言想要成爲可執行文件需要先編譯爲彙編語言再彙編爲機器語言,機器語言也是計算機能夠識別的唯一語言,但是OC並不能直接編譯爲彙編語言,而是要先轉寫爲純C語言再進行編譯和彙編的操作,從OC到C語言的過渡就是由runtime來實現的。然而我們使用OC進行面向對象開發,而C語言更多的是面向過程開發,這就需要將面向對象的類轉變爲面向過程的結構體。

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

Runtime消息傳遞

一個對象的方法  [obj test],編譯器轉成消息發送objc_msgSend(obj, test),Runtime時執行的流程是這樣的:

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

2.在classmethod listtest;

3.如果class中沒到test,繼續往它的superclass中找 ;

4.一旦找到test這個函數,就去執行它的實現IMP

當然了,由於效率的問題,每個消息都遍歷一次objc_method_list並不合理。所以需要把經常被調用的函數緩存下來,去提高函數查詢的效率。這也就是objc_class中另一個重要成員objc_cache做的事情 - 再找到test之後,把test的method_name作爲key,method_imp作爲value給存起來。當再次收到test消息的時候,可以直接在cache裏找到,避免去遍歷objc_method_list。從前面的源代碼可以看到objc_cache是存在objc_class結構體中的。

objec_msgSend的方法:

OBJC_EXPORTidobjc_msgSend(idself, SEL op, ...)

我們看看對象(object),類(class),方法(method)這幾個的結構體:

類對象(objc_class)

Objective-C類是由Class類型來表示的,它實際上是一個指向objc_class結構體的指針

struct objc_class結構體定義了很多變量。結構體裏保存了指向父類的指針、類的名字、版本、實例大小、實例變量列表、方法列表、緩存、遵守的協議列表等,由此可見,類對象就是一個結構體struct objc_class,這個結構體存放的數據就是元數據(metadata)。

實例(objc_object)

 

類對象中的元數據存儲的都是如何創建一個實例的相關信息,就是從isa指針指向的結構體創建,類對象的isa指針指向的我們稱之爲元類(metaclass)

元類中保存了創建類對象以及類方法所需的所有信息,因此整個結構應該如下圖所示:

實例對象、類對象與元類簡圖

struct objc_object結構體它的isa指針指向類對象;

類對象的isa指針指向了元類;

super_class指針指向了父類的類對象;

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

有點繞口令的感覺,那麼就可以用網上的一個神圖來表示了:

圖6 實例對象、類對象與元類的自閉環

通過上圖我們可以看出整個體系構成了一個自閉環,如果是從NSObject中繼承而來的上圖中的Root class就是NSObject。

 

c1是通過一個實例對象獲取的Class,實例對象可以獲取到其類對象,類名作爲消息的接受者時代表的是類對象,因此類對象獲取Class得到的是其本身。

如果我們想要獲取ISA指針的對象的話,可以用下面這兩個函數

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對象是否爲元類,object_getClass用於獲取對象的isa指針指向的對象。

 

通過代碼可以看出,一個實例對象通過class方法獲取的Class就是它的isa指針指向的類對象,而類對象不是元類,類對象的isa指針指向的對象是元類。

所以關於Runtime部分,我們總結一下就是:首先實例對象是一個結構體,這個結構體只有一個成員變量,指向構造它的那個類對象,這個類對象中存儲了一切實例對象需要的信息包括實例變量、實例方法等,而類對象是通過元類創建的,元類中保存了類變量和類方法,這樣就完美解釋了整個類和實例是如何映射到結構體的。所以理解Runtime就是理解iOS在運行時他的數據存儲以及他的類、實例、類對象、元類他們之間的關係和作用。

消息轉發機制

 上述講了很多Runtime的基本理解和概念,那到底他和消息轉發有什麼關係呢,以及怎麼去運用它呢?這就要講到iOS的消息轉發機制了。

歸根到底,Objective-C中所有的方法調用本質就是向對象發送消息。

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

2.iOS系統爲這個方法創建一個編號即:SEL(todoSomething);並添加到方法列表中。(selector是SEL的一個實例,這點和IMP是不一樣的,IMP是指向最終實現程序的內存地址的指針)

3.當調用這個方法的時候:[Object todoSomething]; 系統去方法列表中插手這個方法編號,查到就執行。

注意:我們在寫C代碼的時候,經常會用到函數重載,就是函數名相同,參數不同,但是這在Objective-C中是行不通的,因爲selector只記了method的name,沒有參數,所以沒法區分不同的method。

所以如果調用了一個方法,就會進行一次發送消息會在相關的類對象中搜索方法列表,如果找不到則會沿着繼承樹向上一直搜索知道繼承樹根部(通常爲NSObject),如果還是找不到並且消息轉發都失敗了就回執行doesNotRecognizeSelector:方法報unrecognized selector錯。那麼消息轉發到底是什麼呢?接下來將會逐一介紹最後的三次機會。

1.動態方法解析

Objective-C運行時會調用 +resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。如果你添加了函數並返回YES, 那運行時系統就會重新啓動一次消息發送的過程。如下圖實例

打印出了“Doing foo”

可以看到雖然沒有實現foo:這個函數,但是我們通過class_addMethod動態添加fooMethod函數,並執行fooMethod這個函數的IMP。從打印結果看,成功實現了。

如果resolve方法返回 NO ,運行時就會移到下一步:forwardingTargetForSelector。

備用接收者

如果目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其他對象的機會。

實現一個備用接收者的例子如下:

可以看到我們通過forwardingTargetForSelector把當前ViewController的方法轉發給了Person去執行了。打印結果也證明我們成功實現了轉發。

完整消息轉發

如果在上一步還不能處理未知消息,則唯一能做的就是啓用完整的消息轉發機制了。

首先它會發送-methodSignatureForSelector:消息獲得函數的參數和返回值類型。如果-methodSignatureForSelector:返回nil,Runtime則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。如果返回了一個函數簽名,Runtime就會創建一個NSInvocation對象併發送-forwardInvocation:消息給目標對象。

也打印出“Doing foo”

這就是Runtime的三次轉發流程。下面我們講講Runtime的實際應用

 

當系統自帶的方法功能不夠,可以給系統自帶的方法擴展一些功能,並保持原有的功能。例如我想知道當前的URL是否爲空如果每次都判斷一下的話會很麻煩,如果我創建擴展來寫,又不知道內部是如何實現的.

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

二、也可以動態添加方法

三、 給分類添加屬性

 

四、KVO實現

KVO的實現依賴於 Objective-C 強大的 Runtime,當觀察某對象 A 時,KVO 機制動態創建一個對象A當前類的子類,併爲這個新的子類重寫了被觀察屬性 keyPath 的 setter 方法。setter 方法隨後負責通知觀察對象屬性的改變狀況。

五、消息轉發(熱更新)解決Bug(JSPatch)

關於消息轉發,消息轉發分爲三級,我們可以在每級實現替換功能,實現消息轉發,從而不會造成崩潰。JSPatch不僅能夠實現消息轉發,還可以實現方法添加、替換能一系列功能

六、實現NSCoding的自動歸檔和自動解檔

原理描述:用runtime提供的函數遍歷Model自身所有屬性,並對屬性進行encode和decode操作。

核心方法:在Model的基類中重寫方法:

 

總結:在整個Objective-C運行中,所有的方法調用都是消息的發送或轉發的過程,最後可以把的第一個圖大致變成下面這樣的,方便理解

 



作者:Jaren_lei
鏈接:https://www.jianshu.com/p/45db86af7b60

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章