Objective-C 中的 Method Swizzling 是一項異常強大的技術,它可以允許我們動態地替換方法的實現,實現 Hook
功能,是一種比子類化更加靈活的“重寫”方法的方式。
Method Swizzling 的原理
Method Swizzling 是一把雙刃劍,使用得當可以讓我們非常輕鬆地實現複雜的功能,而如果一旦誤用,它也很可能會給我們的程式帶來毀滅性的傷害。但是我們大可不必驚慌,在瞭解了它的實現原理後,我們就可以“信手拈來”了。
我們先來了解下 Objective-C 中方法 Method
的資料結構:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
typedef struct method_t *Method; struct method_t { SEL name; const char *types; IMP imp; struct SortBySELAddress : public std::binary_function<const method_t&, const method_t&, bool> { bool operator() (const method_t& lhs, const method_t& rhs) { return lhs.name < rhs.name; } }; }; |
本質上,它就是 struct method_t
型別的指標,所以我們重點看下結構體 method_t
的定義。在結構體 method_t
中定義了三個成員變數和一個成員函式:
name
表示的是方法的名稱,用於唯一標識某個方法,比如@selector(viewWillAppear:)
;types
表示的是方法的返回值和引數型別(詳細資訊可以查閱蘋果官方文件中的 Type Encodings);imp
是一個函式指標,指向方法的實現;SortBySELAddress
顧名思義,是一個根據name
的地址對方法進行排序的函式。
由此,我們也可以發現 Objective-C 中的方法名是不包括引數型別的,也就是說下面兩個方法在 runtime 看來就是同一個方法:
1 2 |
- (void)viewWillAppear:(BOOL)animated; - (void)viewWillAppear:(NSString *)string; |
而下面兩個方法卻是可以共存的:
1 2 |
- (void)viewWillAppear:(BOOL)animated; + (void)viewWillAppear:(BOOL)animated; |
因為例項方法和類方法是分別儲存在類物件和元類物件中的,更多詳情可以檢視我前面的文章《Objective-C 物件模型》。
原則上,方法的名稱 name
和方法的實現 imp
是一一對應的,而 Method Swizzling 的原理就是動態地改變它們的對應關係,以達到替換方法實現的目的。
Method Swizzling 有什麼用
說了這麼多,到底 Method Swizzling 有什麼用呢?表猴急哈,我們接下來看個例子就明白了。用過友盟統計的同學應該知道,要實現頁面的統計功能,我們需要在每個頁面的 view controller
中新增如下程式碼:
1 2 3 4 5 6 7 8 9 |
- (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; [MobClick beginLogPageView:@"PageOne"]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [MobClick endLogPageView:@"PageOne"]; } |
要達到這個目的,我們有兩種比較常規的實現方式:
- 直接修改每個頁面的
view controller
程式碼,簡單粗暴; - 子類化
view controller
,並讓我們的view controller
都繼承這些子類。
第 1 種方式的缺點是不言而喻的,這樣做不僅會產生大量重複的程式碼,而且還很容易遺漏某些頁面,非常難維護;第 2 種方式稍微好一點,但是也同樣需要我們子類化 UIViewController
、UITableViewController
和 UITabBarController
等不同型別的 view controller
。
也許你跟我一樣陷入了思考,難道就沒有一種簡單優雅的解決方案嗎?答案是肯定的,Method Swizzling 就是解決此類問題的最佳方式。
Method Swizzling 的最佳實踐
下面我們就以替換 viewWillAppear
方法為例談談 Method Swizzling 的最佳實踐,話不多說,直接上程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@interface UIViewController (MRCUMAnalytics) @end @implementation UIViewController (MRCUMAnalytics) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(mrc_viewWillAppear:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } #pragma mark - Method Swizzling - (void)mrc_viewWillAppear:(BOOL)animated { [self mrc_viewWillAppear:animated]; [MobClick beginLogPageView:NSStringFromClass([self class])]; } @end |
解析:在上面的程式碼中有三個關鍵點需要引起我們的注意:
- 為什麼是在
+load
方法中實現 Method Swizzling 的邏輯,而不是其他的什麼方法,比如+initialize
等; - 為什麼 Method Swizzling 的邏輯需要用 dispatch_once 來進行排程;
- 為什麼需要呼叫
class_addMethod
方法,並且以它的結果為依據分別處理兩種不同的情況。
下面我們就一起來分析下這三個為什麼到底是為了什麼?
第 1 個為什麼:看過我前面文章《Objective-C +load vs +initialize》 的同學應該知道,+load
和 +initialize
是 Objective-C runtime 會自動呼叫的兩個類方法。但是它們被呼叫的時機卻是有差別的,+load
方法是在類被載入的時候呼叫的,而 +initialize
方法是在類或它的子類收到第一條訊息之前被呼叫的,這裡所指的訊息包括例項方法和類方法的呼叫。也就是說 +initialize
方法是以懶載入的方式被呼叫的,如果程式一直沒有給某個類或它的子類傳送訊息,那麼這個類的 +initialize
方法是永遠不會被呼叫的。此外 +load
方法還有一個非常重要的特性,那就是子類、父類和分類中的 +load
方法的實現是被區別對待的。換句話說在 Objective-C runtime 自動呼叫 +load
方法時,分類中的 +load
方法並不會對主類中的 +load
方法造成覆蓋。綜上所述,+load
方法是實現 Method Swizzling 邏輯的最佳“場所”。
第 2 個為什麼:我們上面提到,+load
方法在類載入的時候會被 runtime 自動呼叫一次,但是它並沒有限制程式設計師對 +load
方法的手動呼叫。什麼?你說不會有程式設計師這麼幹?那可說不定,我還見過手動呼叫 viewDidLoad
方法的程式設計師,就是介麼任性。而我們所能夠做的就是儘可能地保證程式能夠在各種情況下正常執行。
第 3 個為什麼:我們使用 Method Swizzling 的目的通常都是為了給程式增加功能,而不是完全地替換某個功能,所以我們一般都需要在自定義的實現中呼叫原始的實現。所以這裡就會有兩種情況需要我們分別進行處理:
第 1 種情況:主類本身有實現需要替換的方法,也就是 class_addMethod
方法返回 NO
。這種情況的處理比較簡單,直接交換兩個方法的實現就可以了:
1 2 3 4 5 6 7 8 9 10 |
- (void)viewWillAppear:(BOOL)animated { /// 先呼叫原始實現,由於主類本身有實現該方法,所以這裡實際呼叫的是主類的實現 [self mrc_viewWillAppear:animated]; /// 我們增加的功能 [MobClick beginLogPageView:NSStringFromClass([self class])]; } - (void)mrc_viewWillAppear:(BOOL)animated { /// 主類的實現 } |
第 2 種情況:主類本身沒有實現需要替換的方法,而是繼承了父類的實現,即 class_addMethod
方法返回 YES
。這時使用 class_getInstanceMethod
函式獲取到的 originalSelector
指向的就是父類的方法,我們再通過執行 class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
將父類的實現替換到我們自定義的 mrc_viewWillAppear
方法中。這樣就達到了在 mrc_viewWillAppear
方法的實現中呼叫父類實現的目的。
1 2 3 4 5 6 7 8 9 10 |
- (void)viewWillAppear:(BOOL)animated { /// 先呼叫原始實現,由於主類本身並沒有實現該方法,所以這裡實際呼叫的是父類的實現 [self mrc_viewWillAppear:animated]; /// 我們增加的功能 [MobClick beginLogPageView:NSStringFromClass([self class])]; } - (void)mrc_viewWillAppear:(BOOL)animated { /// 父類的實現 } |
看到這裡,相信你對 Method Swizzling 已經有了一定的瞭解,那麼接下來就請你自己親自試一試吧,you should give it a try yourself 。
總結
Method Swizzling 是一種黑魔法,我們在使用它時需要加倍小心,而遵循本文的最佳實踐可以讓你事半功倍。
參考連結
http://nshipster.com/method-swizzling/
https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html