iOS Runtime 初識與應用

夏風_Me發表於2019-02-20

什麼是 Runtime

什麼是執行時呢?從字面意思來看,就是一個程式在其執行的過程中所做的一些事情。而蘋果在 object—C 中提供了一套純 c 語言的 api,這套 api 即為 runtime。

在 iOS 開發的過程中,正式因為runtime 的特性,讓 object-C 具有了吸引人的魅力。使得我們可以真正做到玩語言,做出高逼格的花樣,快樂就完了~

要了解執行時,我們得先了解 object-C 的訊息機制,可以看下面的流程

1、編譯器會先將程式碼 [obj doSomeThing] 轉化為 objc_msgSend(obj, @selector (doSomeThing)) 函式去執行。

2、在 objc_msgSend() 函式中,首先通過 obj 的 isa 指標找到 obj 對應的 class。

3、在 class 中會先去 cache 中 通過 SEL 查詢對應函式 doSomeThing(cache 中method 列表是以 SEL 為 key 通過 hash 表來儲存的,這樣能提高函式查詢速度),若 cache 中未找到。再去 class 中的訊息列表 methodList 中查詢,若 methodList 中未找到,則取 superClass 中查詢。若能找到,則將 doSomeThing 加入到 cache中,以方便下次查詢,並通過 method 中的函式指標跳轉到對應的函式中去執行。

看完上面的流程,可能會迷惑裡面提到的一下字端是什麼意思,那我們來看一下一個 OC 類中都包含了什麼?

iOS Runtime 初識與應用

可以先看類結構體 struct objc_class。雖然該結構體中有許多變數,但是從變數名中我們可以大概理解其含義,結構體裡包含了 指像父類的指標、類名、版本等等資訊,裡面有些變數會在下面的應用中進行用到。

下面就讓我們來看看 runtime 的具體應用吧~

Runtime 的應用

Runtime 的功能非常強大,這也是魅力所在,它能夠在程式執行的時候,獲取一個類中的所有資訊,並且能夠根據開發者意願去修改。腦洞多大,變化就能多大~

接下來,本文主要就下面幾點進行講解:

(1)使用 class_addMethod 函式在執行時對函式進行動態增加新函式

(2)訊息轉發

(3)使用 class_copyPropertyList 及 property_getName 獲取類的屬性列表及每個屬性的名稱

(4) 使用 class_copyMethodList 獲取類的所有方法列表

(5) Method Swizzling

動態新增方法 - class_addMethod

首先來看一下 API 文件解釋

iOS Runtime 初識與應用

先看下函式中的各個引數的含義:

Class _Nullable cls:傳入的是一個類,也就是你想要在哪個類裡進行新增方法

SEL _Nollnull name:想要新增的方法名字

IMP _Nonnull imp:imp 是 Implement 縮寫,表示指向方法的實現地址

const char * _Nonnull types:對於引數與返回值的描述。舉個例子:"v@:",表示的是返回值為 void 並且沒有引數。

下面看一下具體的呼叫:

iOS Runtime 初識與應用

這裡要給 HJXPerson 類動態新增一個 sayHello 的方法。首先要拿到新增的方法 sayHello 的 IMP 指標,然後呼叫相應的介面去新增方法。

(注:當要執行動態新增方法的時候,需要用 performSelector 來進行呼叫,因為 performSelector 是執行時系統負責去找方法的,在編譯時候不做任何校驗;如果直接呼叫編譯是會自動校驗)

訊息轉發

文章開頭介紹了 Objective-C 中呼叫一個方法的流程,但如果最後都沒有找到對應方法,我們的程式都會 crash 並丟擲資訊沒有找到對應的方法。其實在 crash 之前還會進行一套流程,那就是訊息轉發。轉發流程圖如下:

iOS Runtime 初識與應用

下面將分別結合程式碼事例進行介紹這幾種方法的處理:

  • resolveInstanceMethod

此為訊息轉發的先導,也叫動態決議機制。見下面例子:

iOS Runtime 初識與應用

在 HJXPerson 類中並沒有宣告 eat 這個函式,所以在例項呼叫方法的時候會進入到這個回撥之中。

其實,objective-c 的方法就是至少帶有兩個引數(self 和 cmd)的普通的 C 函式。因此在程式碼中提供這樣一個 C 函式 dynamicMethodIMPEat,讓它來充當物件方法 eat 這個 selector 的動態實現。

