Objective-C Runtime 消息機制 - 代碼背后發生的事情
說到 Objective-C Runtime ,可能不是大家常常提及的內容。但它確實又和大家平時的開發過程息息相關,即使使用 Swift 語言,也依然離不開 Objective-C Runtime。 咱們就來一探究竟吧。
什么是 Objective-C Runtime
使用過 Objective-C 進行開發的同學一定會注意到 Objective-C 中的 Selector 機制。 為什么要把它稱為 Selector 呢,它和函數和方法有什么區別呢? 比如給 UIButton 添加事件的時候:
[button addTarget:self action:@selector(buttonClciked) forControlEvents:UIControlEventTouchUpInside];
</div>
為什么給 action 參數傳遞進來的是一個 selector 而不是一個函數的名稱呢?
這就要從 Objective-C Runtime 說起。 所有的 Objective-C 方法調用都是基于 Objective-C Runtime 進行的。 比如最簡單的方法調用:
[person sayHello];
</div>
如果按照面向對象的思維去解釋,可以將這行代碼解釋為調用 person 對象的 sayHello 方法。 但如果從 Objective-C Runtime 的角度來說,這個代碼實際上是在發送一個 消息 。
要牢牢記住上面的代碼是 發送消息 。 剛剛那段代碼,編譯器實際上會將它轉換成這樣一個函數調用:
objc_msgSend(person,sayHello)
</div>
objc_msgSend 是 Objective-C Runtime 中的函數,這個函數定義在 <objc/message.h> 頭文件中。
我們在 Objective-C 中所有通過一對方括號所進行的方法調用,其實都是通過 Objective-C Runtime 的 objc_msgSend 函數發送的一個消息傳遞。
objc_msgSend
那么既然所有的方法調用本質上都是通過 objc_msgSend 進行的消息傳遞。 那么 objc_msgSend 這個函數做了什么呢?
objc_msgSend 負責 Objective-C Runtime 中消息機制的核心 - 叫做 消息分發 。
在了解 消息分發 之前,咱們還需要了解 runtime 中關于類的定義, <objc/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 }
</div>
大家可能會想了, 怎么又冒出這么一個 struct 結構呢? 我們不是已經在 Objective-C 用諸如這樣的代碼定義過我們的類了么:
@interface Person : NSObject - (void) sayHello; @end @implementation Person - (void)sayHello { NSLog(@"hello"); } @end
</div>
簡單來說呢,咱們用 Objective-C 代碼定義了類,比如我們這里定義的 Person 類,都是應用層的邏輯。它服務于我們開發的 App 的需求 - 比如,有這樣一個 Person 類,它可以通過 sayHello 方法向命令行輸出內容。
但在系統層級,我們定義的 Person 類是如何在內存中表示的?
對 Person 類中方法的調用是如何實現的呢?
這些系統層級的邏輯就要靠 Objective-C Runtime 為我們完成了。
比如 Person 類中所定義的屬性和方法,在內存中的存儲方式就是通過 Runtime 的 struct objc_class 結構來定義了。每一個類的實例在 Runtime 中都會用 objc_class 這個結構來表示,這也就意味著所有的對象也都包含了 objc_class 結構中所定義的屬性。
那么我們繼續, objc_class 結構包含了很多屬性, 其中一個叫做 isa , 它的類型是 Class 。 那么繼續追根溯源,在 <objc/objc.h> 中找到了 Class 類型的定義:
typedef struct objc_class *Class;
</div>
實際上 isa 的類型,就是 objc_class 這個結構的類型。 isa 所指向的結構正是這個類的元信息(屬性,方法的定義)。
現在,我們對 Runtime 的基礎結構有了一個了解。 再回到 objc_msgSend 函數中, 它的第一個參數就是我們要發送消息的實例。首先, objc_msgSend 函數會檢測這個實例的 isa 屬性,找到 isa 中定義的:
struct objc_method_list **methodLists
</div>
methodLists 屬性表示當前實例的方法列表,它是一個 objc_method_list 類型的結構:
struct objc_method_list { struct objc_method_list *obsolete OBJC2_UNAVAILABLE; int method_count OBJC2_UNAVAILABLE; #ifdef __LP64__ int space OBJC2_UNAVAILABLE; #endif /* variable length structure */ struct objc_method method_list[1] OBJC2_UNAVAILABLE; }
</div>
這個結構的定義可能大家會有些地方不太明白,比如 space 屬性是干什么的。咱們可以暫時拋開這些問題,只關心和 消息分發 相關的屬性 - method_count 屬性表示當前這個實例中方法的個數, method_list 結構表示當前實例上面所有方法的列表:
struct objc_method method_list[1]
</div>
它的每一個元素又是一個 objc_method 類型的結構:
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
</div>
這個結構有三個屬性,method_name 是 SEL 類型。 就是 @selector(sayHello) 這樣的表達式所表示的類型。 也就是我們所說的 Selector 。 看到這里是不是有些豁然開朗的感覺呢?
Selector ,它其實是 Runtime 的一個數據結構。它代表一個方法的唯一 標識 。
然后再看第二個屬性 method_types , 這個屬性用一個字符串表示方法返回值類型以及每個參數的類型。 它使用 @encode 規則對類型信息進行編碼,咱們現在只要了解到這就好,細節先不深究。
最后一個參數是 IMP 類型,它表示這個 Selector 對應的函數的地址。 對,是函數沒錯。 Objective-C 中定義的所有類的方法在底層實現上就是一個函數。
消息分發流程
咱們對 Objective-C Runtime 的消息的底層數據結構已經有了足夠的了解。 接下來就探討一下消息分發機制吧。
說了這么多之后大家還記得是怎么調用 objc_msgSend 函數的么? 咱們再來回顧一下:
objc_msgSend(person,@selector(sayHello))
</div>
第一個參數是要發送消息的實例,也就是 person 對象。 objc_msgSend 會先查詢它的 methodLists 方法列表,使用第二個參數 sayHello 逐個和 person 的 methodLists 中的每一個方法信息的 SEL 進行對比,如果找到對應的方法,就調用它所對應的函數,也就是 IMP,然后調用這個函數。
消息分發的基本流程用一張圖來描繪:
這張流程圖最后一步,我們看到這樣調用 sayHello 函數:
sayHello(person,@selector(sayHello));
</div>
是不是覺得有點奇怪? sayHello 方法我們明明是這樣定義的:
- (void) sayHello;
</div>
它不接受任何參數,而我們流程圖中的 sayHello 卻傳入了兩個參數。這就引出了 runtime 的另外一個機制,我們繼續討論。
方法實現
Objective-C 中所有的方法調用,其實都會隱式的傳遞進來兩個參數。第一個參數我們比較熟悉了, 就是 self 。 只不過我們習以為常的把 self 當成一個關鍵字,其實它是一個傳遞進來的參數。
第二個參數叫做 _cmd 用于表示當前函數所對應的 Selector。 這個參數很少會用到,咱們不進一步展開。
這就解釋了我們剛才的問題, Objective-C 中即便這個方法聲明為不接受任何參數,但在實際調用它的時候,也會至少將這兩個隱含的參數傳遞進來。 這個兩個參數的傳遞過程,我們在應用層開發的時候是完全不用管的,這些工作都由 Objective-C Runtime 替我們完成了。
更進一步的說,我們為 Person 定義的 sayHello 方法,在 runtime 中實際上就是一個函數而已。 它的簽名如下:
void sayHello(Person person, SEL _cmd) { ... }
</div>
有了這個概念后,我們就更加理解 objc_msgSend 消息分發的過程了。 Objective-C Runtime 用 objc_msgSend(person,@selector(sayHello)) 這樣的方式將 sayHello 消息發送給 person 實例。 objc_msgSend 函數找到 @selector(sayHello) 在 Person 類中所對應的函數,然后調用這個函數,并傳入兩個隱含的參數。
從這個流程不難看出, Selector 實際上是函數的一個標識,它 不是函數 。
runtime 通過 SEL 類型的 Selector 標識,在 Person 類的方法列表中找到和這個 Selector 相同的條目, 然后執行這個條目 IMP 屬性所指向的函數地址,并傳入 self 和 _cmd 兩個隱含的參數。
回想一下前面提到的 objc_method 結構的定義:
struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE; }
</div>
這就更加明確了, SEL 類型的 method_name 屬性僅僅作為一個標識。 而 method_imp 才是真正要執行的函數地址。
消息緩存
經過了前面長篇大論的分析,了解了 Objective-C Runtime 的消息分發的整體流程,這樣一個簡單的方法調用:
[person sayHello];
</div>
實際上在它的背后,對應著一系列復雜的機制。實際上在 Objective-C 中調用一個方法需要兩個過程,首先通過 消息分發 找到對應的 函數
注意這里是函數,在 runtime 中只有函數(Function)
然后再調用這個函數,并傳遞相應的參數。
實際上 消息分發 的過程是比較消耗性能的,需要進行一系列的查表操作。 所以 Objective-C Runtime 對消息的分發建立了緩存機制。 這點我們可以回顧一下 objc_class 結構的定義,是否還記得它也定義了一個 cache 屬性:
struct objc_class { ... struct objc_cache *cache OBJC2_UNAVAILABLE; ... }
</div>
它的類型是 objc_cache , 繼續找到這個結構的定義:
struct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method buckets[1] OBJC2_UNAVAILABLE; };
</div>
實際上 objc_cache 維護了一個哈希表,使用 Selector 作為鍵,存儲了緩存的函數列表。
了解了這些, 消息分發 的時候,首先會在 cache 里面進行匹配,如果我們發送的消息所對應的函數在 cache 中能夠找到,就直接執行這個函數了。 如果 cache 中沒有,才會真的去查找 methodLists 列表,并且成功匹配一次后,就將它放入緩存中,以后再調用這個方法就不會重新的進行查表操作了。
消息分發的傳遞機制
我們在發送一個消息時,如果當前實例的方法列表中沒找到對應的函數怎么辦呢,比如我們發送這樣一個消息:
[person description]
</div>
Person 類中確實沒有定義 description 方法。對于這樣的情況, Objective-C Runtime 會繼續查找它的父類,使用定義在 objc_class 結構中的 super_class 屬性。
struct objc_class { ... Class super_class OBJC2_UNAVAILABLE; ... }
</div>
直到查找到最頂層的根類。 比如在我們上面的例子中, Person 類確實沒有定義 description 方法,但它的父類 NSObject 是有這個方法的,所以就會執行定義在 NSObject 的方法列表中的這個函數。 這個流程也很好的解釋了 Objective-C 中方法重載的機制。 Runtime 會現在子類的 methodLists 中查找,如果子類有相應的重載,就會優先使用子類的實現。
如果遍歷完整個類層級依然找不到對應的方法實現,默認情況下就會拋出類似這樣的異常:
unrecognized selector sent to instance 0x7fe672452350
</div>
相信大家在開發中會不少次遇到這種情況吧。 不過,這只是默認行為,其實這個異常是可以不拋出的。 我們完全可以在發送了一個并沒有實現的消息的時候不讓程序崩潰。 這就涉及到 runtime 的 消息轉發 機制了。 這次就先不討論啦,改天幫大家總結一篇單獨的文章。
直接發送消息
介紹了這么多,相信大家通過這篇文章的內容,對 Runtime 的消息機制已經有了比較多的了解。 所有的方法調用,在 Runtime 中都會通過 objc_msgSend 來發送。 如果這么說來其實是可以直接調用 objc_msgSend 來發送消息的。 可以驗證一下:
#import <objc/message.h> 。。。 ((void (*)(id, SEL)) objc_msgSend)(person,@selector(sayHello));
</div>
這段代碼是可以在真實環境中編譯并運行的。 運行程序后,控制臺上會有這樣的輸出:
hello
</div>
這說明我們通過直接調用 objc_msgSend 的方式完成了方法調用。 解釋一下剛才的代碼, 首先需要引入 Runtime 的頭文件 #import <objc/message.h> 。
message.h 中定義的 objc_msgSend 函數,并沒有明確參數列表和返回類型, 所以我們需要強制轉換一下,否則我們會遇到編譯錯誤:
((void (*)(id, SEL)) objc_msgSend)
</div>
然后調用這個轉換后的函數,并傳入相應的參數:
((void (*)(id, SEL)) objc_msgSend)(person,@selector(sayHello));
</div>
這樣,編譯順利通過,驗證成功~
直接調用函數
雖然我們可以調用 objc_msgSend 來發送消息,但它還是要經過 消息分發 的過程。 當然,如果你需要的話,你是可以完全繞過消息分發機制直接調用函數的。 NSObject 中定義了一個 methodForSelector 方法,可以得到 Selector 所對應的函數:
void (*sayHello)(id, SEL); sayHello = (void (*)(id,SEL))[person methodForSelector:@selector(sayHello)]; sayHello(person, @selector(sayHello));
</div>
我們通過 methodForSelector 得到了 sayHello 函數的地址引用,這樣我們就可以直接調用 sayHello 函數了,這樣就會繞過 Runtime 的 消息分發 機制。
當然,這兩個小例子主要是幫助大家了解 Objective-C Runtime 的機制。我們在實際代碼中,如非必要,還是不建議繞過默認的 消息分發 機制。
總結
Objective-C Runtime 可以說是隱藏在幕后的精英。 我們所寫的幾乎每一行代碼,都和 Objective-C Runtime 形影不離。 比如,傳遞給 objc_msgSend 的第一個參數是 nil 的話, objc_msgSend 就會判斷并進行短路操作。 這也解釋了為什么在值為 nil 的引用上面調用方法不會導致程序崩潰。 Objective-C Runtime 中所涉及的內容,以及它定義的函數,很少會再我們的日常開發中用到。但了解 Objective-C Runtime 背后的機制,會讓你在寫代碼的時候更加有把握,有一種內功暴增的感覺。