原文鏈接: "https://www.cnblogs.com/mddblog/p/11105450.html" 如果已經比較熟悉,可以跳過整體介紹,直接看常見問題部分 整體介紹 方法交換是runtime的重要體現,也是"消息語言"的核心。OC給開發者開放了很多介面,讓開發者也能全程參與這一過程。 原 ...
原文鏈接:https://www.cnblogs.com/mddblog/p/11105450.html
如果已經比較熟悉,可以跳過整體介紹,直接看常見問題部分
整體介紹
方法交換是runtime的重要體現,也是"消息語言"的核心。OC給開發者開放了很多介面,讓開發者也能全程參與這一過程。
原理
oc的方法調用,比如[self test]
會轉換為objc_msgSend(self,@selfector(test))
。objc_msgsend會以@selector(test)
作為標識,在方法接收者(self)所屬類(以及所屬類繼承層次)方法列表找到Method,然後拿到imp函數入口地址,完成方法調用。
typedef struct objc_method *Method;
// oc2.0已廢棄,可以作為參考
struct objc_method {
SEL _Nonnull method_name;
char * _Nullable method_types;
IMP _Nonnull method_imp;
}
基於以上鋪墊,那麼有兩種辦法可以完成交換:
- 一種是改變
@selfector(test)
,不太現實,因為我們一般都是hook系統方法,我們拿不到系統源碼,不能修改。即便是我們自己代碼拿到源碼修改那也是編譯期的事情,並非運行時(跑題了。。。) - 所以我們一般修改imp函數指針。改變sel與imp的映射關係;
系統為我們提供的介面
typedef struct objc_method *Method;
Method是一個不透明指針,我們不能夠通過結構體指針的方式來訪問它的成員,只能通過暴露的介面來操作。
介面如下,很簡單,一目瞭然:
#import <objc/runtime.h>
/// 根據cls和sel獲取實例Method
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
/// 給cls新增方法,需要提供結構體的三個成員,如果已經存在則返回NO,不存在則新增並返回成功
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
/// method->imp
IMP _Nonnull method_getImplementation(Method _Nonnull m);
/// 替換
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
/// 跟定兩個method,交換它們的imp:這個好像就是我們想要的
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
簡單使用
假設交換UIViewController的viewDidLoad方法
/// UIViewController 某個分類
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
Method originMethod = class_getInstanceMethod(target, originalSelector);
Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
method_exchangeImplementations(originMethod, swizzledMethod);
}
+ (void)load {
[self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
}
/// hook
- (void)swizzle_viewDidLoad {
[self swizzle_viewDidLoad];
}
交換本身簡單:原理簡單,介面方法也少而且好理解,因為結構體定義也就三個成員變數,也難不到哪裡去!
但是,具體到使用場景,疊加上其它外部的不穩定因素,想要穩定的寫出通用或者半通用交換方法,上面的"簡單使用"遠遠不夠的。
下麵就詳細介紹下幾種常見坑,也是為啥網上已有很多文章介紹方法交換,為什麼還要再寫一篇的原因:不再有盲點
常見問題一、被多次調用(多次交換)
"簡單使用"中的代碼用於hook viewDidload一般是沒問題的,+load 方法一般也執行一次。但是如果一些程式員寫法不規範時,會造成多次調用。
比如寫了UIViewController的子類,在子類裡面實現+load
方法,又習慣性的調用了super方法
+ (void)load {
// 這裡會引起UIViewController父類load方法多次調用
[super load];
}
又或者更不規範的調用,直接調用load,類似[UIViewController load]
為了沒盲點,我們擴展下load的調用:
- load方法的調用時機在dyld映射image時期,這也符合邏輯,載入完調用load。
- 類與類之間的調用順序與編譯順序有關,先編譯的優先調用,繼承層次上的調用順序則是先父類再子類;
- 類與分類的調用順序是,優先調用類,然後是分類;
- 分類之間的順序,與編譯順序有關,優先編譯的先調用;
- 系統的調用是直接拿到imp調用,沒有走消息機制;
手動的[super load]
或者[UIViewController load]
則走的是消息機制,分類的會優先調用,如果你運氣好,另外一個程式員也實現了UIViewController的分類,且實現+load方法,還後編譯,則你的load方法也只執行一次;(分類同名方法後編譯的會“覆蓋”之前的)
為了保險起見,還是:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
});
}
繼續擴展:多次調用的副作用是什麼呢?
- 根據原理,如果是偶數次
結果就是方法交換不生效,但是有遺留問題,這時手動調用
- (void)swizzle_viewDidLoad {
[self swizzle_viewDidLoad];
}
會引起死迴圈。
其實,方法交換後,任何時候都不要嘗試手動調用,特別是交換的系統方法。實際開發中,也沒人會手動調用,這裡我們只討論這種場景的技術及後果,幫助理解
- 奇數次調用
奇數次之後一切正常。但是,奇數次之前,它會先經歷偶數次。
比如,第一次交換,正常,第二次交換,那麼相當於沒有交換,如果你手動調用了swizzle_viewDidLoad,很明顯死迴圈了,然後你又在其它線程進行第三次交換,又不死迴圈了。哈哈,好玩,但你要保重,別玩失火了玩到線上了!!!
這種情況還是有可能發生的,比如交換沒有放在load方法,又沒有dispatch_once,而是自己寫了個類似start的開始方法,被自己或者他人誤調用。
最後:為了防止多次交換始終加上dispatch_once,除非你清楚你自己在幹啥。
再次擴展:常見的多次交換
這裡說的多次交換,和上面說的不一樣,交換方法不一樣,比如我們開發中經常遇到的。
我們自己交換了viewDidLoad,然後第三方庫也交換了viewDidLoad,那麼交換前(箭頭代表映射關係):
sysSel -> sysImp
ourSel -> ourImp
thirdSel -> thirdImp
第一步,我們與系統交換:
sysSel -> ourImp
ourSel -> sysImp
thirdSel -> thirdImp
第二步,第三方與系統交換:
sysSel -> thirdImp
ourSel -> sysImp
thirdSel -> ourImp
假設,push了一個VC,首先是系統的sysSel,那麼調用順序:
thirdImp、ourImp、sysImp
沒毛病!
多次交換這種場景是真實存在的,比如我們監控viewDidload/viewWillappear,在程式退到後臺時,想停止監控,則再進行一次(偶數)交換也是一種取消監控的方式。當再次進入前臺時,則再次(奇數)交換,實現監控。(通過標誌位實現用的更多,更簡單)
問題二、被交換的類沒有實現該方法
我們還是在分類裡面添加方法來交換
情況一:父類實現了被交換方法
我們本意交換的是子類方法,但是子類沒有實現,父類實現了class_getInstanceMethod(target, swizzledSelector);
執行的結果返回父類的Method,那麼後續交換就相當於和父類的方法實現了交換。
一般情況下也不會出問題,可是埋下了一系列隱患。如果其它程式員也繼承了這個父類。舉例代碼如下
/// 父類
@interface SuperClassTest : NSObject
- (void)printObj;
@end
@implementation SuperClassTest
- (void)printObj {
NSLog(@"SuperClassTest");
}
@end
/// 子類1
@interface SubclassTest1 : SuperClassTest
@end
@implementation SubclassTest1
- (void)printObj {
NSLog(@"printObj");
}
@end
/// 子類2
@interface SubclassTest2 : SuperClassTest
@end
@implementation SubclassTest2
/// 有沒有重寫此方法,會呈現不同的結果
- (void)printObj {
// 有沒有調用super 也是不同的結果
[super printObj];
NSLog(@"printObj");
}
@end
/// 子類1 分類實現交換
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
Method originMethod = class_getInstanceMethod(target, originalSelector);
Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
method_exchangeImplementations(originMethod, swizzledMethod);
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceMethod:[SubclassTest1 class] original:@selector(printObj) swizzled:@selector(swiprintObj)];
});
}
- (void)swiprintObj {
NSLog(@"swi1:%@",self);
[self swiprintObj];
}
示例代碼,實現了printObj
與 swiprintObj
的交換。
- 問題1:父類的實例對象正常調用printObj,也會造成swiprintObj優先調用,然後再調用printObj,這不是我們想要的,如果你想監控父類,那麼完全可以直接交換父類的方法;
- 問題2:假設sub2(子類2)沒有實現printObj,但它的實例對象也調用了printObj,正常應該是能夠調用父類的printObj方法,但是由於被交換,會調用sub1的swiprintObj,swiprintObj的實現裡面有
[self swiprintObj]
,這裡的self是sub2,sub2是沒有實現swiprintObj
的,直接崩潰。 - 問題3:sub2子類重寫了printObj,一切正常,sub2實例對象調用正常,但是如果在printObj裡面調用super方法就。。。
那麼如何避免這種情況呢?
使用class_addMethod方法來避免。再次優化後的結果:
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
Method originMethod = class_getInstanceMethod(target, originalSelector);
Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}
else {
method_exchangeImplementations(originMethod, swizzledMethod);
}
}
分步驟詳細解析如下:
- class_addMethod 執行前
superSel -> superImp
sub1SwiSel -> sub1SwiImp
- class_addMethod 執行後,給子類增加了sel,但是對應的imp實現還是swizzledMethod的imp即交換方法的imp
superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> sub1SwiImp
被交換的方法sub1Sel
已經指向了交換方法的imp實現,下一步將交換方法的sel 指向被交換方法的imp即可。被交換方法不是沒有實現嗎??? 有的,OC繼承關係,父類的實現就是它的實現superImp
- class_replaceMethod,將sub1SwiSel的實現替換為superImp
superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> superImp
系統在給對象發送sel消息時,執行sub1SwiImp,sub1SwiImp裡面發送sub1SwiSel,執行superImp,完成hook。
我們說的給子類新增method,其實並不是一個全新的,而是會共用imp,函數實現沒有新增。這樣的好處是superSel
對應的imp沒有改變,它自己的以及它的其它子類不受影響,完美解決此問題;但是繼續往下看其它問題
情況2:父類也沒有實現
尷尬了,都沒有實現方法,那還交換個錘子???
先說結果吧,交換函數執行後,方法不會被交換,但是手動調用下麵這些,同樣會死迴圈。
- (void)swiprintObj {
NSLog(@"swi1:%@",self);
[self swiprintObj];
}
所以我們要加判斷,然後返回給方法調用者一個bool值,或者更直接一點,拋出異常。
/// 交換類方法的註意獲取meta class, object_getClass。class_getClassMethod
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
Method originMethod = class_getInstanceMethod(target, originalSelector);
Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
if (originMethod && swizzledMethod) {
if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}
else {
method_exchangeImplementations(originMethod, swizzledMethod);
}
}
else {
@throw @"originalSelector does not exit";
}
}
再加上 dispatch_once 上面已經算是比較完美了,但是並沒有完美,主要是場景不同,情況就不同。我們只有理解原理,不同場景不同對待。
最後補充一點:新建類來交換系統方法
上面說的都是在分類裡面實現交換方法,這裡新建"私有類"來交換系統方法。
在寫SDK時,分類有重名覆蓋問題,編譯選項還要加-ObjC。出問題編譯階段還查不出來。那麼我們可以用新建一個私有類實現交換,類重名則直接編譯報錯。交換方法和上面的分類交換稍不一樣
比如hook viewDidload,代碼如下:
@interface SwizzleClassTest : NSObject
@end
@implementation SwizzleClassTest
+ (void)load {
/// 私有類,可以不用dispatch_once
Class target = [UIViewController class];
Method swiMethod = class_getInstanceMethod(self, @selector(swi_viewDidLoad));
Method oriMethod = class_getInstanceMethod(target, @selector(viewDidLoad));
if (swiMethod && oriMethod) {
if (class_addMethod(target, @selector(swi_viewDidLoad), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))) {
// 這裡獲取給UIViewController新增的method
swiMethod = class_getInstanceMethod(target, @selector(swi_viewDidLoad));
method_exchangeImplementations(oriMethod, swiMethod);
}
}
}
- (void)swi_viewDidLoad {
// 不能調用,這裡的self是UIViewController類或者子類的實例,調用test的話直接崩潰。或者做類型判斷 [self isKindOfClass:[SwizzleClassTest class]],然後再調用
// [self test];
[self swi_viewDidLoad];
}
- (void)test {
NSLog(@"Do not do this");
}
@end
這裡也用到class_addMethod,給UIViewController新增了一個swi_viewDidLoad sel及其imp實現,共用了SwizzleClassTest 的imp實現。
另外系統發送viewdidload消息進而調用swi_viewDidLoad方法,裡面的self是UIViewController,所以不能再[self test]
,否則崩潰。也不能在其它地方手動[self swi_viewDidLoad];
會死迴圈,因為這時候self是SwizzleClassTest,而它的method是沒有被交換的,好處是我們可以通過self的類型判斷來避免。
可以比較下交換前後,
交換前:
SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp
UIViewController_viewDidLoadSel -> UIViewController_viewDidLoadImp
交換後:
SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp
UIViewController_swi_viewDidLoadSel -> UIViewController_viewDidLoadImp
UIViewController_viewDidLoadSel -> UIViewController_swi_viewDidLoadImp
可以看出 SwizzleClassTest 沒有受影響,映射關係不變。
這種想取消的話,也很簡單method_exchangeImplementations
總結
- 首先要知道方法交換的原理;
- 熟悉它常用介面;
- 被交換方法不存在引發的 父類、子類問題;
- 以及oc中方法的繼承、“覆蓋”問題;
- 可能引發重覆交換的問題,以及後果;
- 理解self只是個隱藏參數,並不一定是當前方法所在的類的實例對象
最後,每次使用方法交換時,都要認真推演一遍,計算下可能產生的影響。