因為 eat 是被物件所呼叫,所以它被認為是一個物件方法,因而應該在 resolveInstanceMethod 方法中為其提供實現。

  • forwardingTargetForSelector

這個方法只能讓我們把訊息轉發到另一個能處理這個訊息的物件,但是無法處理訊息的內容,比如引數和返回值。例子如下:

iOS Runtime 初識與應用

該類為 HJXCat 類,裡面宣告瞭 “useTool” 這個方法但並沒有實現,所以在未新增 resolveInstanceMethod 處理的時候,會進入到 forwardingTargetForSelector 方法中,然後可以根據傳入的 aSelector,來轉交給其他類去處理相應的方法。

(注:返回的為想要把方法交給的類的一個例項物件)

  • forwardInvocation

先看下工程程式碼

iOS Runtime 初識與應用

forwardInvocation 的整體流程和 forwardingTargetForSelector 基本上是差不多的。通過 aSelector 來進行判斷是否要進行轉發,然後進行手動簽名。然後在 anInvocation 中可以獲取到函式傳裡過程中所有資訊。

forwardingTargetForSelector 和 forwardInvocation 區別

快速轉發:forwardingTargetForSelector 僅支援一個物件的返回,也就是說訊息只能被轉發給一個物件、無法處理訊息的內容,比如引數和返回值。

普通轉發:forwardInvocation 可以將訊息同時轉發給任意多個物件。

訊息轉發總結

  • 首先看是否為該 selector 提供了動態方法決議機制,如果提供了則轉到 2;如果沒有提供則轉到 3;

  • 如果動態方法決議真正為該 selector 提供了實現,那麼就呼叫該實現,完成訊息傳送流程,訊息轉發就不會進行了;如果沒有提供,則轉到 3;

  • 其次看是否為該 selector 提供了訊息轉發機制,如果提供了訊息了則進行訊息轉發,此時,無論訊息轉發是怎樣實現的,程式均不會 crash。(因為訊息呼叫的控制權完全交給訊息轉發機制處理,即使訊息轉發並沒有做任何事情,執行也不會有錯誤,編譯器更不會有錯誤提示。);如果沒提供訊息轉發機制,則轉到 4;

  • 執行報錯:無法識別的 selector,程式 crash

property_getName 及 class_copyMethodList

這兩個函式總從字面意思上,可以看出分別是獲取一個類中的所有屬性名稱及方法名。這兩個方法可以算是基礎,讓你可以知道一個類內的所有屬性及方法,以便你接下來可以隨心所欲的對於其修改,修改哪裡。

下面為呼叫方法來獲取 HJXCat 中的屬性及方法:

iOS Runtime 初識與應用

iOS Runtime 初識與應用

(上面的各個屬性都是宣告在 .m 檔案中的)

iOS 黑魔法 - Method Swizzling

首先讓我們來看下方法交換的原理:

  • 在 Objective-C 中呼叫一個方法,其實是向一個物件傳送訊息,查詢訊息的唯一依據是 selector 的名字。利用 Objective-C 的動態特性,可以實現在執行時偷換 selector 對應的方法實現,達到給方法掛鉤的目的。

  • 每個類都有一個方法列表,存放著 selector 的名字和方法實現的對映關係。IMP 有點類似函式指標,指向具體的 Method 實現。

歸根結底,都是偷換了 selector 的 IMP

跟訊息轉發相比,Method Swizzling 的做法更為隱蔽,甚至有些冒險,也增大了debug的難度。

下面我們來看一下程式碼部分:

iOS Runtime 初識與應用

就這麼簡簡單單幾行,就能夠實現方法的交換。原有的 eat 方法在呼叫 hasEatenFull 方法後,就與 play 方法進行了交換,再次執行 eat 方法,其實就是呼叫了 play 方法。

雖然 Swizzling 可以任你喜歡的去弄,想要玩起來,anyway,都可以。但是開發工程中,我們需要先好好的去想一想,能不能利用良好的程式碼和架構設計來實現,或者是深入語言的特性來實現。好用,但是不能濫用。

後記

Runtime 在程式碼執行的時候,最大提供給我們操作性,可以幫助我們理解程式碼的執行,窺探我們看不到的程式碼,擴充套件更多更有趣的功能。

程式碼玩起來,快樂就完啦~用好,不濫用~

相關文章