標籤:
在上周associated objects一文中,我們開始探索Objective-C運行時的一些黑魔法。本周我們繼續前行,來討論可能是最受爭議的運行時技術:method swizzling。
Method swizzling指的是改變一個已存在的選取器對應的實現的過程,它依賴於Objectvie-C中方法的調用能夠在運行時進改變——通過改變類的調度表(dispatch table)中選取器到最終函數間的映射關係。 舉個例子,假設我們想跟蹤在一個iOS應用中每個視圖控制器展現給使用者的次數: 我們可以給每個視圖控制器對應的viewWillAppear:實現方法中增加相應的跟蹤代碼,但是這樣做會產生大量重複的代碼。子類化可能是另一個選擇,但要求你將UIViewController、 UITableViewController、 UINavigationController 以及所有其他視圖控制器類都子類化,這也會導致代碼重複。 幸好,還有另一個方法,在分類中進行method swizzling,下面來看怎麼做:
- #import
-
- @implementation UIViewController (Tracking)
-
- + (void)load {
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- Class class = [self class];
-
- // When swizzling a class method, use the following:
- // Class class = object_getClass((id)self);
-
- SEL originalSelector = @selector(viewWillAppear:);
- SEL swizzledSelector = @selector(xxx_viewWillAppear:);
-
- Method originalMethod = class_getInstanceMethod(class, originalSelector);
- Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
-
- BOOL didAddMethod =
- class_addMethod(class,
- originalSelector,
- method_getImplementation(swizzledMethod),
- method_getTypeEncoding(swizzledMethod));
-
- if (didAddMethod) {
- class_replaceMethod(class,
- swizzledSelector,
- method_getImplementation(originalMethod),
- method_getTypeEncoding(originalMethod));
- } else {
- method_exchangeImplementations(originalMethod, swizzledMethod);
- }
- });
- }
-
- #pragma mark - Method Swizzling
-
- - (void)xxx_viewWillAppear:(BOOL)animated {
- [self xxx_viewWillAppear:animated];
- NSLog(@"viewWillAppear: %@", self);
- }
-
- @end
在電腦學科中,指標變換(pointer swizzling)是指將基於名字或位置的引用轉變為直接的指標引用。 然而在Objective-C中,這個詞的起源並不完全知道,但關於這一借鑒其實也很好理解,method swizzling可以通過選取器來改變它引用的函數指標。 現在,當UIViewController或它子類的任何執行個體觸發viewWillAppear:方法都會列印一條log日誌。 向視圖控制器的生命週期中注入操作、事件的響應、視圖的繪製,或Foundation中的網路堆棧都是能夠利用method swizzling產生明顯效果的情境。還有一些其他的情境使用swizzling會是一個合適的選擇,這隨著Objective-C開發人員經驗不斷豐富會變得越來越明顯。 先不說為什麼和在哪些地方使用swizzling,來看一下應該怎樣實現:
+load vs. +initializeSwizzling應該在+load方法中實現。每個類的這兩個方法會被Objective-C運行時系統自動調用,+load是在一個類最開始載入時調用,+initialize是在應用中第一次調用該類或它的執行個體的方式之前調用。這兩個方法都是可選的,只有實現了才會被執行。 因為method swizzling會影響全域,所以減少冒險情況就很重要。+load能夠保證在類初始化的時候就會被載入,這為改變系統行為提供了一些統一性。但+initialize並不能保證在什麼時候被調用——事實上也有可能永遠也不會被調用,例如應用程式從未直接的給該類發送訊息。
dispatch_onceSwizzling應該在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下面這段代碼看起來像是會導致一個死迴圈:
- - (void)xxx_viewWillAppear:(BOOL)animated {
- [self xxx_viewWillAppear:animated];
- 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 和 去理解代碼是怎樣和為什麼這樣執行的,努力的用你的理解來消滅你的疑惑。 謹慎行事:不管你多麼自信你能夠swizzling Foundation、UIKit 或者其他內建架構,請記住所有這些都可能在下一個版本中就不好使。提前做好準備,防範於未然才不至於到時候焦頭爛額。 不敢放心大膽的直接使用Objective-C運行時?Jonathan ‘Wolf’ Rentzsch提供了經過實戰檢驗的、支援CocoaPads的庫JRSwizzle,它會為你考慮好了一切。 與associated objects一樣,method swizzling是一個強大的技術,但是你也應該謹慎使用。
Objective-C Runtime 運行時之四:Method Swizzling