Method Swizzling實現原理

在上週 associated objects一文中,咱們開始探索Objective-C運行時的一些黑魔法。本週咱們繼續前行,來討論多是最受爭議的運行時技術:method swizzling。
 
Method swizzling指的是改變一個已存在的選擇器對應的實現的過程,它依賴於Objectvie-C中方法的調用可以在運行時進改變——經過改變類的調度表(dispatch table)中選擇器到最終函數間的映射關係。
 
舉個例子,假設咱們想跟蹤在一個iOS應用中每一個視圖控制器展示給用戶的次數:
 
咱們能夠給每一個視圖控制器對應的viewWillAppear:實現方法中增長相應的跟蹤代碼,可是這樣作會產生大量重複的代碼。子類化多是另外一個選擇,但要求你將UIViewController、 UITableViewController、 UINavigationController 以及全部其餘視圖控制器類都子類化,這也會致使代碼重複。
 
幸虧,還有另外一個方法,在分類中進行method swizzling,下面來看怎麼作:
  1. #import <objc/runtime.h> 
  2.  
  3. @implementation UIViewController (Tracking) 
  4.  
  5. + (void)load { 
  6.     static dispatch_once_t onceToken; 
  7.     dispatch_once(&onceToken, ^{ 
  8.         Class class = [self class]; 
  9.  
  10.         // When swizzling a class method, use the following: 
  11.         // Class class = object_getClass((id)self); 
  12.  
  13.         SEL originalSelector = @selector(viewWillAppear:); 
  14.         SEL swizzledSelector = @selector(xxx_viewWillAppear:); 
  15.  
  16.         Method originalMethod = class_getInstanceMethod(class, originalSelector); 
  17.         Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); 
  18.  
  19.         BOOL didAddMethod = 
  20.             class_addMethod(class, 
  21.                 originalSelector, 
  22.                 method_getImplementation(swizzledMethod), 
  23.                 method_getTypeEncoding(swizzledMethod)); 
  24.  
  25.         if (didAddMethod) { 
  26.             class_replaceMethod(class, 
  27.                 swizzledSelector, 
  28.                 method_getImplementation(originalMethod), 
  29.                 method_getTypeEncoding(originalMethod)); 
  30.         } else { 
  31.             method_exchangeImplementations(originalMethod, swizzledMethod); 
  32.         } 
  33.     }); 
  34.  
  35. #pragma mark - Method Swizzling 
  36.  
  37. - (void)xxx_viewWillAppear:(BOOL)animated { 
  38.     [self xxx_viewWillAppear:animated]; 
  39.     NSLog(@"viewWillAppear: %@", self); 
  40.  
  41. @end 
在計算機學科中,指針變換(pointer swizzling)是指將基於名字或位置的引用轉變爲直接的指針引用。 然而在Objective-C中,這個詞的起源並不徹底知道,但關於這一借鑑其實也很好理解,method swizzling能夠經過選擇器來改變它引用的函數指針。
 
如今,當UIViewController或它子類的任何實例觸發viewWillAppear:方法都會打印一條log日誌。
 
向視圖控制器的生命週期中注入操做、事件的響應、視圖的繪製,或Foundation中的網絡堆棧都是可以利用method swizzling產生明顯效果的場景。還有一些其餘的場景使用swizzling會是一個合適的選擇,這隨着Objective-C開發者經驗不斷豐富會變得愈來愈明顯。
 
先不說爲何和在哪些地方使用swizzling,來看一下應該怎樣實現:
 
+load vs. +initialize
Swizzling應該在+load方法中實現。
每一個類的這兩個方法會被Objective-C運行時系統自動調用,+load是在一個類最開始加載時調用,+initialize是在應用中第一次調用該類或它的實例的方式以前調用。這兩個方法都是可選的,只有實現了纔會被執行。
 
由於method swizzling會影響全局,因此減小冒險狀況就很重要。+load可以保證在類初始化的時候就會被加載,這爲改變系統行爲提供了一些統一性。但+initialize並不能保證在何時被調用——事實上也有可能永遠也不會被調用,例如應用程序從未直接的給該類發送消息。
 
dispatch_once
Swizzling應該在dispatch_once中實現。
 
仍是由於swizzling會改變全局,咱們須要在運行時採起全部可用的防範措施。保障原子性就是一個措施,它確保代碼即便在多線程環境下也只會被執行一次。GCD中的diapatch_once就提供這些保障,它應該被當作swizzling的標準實踐。
 
選擇器、方法及實現
在Objective-C中,儘管這些詞常常被放在一塊兒來描述消息傳遞的過程,但選擇器、方法及實現分別表明運行時的不一樣方面。
 
下面是蘋果Objective-C Runtime Reference文檔中對它們的描述:
1.選擇器(typedef struct objc_selector *SEL):選擇器用於表示一個方法在運行時的名字,一個方法的選擇器是一個註冊到(或映射到)Objective-C運行時中的C字符串,它是由編譯器生成並在類加載的時候被運行時系統自動映射。
 
2.方法(typedef struct objc_method *Method):一個表明類定義中一個方法的不明類型。
 
3.實現(typedef id (*IMP)(id, SEL, ...)):這種數據類型是實現某個方法的函數開始位置的指針,函數使用的是基於當前CPU架構的標準C調用規約。第一個參數是指向self的指針(也就是該類的某個實例的內存空間,或者對於類方法來講,是指向元類(metaclass)的指針)。第二個參數是方法的選擇器,後面跟的都是參數。
 
理解這些概念之間關係最好的方式是:一個類(Class)維護一張調度表(dispatch table)用於解析運行時發送的消息;調度表中的每一個實體(entry)都是一個方法(Method),其中key值是一個惟一的名字——選擇器(SEL),它對應到一個實現(IMP)——實際上就是指向標準C函數的指針。
 
Method Swizzling就是改變類的調度表讓消息解析時從一個選擇器對應到另一個的實現,同時將原始的方法實現混淆到一個新的選擇器。
 
調用_cmd
下面這段代碼看起來像是會致使一個死循環:
  1. - (void)xxx_viewWillAppear:(BOOL)animated { 
  2.     [self xxx_viewWillAppear:animated]; 
  3.     NSLog(@"viewWillAppear: %@", NSStringFromClass([self class])); 
 
但其實並無,在Swizzling的過程當中,xxx_viewWillAppear:會被從新分配給UIViewController的-viewWillAppear:的原始實現。一個優秀程序員應有的直覺會告訴你在一個方法的實現中經過self調用當前方法自身會產生錯誤,可是在當前這種狀況下,若是咱們記住究竟是怎麼回事更有意義。反而,若是咱們在這個方法中調用viewWillAppear:纔會真的致使死循環,由於這個方法的實現會在運行時被swizzle到viewWillAppear:的選擇器。
 
記住給swizzled方法加上前綴,這和你須要給可能產生衝突的分類方法加前綴是一個道理。
 
注意事項
Swizzling被廣泛認爲是一種巫術,容易致使不可預料的行爲和結果。儘管不是最安全的,可是若是你採起下面這些措施,method swizzling仍是很安全的。
 
1.始終調用方法的原始實現(除非你有足夠的理由不這麼作): API爲輸入和輸出提供規約,但它裏面具體的實現實際上是個黑匣子,在Method Swizzling過程當中不調用它原始的實現可能會破壞一些私有狀態,甚至是程序的其餘部分。
 
2.避免衝突:給分類方法加前綴,必定要確保不要讓你代碼庫中其餘代碼(或是依賴庫)在作與你相同的事。
 
3.理解:只是簡單的複製粘貼swizzling代碼而不去理解它是怎麼運行的,這不只很是危險,並且還浪費了學習Objective-C運行時的機會。閱讀  Objective-C Runtime Reference 和 <objc/rumtime.h> 去理解代碼是怎樣和爲何這樣執行的,努力的用你的理解來消滅你的疑惑。
 
謹慎行事:無論你多麼自信你可以swizzling Foundation、UIKit 或者其餘內置框架,請記住全部這些均可能在下一個版本中就很差使。提早作好準備,防範於未然纔不至於到時候焦頭爛額。
 
不敢放心大膽的直接使用Objective-C運行時? Jonathan ‘Wolf’ Rentzsch提供了通過實戰檢驗的、支持CocoaPads的庫 JRSwizzle,它會爲你考慮好了一切。
 
與associated objects同樣,method swizzling是一個強大的技術,可是你也應該謹慎使用。
相關文章
相關標籤/搜索