iOS 常見知識點(一):Runtime

發表於2016-08-19

Runtime

Runtime 是一個執行時庫,主要使用 C 和彙編寫的庫,為 C 新增了物件導向的能力並創造了 Objective-C,並且擁有訊息分發,訊息轉發等功能。

也就是 Runtime 涉及三個點,物件導向,訊息分發,訊息轉發。

物件導向:

Objective-C 的物件是基於 Runtime 建立的結構體。先從程式碼層面分析一下。

alloc 方法會為物件分配一塊記憶體空間,空間的大小為 isa_t(8 位元組)的大小加上所有成員變數所需的空間,再進行一次記憶體對齊。分配完空間後會初始化 isa_t ,而 isa_t 是一個 union 型別的結構體(或者稱之為聯合體),它的結構是在 Runtime 裡被定義的。

從 isa_t 的結構可以看出,isa_t 可以儲存 struct,uintptr_t 或者 Class 型別

init 方法就直接返回了初始化好的物件,class 指標指向這個初始化好的物件。

也就是在 Runtime 的協助之下,一個物件完成了建立。

你可能想知道,這個物件只存放了一個 isa_t 結構體和成員變數,物件的方法在哪裡?

在編譯的時候,類在記憶體中的位置就已經確定,而在 main 方法之前,Runtime 將可執行檔案中和動態庫所有的符號(Class,Protocol,Selector,IMP,…)載入到記憶體中,由 Runtime 管理,這裡也包括了也是一個物件的類。

類物件裡儲存著一個 isa_t 的結構體,super_class 指標,cache_t 結構體,class_data_bits_t 指標。

class_data_bits_t 指向類物件的資料區域,資料區域存放著這個類的例項方法連結串列。而類方法存在元類物件的資料區域。也就是有物件,類物件,元類物件三個概念,物件是在執行時動態建立的,可以有無數個,類物件和元類物件在 main 方法之前建立的,分別只會有一個。

訊息分發

在 Objective-C 中的“方法呼叫”其實應該叫做訊息傳遞,[object message] 會被編譯器翻譯為 objc_msgSend(object, @selector(message)),這是一個 C 方法,首先看它的兩個引數,第一個是 object ,既方法呼叫者,第二個引數稱為選擇子 SEL,Objective-C 為我們維護了一個巨大的選擇子表,在使用 @selector() 時會從這個選擇子表中根據選擇子的名字查詢對應的 SEL。如果沒有找到,則會生成一個 SEL 並新增到表中,在編譯期間會掃描全部的標頭檔案和實現檔案將其中的方法以及使用 @selector() 生成的選擇子加入到選擇子表中。

通過第一個引數 object,可以找到 object 物件的 isa_t 結構體,從上文中能看 isa_t 結構體的結構,在 isa_t 結構體中,shiftcls 存放的是一個 33 位的地址,用於指向 object 物件的類物件,而類物件裡有一個 cache_t 結構體,來看一下 cache_t 的具體程式碼

_mask:分配用來快取 bucket 的總數。
_occupied:表明目前實際佔用的快取 bucket 的個數。
_buckets:一個雜湊表,用來方法快取,bucket_t 型別,包含 key 以及方法實現 IMP。

objc_msgSend() 方法會先從快取表裡,查詢是否有該 SEL 對應的 IMP,有的話算命中快取,直接通過函式指標 IMP ,找到方法的具體實現函式,執行。

當然快取表裡可能並不會命中,則此時會根據類物件的 class_data_bits_t 指標找到資料區域,資料區域裡用連結串列存放著類的例項方法,例項方法也是一個結構體,其結構為:

編譯器將每個方法的返回值和引數型別編碼為一個字串,types 指向的就是這樣一個字串,objc_msgSend() 會在類物件的方法連結串列裡按連結串列順序去匹配 SEL,匹配成功則停止,並將此方法加入到類物件的 _buckets 裡快取起來。如果沒找到則會通過類物件的 superclass 指標找到其父類,去父類的方法列表裡尋找(也會從父類的方法快取列表開始)。

如果繼續沒有找到會一直向父類尋找,直到遇見 NSObject,NSObject 的 superclass 指向 nil。也就意味著尋找結束,並沒有找到實現方法。(如果這個過程找到了,也同樣會在 object 的類物件的 _buckets 裡快取起來)。

選擇子在當前類和父類中都沒有找到實現,就進入了方法決議(method resolve),首先判斷當前 object 的類物件是否實現了 resolveInstanceMethod: 方法,如果實現的話,會呼叫 resolveInstanceMethod:方法,這個時候我們可以在 resolveInstanceMethod:方法裡動態的新增該 SEL 對應的方法(也可以去做點別的,比如寫入日誌)。之後會重新執行查詢方法實現的流程,如果依舊沒找到方法,或者沒有實現 resolveInstanceMethod: 方法,Runtime 還有另一套機制,訊息轉發。

訊息轉發

訊息轉發分為以下幾步:

1.呼叫 forwardingTargetForSelector: 方法,嘗試找到一個能響應該訊息的物件。如果獲取到,則直接轉發給它。如果返回了 nil,繼續下面的動作。

2.呼叫 methodSignatureForSelector: 方法,嘗試獲得一個方法簽名。如果獲取不到,則直接呼叫 doesNotRecognizeSelector 丟擲異常。

3.呼叫 forwardInvocation: 方法,將第 3 步獲取到的方法簽名包裝成 Invocation 傳入,如何處理就在這裡面了。

以上三個方法都可以通過在 object 的類物件裡實現, forwardingTargetForSelector: 可以通過對引數 SEL 的判斷,返回一個可以響應該訊息的物件。這樣則會重新從該物件開始執行查詢方法實現的流程,找到了也同樣會在 object 的類物件的 _buckets 裡快取起來。而 2,3 方法則一般是配套使用,實現 methodSignatureForSelector: 方法根據引數 SEL ,做相應處理,返回 NSMethodSignature (方法簽名) 物件,NSMethodSignature 物件會被包裝成 NSInvocation 物件,forwardInvocation: 方法裡就可以對 NSInvocation 進行處理了。

上面是講的是例項方法,類方法沒什麼區別,類方法儲存在元類物件的資料區域裡,通過類物件的 isa_t 找到元類物件,執行查詢方法實現的流程,元類物件的 superclass 最終也會指向 NSObject。沒找到的話,也會有方法決議以及訊息轉發。

runtime 可以做什麼:

實現多繼承:從 forwardingTargetForSelector: 方法就能知道,一個類可以做到繼承多個類的效果,只需要在這一步將訊息轉發給正確的類物件就可以模擬多繼承的效果。

交換兩個方法的實現

關聯物件

通過下面兩個方法,可以給 category 實現新增成員變數的效果。

動態新增類和方法:

objc_allocateClassPair 函式與 objc_registerClassPair 函式可以完成一個新類的新增,class_addMethod 給類新增方法,class_addIvar 新增成員變數,objc_registerClassPair 來註冊類,其中成員變數的新增必須在類註冊之前,類註冊後就可以建立該類的物件了,而再新增成員變數就會破壞建立的物件的記憶體結構。

將 json 轉換為 model

用到了 Runtime 獲取某一個類的全部屬性的名字,以及 Runtime 獲取屬性的型別。

相關文章