iOS Runtime 原理

Jerry4me發表於2016-09-05

PS 一些關於runtime的小demo在我的下一篇文章[iOS-Runtime-實踐篇](http://www.jianshu.com/p/d6a2656fc2cb)中

我們都知道Objective-C是一門動態語言,動態之處體現在它將許多靜態語言編譯連結時要做的事通通放到執行時去做,這大大增加了我們程式設計的靈活性。

毫不過分地說,Runtime就是OC的靈魂。接下來我就要撥開OC最外層的外衣,帶大家看看OC的真面目(C/C++)。

類和物件

為了更好地說明類在底層的表現形式是怎樣, 我們將上面程式碼利用clang -rewrite-objc Person.m指令將其用C/C++重寫, 一窺究竟.

把不必要的刪除, 整理後為下面

原來(顯然), 我們的類其實就是一個結構體!!! 類跟我們的物件一樣, 都有一個isa指標, 所以類其實也是物件的一種.

isa指標

isa指標非常重要, 物件需要通過isa指標找到它的類, 類需要通過isa找到它的元類. 這在呼叫例項方法和類方法的時候起到重要的作用.

 

iOS Runtime 原理

例項物件在呼叫方法時, 首先通過isa指標找到它所屬的類, 然後在類的快取(cache)裡找該方法的IMP, 如果沒有, 則去類的方法列表中查詢, 然後找到則呼叫該方法, 找不到則報錯.

類物件呼叫方法則如出一轍, 通過isa指標找到元類, 然後就跟上述一致了. 這裡涉及的傳送訊息機制下面會詳細講..

下面展示一些執行時動態獲取物件和類的屬性的C語言方法

類和類名 :

ivar和屬性 :

方法 :

這裡說個注意點 : addIvar並不能為一個已經存在的類新增成員變數, 只能為那些執行時動態新增的類, 並且只能在objc_allocateClassPairobjc_registerClassPair這兩個方法之間才能新增Ivar.

訊息傳送和轉發機制

在OC中, 如果向某物件傳送訊息, 那就會使用動態繫結機制來決定需要呼叫的方法. OC的方法在底層都是普通的C語言函式, 所以物件收到訊息後究竟要呼叫什麼函式完全由執行時決定, 甚至可以在執行時改變執行的方法.

[person read:book];
會被編譯成
objc_msgSend(person, @selector(read:), book);

objc_msgSend的具體流程如下

1. 通過isa指標找到所屬類
2. 查詢類的cache列表, 如果沒有則下一步
3. 查詢類的”方法列表”
4. 如果能找到與選擇子名稱相符的方法, 就跳至其實現程式碼
5. 找不到, 就沿著繼承體系繼續向上查詢
6. 如果能找到與選擇子名稱相符的方法, 就跳至其實現程式碼
7. 找不到, 執行”訊息轉發”.

iOS Runtime 原理

訊息轉發

上面我們提到, 如果到最後都找不到, 就會來到訊息轉發

  • 動態方法解析 : 先問接收者所屬的類, 你看能不能動態新增個方法來處理這個”未知的選擇子”? 如果能, 則訊息轉發結束.
  • 備胎(後備接收者) : 請接收者看看有沒有其他物件能處理這條訊息? 如果有, 則把訊息轉給那個物件, 訊息轉發結束.
  • 訊息簽名 : 這裡會要求你返回一個訊息簽名, 如果返回nil, 則訊息轉發結束.
  • 完整的訊息轉發 : 備胎都搞不定了, 那就只能把該訊息相關的所有細節都封裝到一個NSInvocation物件, 再問接收者一次, 快想辦法把這個搞定了. 到了這個地步如果還無法處理, 訊息轉發機制也無能為力了.
動態方法解析 :

物件在收到無法解讀的訊息後, 首先呼叫其所屬類的這個類方法 :

假如尚未實現的方法不是例項方法而是類方法, 則會呼叫另一個方法resolveClassMethod:

備胎 :

動態方法解析失敗, 則呼叫這個方法

通過備胎這個方法, 可以用”組合”來模擬出”多重繼承”.

訊息簽名 :

備胎搞不定, 這個方法就準備要被包裝成一個NSInvocation物件, 在這裡要先返回一個方法簽名

完整的訊息轉發 :

給接收者最後一次機會把這個方法處理了, 搞不定就直接程式崩潰!

在這裡能做的比較現實的事就是 : 在觸發訊息前, 先以某種方式改變訊息內容, 比如追加另外一個引數, 或是改變選擇子等等. 實現此方法時, 如果發現某呼叫操作不應該由本類處理, 可以呼叫超類的同名方法. 則繼承體系中的每個類都有機會處理該請求, 直到NSObject. 如果NSObject搞不定, 則還會呼叫doesNotRecognizeSelector:來丟擲異常, 此時你就會在控制檯看到那熟悉的unrecognized selector sent to instance..

iOS Runtime 原理

上面這4個方法均是模板方法,開發者可以override,由runtime來呼叫。最常見的實現訊息轉發,就是重寫方法3和4,忽略這個訊息或者代理給其他物件.

Method Swizzling

被稱為黑魔法的一個方法, 可以把兩個方法的實現互換.
如上文所述, 類的方法列表會把選擇子的名稱對映到相關的方法實現上, 使得”動態訊息派發系統”能夠據此找到應該呼叫的方法. 這些方法均以函式指標的形式來表示, 這種指標叫做IMP,

 

iOS Runtime 原理

OC執行時系統提供了幾個方法能夠用來操作這張表, 動態增加, 刪除, 改變選擇子對應的方法實現, 甚至交換兩個選擇子所對映到的指標. 如,

iOS Runtime 原理

經過一些操作後的NSString選擇子對映表

如何交換兩個已經寫好的方法實現?

通過Method Swizzling可以為一些完全不知道其具體實現的黑盒方法增加日誌記錄功能, 利於我們除錯程式. 並且我們可以將某些系統類的具體實現換成我們自己寫的方法, 以達到某些目的. (例如, 修改主題, 修改字型等等)

KVO原理

KVO的實現也依賴Runtime. Apple文件曾簡單提到過KVO的實現原理 :

Automatic key-value observing is implemented using a technique called isa-swizzling… When an observer is registered for an attribute of an object the isa pointer of the observed object is modified, pointing to an intermediate class rather than at the true class …

Apple的文件提得不多, 但是大神Mike Ash在很早很早以前就已經做過研究, 摘下了KVO神祕的面紗了, 有興趣的可以去查下, 這裡不多深究, 只是簡單闡述下原理.

原來當你對一個物件進行觀察時, 系統會自動新建一個類繼承自原類, 然後重寫被觀察屬性的setter方法. 然後重寫的setter方法會負責在呼叫原setter方法前後通知觀察者. 然後把原物件的isa指標指向這個新類, 我們知道, 物件是通過isa指標去查詢自己是屬於哪個類, 並去所在類的方法列表中查詢方法的, 所以這個時候這個物件就自然地變成了新類的例項物件.

不僅如此, Apple還重寫了原類的- class方法, 檢視欺騙我們, 這個類沒有變, 還是原來的那個類. 只要我們懂得Runtime的原理, 這一切都只是掩耳盜鈴罷了.


後記

這只是我的Runtime文章的第一篇, 之後還會有Runtime實踐篇以及利用Runtime解決實際問題的幾個demo, 感興趣的話還請大家關注關注^_^

打賞支援我寫出更多好文章,謝謝!

打賞作者

打賞支援我寫出更多好文章,謝謝!

任選一種支付方式

iOS Runtime 原理 iOS Runtime 原理

相關文章