Method Swizzling 是什么
Method Swizzling是objective-c中的黑魔法,算是runtime中的一種實戰使用模式,它允許我們動態的替換方法,實現Hook功能。
但是它也是一把雙刃劍,用得好的人可以用它來很輕松的實現一些復雜的功能,而如果用的不好,后果就真的是毀滅性的傷害,這樣的黑魔法,我們一定要盡力去掌握并駕馭它。
Method Swizzling 能做什么
先從名字來看,Method方法Swizzling混合,那他的意思就是方法混合???? 好像也沒有一個準確的翻譯,我們就姑且翻譯成方法交換吧。
也就是說把原來 A方法實現的a,原來B方法實現的b交換一下,讓A來實現b的功能,讓B來實現a的功能。咋一看好像沒什么厲害的地方,不就是交換個方法么,有什么用呢?您先別急,往下看。
Method Swizzling 原理
我們來看看 Method Swizzling 的原理。
雖然之前我們說Method Swizzling是一把雙刃劍,用不好就容易把自己砍死,但是如果我們知道如何使用,這些就不會那么可怕了。
在Method方法中,有兩個關鍵的成員變量:SEL和IMP。
SEL就是我們平時看到的方法的名稱,比如@selector(viewWillAppear:)。
IMP是一個函數指針,指向的是方法的實現。
原則上,方法名SEL和IMP是一一對應的,那Method Swizzling的本質就是改變他們的對應關系,達到交換方法實現的目的。
Method Swizzling實現&實踐
好了,廢話說了一大堆,接下來我們就來說說到底是怎么實現Method Swizzling吧。
我們用一個日常開發中都會用到的需求作為例子,平時我們可能會被產品要求在頁面中添加監聽事件,監聽每個頁面的訪問,那我們需要再每個頁面都添加這樣的代碼:
我們可以直接在每個頁面中都添加這樣的代碼,簡單粗暴,但是缺點也很明顯,這樣不僅要干很多體力活兒,還容易漏寫某些頁面,后期可能會被后邊的程序員罵死。
或者我們寫一個UIViewController的基類,讓其他類都繼承他,但是對于已經寫好的代碼,幾十個類這樣修改還是非常麻煩。
這時候就用到了我們的黑魔法Method Swizzling。
Method Swizzling 相關函數介紹
在真正使用黑魔法之前,我們先熟悉一下需要用到的函數:
獲取一個方法名稱(SEL):?class_getInstanceMethod
獲取一個方法的實現(IMP):method_getImplementation
獲取一個實現編碼類型:method_getTypeEncoding
給方法添加實現:class_addMethod
用一個方法實現替換另一個方法的實現:class_replaceMethod
交換兩個方法的實現:method_exchangeImplementations
Method Swizzling 實現
在進行方法替換前,我們要考慮兩種情況:
要替換的方法在target class中有實現
要替換的方法在target class中沒有實現,而是在其父類中實現
對于第一種情況,很簡單,我們直接調用method_exchangeImplementations即可達成方法。
而對于第二種情況,我們要仔細想想了。
因為在target class中沒有對應的方法實現,方法實際上是在target class的父類中實現的,因此當我們要交換方法實現時,其實是交換了target class父類的實現。這樣當其他地方調用這個父類的方法時,也會調用我們所替換的方法,這顯然使我們不想要的。
比如,我想替換UIViewController類中的methodForSelector:方法,其實該方法是在其父類NSObject類中實現的。如果我們直接調用method_exchangeImplementations,則會替換掉NSObject的方法。這樣當我們在別的地方,比如UITableView中再調用methodForSelector:方法時,其實會調用到父類NSObject,而NSObject的實現,已經被我們替換了。
為了避免這種情況,我們在進行方法替換前,需要檢查target class是否有對應方法的實現,如果沒有,則要講方法動態的添加到class的method list中。
+ (void)load{
staticdispatch_once_tonceToken;
dispatch_once(&onceToken, ^{
// 獲取類方法
// Class selfClass = object_getClass([self class]);
// 獲取實例方法
Class selfClass = [selfclass];
//獲取原方法的名稱和實現
SEL oriSEL =@selector(viewWillAppear:);?
Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);
//獲取替換方法的名稱和實現
SEL swizzlingSEL =@selector(swizzlingViewWillAppear:);?
Method swizzlingMethod = class_getInstanceMethod(selfClass, swizzlingSEL);
//給原方法添加替換方法實現,為了避免原方法沒有實現
BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(swizzlingMethod), method_getTypeEncoding(swizzlingMethod));
//成功,將原方法的實現替換到替換方法的實現
if(addSucc) {
?class_replaceMethod(selfClass, swizzlingSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }
//失敗,說明原方法已經實現,直接交換方法
else{ method_exchangeImplementations(oriMethod, swizzlingMethod);?
}?
});
}
另外附上替換方法的實現,這里我直接打印了類的名稱,具體需要怎么做直接按需求來就行了。
- (void)swizzlingViewWillAppear(BOOL)animated{
NSLog(@"%@",self.class);
return[selfswizzlingViewWillAppear:animated];
}
Method Swizzling類簇
之前我也說到,在我們項目開發過程中,經常因為NSArray數組越界或者NSDictionary的key或者value值為nil等問題導致的崩潰,對于這些問題蘋果并不會報一個警告,而是直接崩潰,感覺蘋果這樣確實有點“太狠了”。
由此,我們可以根據上面所學,對NSArray、NSMutableArray、NSDictionary、NSMutableDictionary等類進行Method Swizzling,實現方式還是按照上面的例子來做。但是....你發現Method Swizzling根本就不起作用,代碼也沒寫錯啊,到底是什么鬼?
這是因為Method Swizzling對NSArray這些的類簇是不起作用的。因為這些類簇類,其實是一種抽象工廠的設計模式。抽象工廠內部有很多其它繼承自當前類的子類,抽象工廠類會根據不同情況,創建不同的抽象對象來進行使用。例如我們調用NSArray的objectAtIndex:方法,這個類會在方法內部判斷,內部創建不同抽象類進行操作。
所以也就是我們對NSArray類進行操作其實只是對父類進行了操作,在NSArray內部會創建其他子類來執行操作,真正執行操作的并不是NSArray自身,所以我們應該對其“真身”進行操作。
代碼示例
下面我們實現了防止NSArray因為調用objectAtIndex:方法,取下標時數組越界導致的崩潰:
大家發現了嗎,__NSArrayI才是NSArray真正的類,而NSMutableArray又不一樣??。我們可以通過runtime函數獲取真正的類:objc_getClass("__NSArrayI");
舉例
下面我們列舉一些常用的類簇的“真身”:
Method Swizzling 注意要點
功能雖然實現完了,但是這里邊有幾個部分我們要注意一下。
1.為什么要在+load中實現,不要調用【super load】方法
因為+load方法是在類被加載的時候調用的,與之相似的有類的+initialize方法,但是他是一種懶加載模式,當這個類或子類收到第一條消息之前才會調用他,所以+load方法是最好的實現場所。
由于load類方法是程序運行時這個類被加載到內存中就調用的一個方法,執行比較早,并且不需要我們手動調用。而且這個方法具有唯一性,也就是只會被調用一次,不用擔心資源搶奪的問題。
Swizzling在+load中執行時,不要調用[super load]。如果多次調用了[super load],可能會出現“Swizzle無效”的假象。
2.+load只會被調用一次,為什么還要用dispatch_once?
首先我們要確認我們的Method Swizzling只實現一次,因為多次實現會反復交換方法,導致偶數次調用的實現沒有交換,造成不必要的麻煩,添加dispatch_once算是添加一個雙保險,因為誰知道有沒有人會手動調用+load呢?
3.為什么不直接交換方法,而是先要添加方法?
一般情況下,我們都是為了和我們未知的系統方法添加Method Swizzling,而不是完全替換某個功能,所以我們一般都需要再自定義的實現中調用原始的實現,所以就會出現兩種情況:
**.本身就有實現要替換的方法,這個時候比較簡單,class_addMethod返回NO,我們直接交換方法就好。
**.本身沒有實現要替換的方法,而是繼承了父類的實現,class_addMethod返回YES,這個時候,我們需要使用class_getInstanceMethod函數獲取到原始實現方法指向的方法,也就是父類中的實現,再通過實現class_replaceMethod來實現調用。
4.在swizzlingViewWillAppear:方法里調用swizzlingViewWillAppear:,不會引起死循環么?
因為Method Swizzling的原理為方法互換,所以這時候用執行的swizzlingViewWillAppear:方法,實際上是執行的viewWillAppear:方法,因為我們并不知道OC的viewWillAppear:方法實現了什么內容,所以我們通過這個調用來實現系統的功能。
相關參考文章:
iOS 黑魔法 Runtime Method Swizzling 背后的原理 - 一個帥氣瀟灑的豆子 - CSDN博客
iOS開發-runtime-黑魔法Method swizzling - 簡書
iOS開發·runtime原理與實踐: 方法交換篇(Method Swizzling)(iOS“黑魔法”,埋點統計,禁止UI控件連續點擊,防奔潰處理) - 云+社區 - 騰訊云