iOS開發之 Method Swizzling 深入淺出

DotzuX發表於2018-04-23

<p align=”center”>
<img src =”https://raw.githubusercontent…;/>
</p>

iOS開發之 Method Swizzling 深入淺出

只要善用Google,網上有很多關於Method Swizzling的Demo,在這裡我就不打算貼程式碼了,主要介紹下概念,原理,注意事項等等。

開發需求

如果產品經理突然說:”在所有頁面新增統計功能,也就是使用者進入這個頁面就統計一次”。我們會想到下面的一些方法:

  • 手動新增

直接簡單粗暴的在每個控制器中加入統計,複製、貼上、複製、貼上…
上面這種方法太Low了,消耗時間而且以後非常難以維護,會讓後面的開發人員罵死的。

  • 繼承

我們可以使用繼承的方式來解決這個問題。建立一個基類,在這個基類中新增統計方法,其他類都繼承自這個基類。

然而,這種方式修改還是很大,而且定製性很差。以後有新人加入之後,都要囑咐其繼承自這個基類,所以這種方式並不可取。

  • Category

我們可以為UIViewController建一個Category,然後在所有控制器中引入這個Category。當然我們也可以新增一個PCH檔案,然後將這個Category新增到PCH檔案中。

  • Method Swizzling

我們可以使用蘋果的“黑魔法”Method SwizzlingMethod Swizzling本質上就是對IMPSEL進行交換。

先了解幾個概念

Selectors, Methods, & Implementations

Objective-C 的執行時中,selectors, methods, implementations 指代了不同概念,然而我們通常會說在訊息傳送過程中,這三個概念是可以相互轉換的。 下面是蘋果 Objective-C Runtime Reference中的描述:

  • Selector(typedef struct objc_selector *SEL):在執行時 Selectors 用來代表一個方法的名字。Selector 是一個在執行時被註冊(或對映)的C型別字串。Selector由編譯器產生並且在當類被載入進記憶體時由執行時自動進行名字和實現的對映。
  • Method(typedef struct objc_method *Method):方法是一個不透明的用來代表一個方法的定義的型別。
  • Implementation(typedef id (*IMP)(id, SEL,...)):這個資料型別指向一個方法的實現的最開始的地方。該方法為當前CPU架構使用標準的C方法呼叫來實現。該方法的第一個引數指向呼叫方法的自身(即記憶體中類的例項物件,若是呼叫類方法,該指標則是指向元類物件(metaclass)。第二個引數是這個方法的名字selector,該方法的真正引數緊隨其後。

理解 selector, method, implementation 這三個概念之間關係的最好方式是:在執行時,類(Class)維護了一個訊息分發列表來解決訊息的正確傳送。每一個訊息列表的入口是一個方法(Method),這個方法對映了一對鍵值對,其中鍵值是這個方法的名字 selector(SEL),值是指向這個方法實現的函式指標 implementation(IMP)Method swizzling 修改了類的訊息分發列表使得已經存在的 selector 對映了另一個實現 implementation,同時重新命名了原生方法的實現為一個新的 selector

Method Swizzling原理

Method Swizzing是發生在執行時的,主要用於在執行時將兩個Method進行交換,我們可以將Method Swizzling程式碼寫到任何地方,但是隻有在這段Method Swilzzling程式碼執行完畢之後互換才起作用。

Method Swizzling 使用注意

類簇設計模式

在iOS中NSNumber、NSArray、NSDictionary等這些類都是類簇(Class Clusters),一個NSArray的實現可能由多個類組成。
所以如果想對NSArray進行Swizzling,必須獲取到其“真身”進行Swizzling,直接對NSArray進行操作是無效的。

下面列舉了NSArray和NSDictionary本類的類名,可以通過Runtime函式取出本類。

| 類名 | 真身 |
| ——– | —–: |
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |

注意要點

  • Swizzling應該總在+load中執行
  • Swizzling應該總是在dispatch_once中執行
  • Swizzling在+load中執行時,不要呼叫[super load]。如果多次呼叫了[super load],可能會出現“Swizzle無效”的假象,原理見下圖:

Swift 自定義類中使用 Method Swizzling

要在 Swift 自定義類中使用 Method Swizzling 有兩個必要條件:

  • 包含 Swizzle 方法的類需要繼承自 NSObject
  • 需要 Swizzle 的方法必須有動態屬性(dynamic attribute)

注:對於 Swift 的自定義類,因為預設並沒有使用 Objective-C 執行時,因此也沒有動態派發的方法列表,所以如果要 Swizzle 的是 Swift 型別的方法的話,是需要將原方法和替換方法都加上 dynamic 標記,以指明它們需要使用動態派發機制。當然類也要繼承自 NSObject。

再注:下面這個例子使用了 Objective-C 的動態派發,對於 NSObject 的子類(UIViewController)是可以直接使用的,並不是 Swift 中自定義的類,因此沒有加 dynamic 標記也是可以的。

Method Swizzling 中 Objective-C 與 Swift 的異同

區別 Objective-C Swift
Runtime 標頭檔案 #import <objc/runtime.h> 不需要
Swizzling 呼叫處 load 方法 initialize 方法

注:load 方法只在 Objective-C 裡有,而且不能在 Swift 裡過載,不管怎麼試都會報編譯錯誤。接下來執行 Swizzle 最好的地方就是 initialize 了,這是呼叫第一個方法前的地方。

因為 Swizzling 會改變全域性狀態,所以我們需要在執行時採取一些預防措施。GCD 的dispatch_once 可以保證操作的原子性,確保程式碼只被執行一次,不管有多少個執行緒。

Method Swizzling 實際應用

APM(應用效能管理)

網路監控的原理,應該就是hook NSURLConnection , NSURLSession 。崩潰收集的原理,應該就是hook NSException

國外資料 ?

可能需要梯子

小廣告 ?

GitHub開源了一款iOS除錯小工具,功能之一就是實現網路請求抓包(程式碼零入侵),原理也是使用了
Method Swizzling, 感興趣的童鞋可以進來看看, 也歡迎使用? http://DotzuX.com

相關文章