原文連接:http://www.javashuo.com/article/p-olcodhgo-dp.htmlhtml
若是對方法交換已經比較熟悉,能夠跳過總體介紹,直接看常見問題部分程序員
方法交換是runtime的重要體現,也是"消息語言"的核心。OC給開發者開放了不少接口,讓開發者也能全程參與這一過程。app
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系統方法,咱們拿不到系統源碼,不能修改。即使是咱們本身代碼拿到源碼修改那也是編譯期的事情,並不是運行時(跑題了。。。)typedef struct objc_method *Method;
Method是一個不透明指針,咱們不可以經過結構體指針的方式來訪問它的成員,只能經過暴露的接口來操做。this
接口以下,很簡單,一目瞭然:spa
#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]; }
交換自己簡單:原理簡單,接口方法也少並且好理解,由於結構體定義也就三個成員變量,也難不到哪裏去!指針
可是,具體到使用場景,疊加上其它外部的不穩定因素,想要穩定的寫出通用或者半通用交換方法,上面的"簡單使用"遠遠不夠的。code
下面就詳細介紹下幾種常見坑,也是爲啥網上已有不少文章介紹方法交換,爲何還要再寫一篇的緣由:再也不有盲點
"簡單使用"中的代碼用於hook viewDidload通常是沒問題的,+load 方法通常也執行一次。可是若是一些程序員寫法不規範時,會形成屢次調用。
好比寫了UIViewController的子類,在子類裏面實現+load
方法,又習慣性的調用了super方法
+ (void)load { // 這裏會引發UIViewController父類load方法屢次調用 [super load]; }
又或者更不規範的調用,直接調用load,相似[UIViewController load]
手動的[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
的交換。
[self swiprintObj]
,這裏的self是sub2,sub2是沒有實現swiprintObj
的,直接崩潰。那麼如何避免這種狀況呢?
使用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); } }
分步驟詳細解析以下:
superSel -> superImp
sub1SwiSel -> sub1SwiImp
superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> sub1SwiImp
被交換的方法sub1Sel
已經指向了交換方法的imp實現,下一步將交換方法的sel 指向被交換方法的imp便可。被交換方法不是沒有實現嗎??? 有的,OC繼承關係,父類的實現就是它的實現superImp
superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> superImp
系統在給對象發送sel消息時,執行sub1SwiImp,sub1SwiImp裏面發送sub1SwiSel,執行superImp,完成hook。
咱們說的給子類新增method,其實並非一個全新的,而是會共享imp,函數實現沒有新增。這樣的好處是superSel
對應的imp沒有改變,它本身的以及它的其它子類不受影響,完美解決此問題;可是繼續往下看其它問題
尷尬了,都沒有實現方法,那還交換個錘子???
先說結果吧,交換函數執行後,方法不會被交換,可是手動調用下面這些,一樣會死循環。
- (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
這裏講的是用C函數交換系統類的方法。而不是fishhook的hook C的函數,目標不同。原理也不同
還以hook UIViewController
的viewDidLoad
爲例
上面說到,oc方法調用會轉換爲objc_msgSend(self,_cmd,param)
這種形式,這裏再補充一點,objc_msgSend找到imp函數指針後,最終會是imp(self,_cmd,param)
調用C函數,imp其實就是個C函數指針。
那麼咱們能夠定義一個C函數,讓sel和咱們新建的C函數(imp)造成映射。另外還須要記錄以前的imp實現,能夠定義一個函數指針來保存sel以前的imp實現;大概示意:
以前:
pOriImp = NULL
vcSel -> vcImp
Cfun(){};
以後:
pOriImp = vcImp;
vcSel -> cFun;// 函數名即爲函數指針
詳細以下:
/// 準備1. 定義一個函數指針,用於記錄系統本來的IMP實現,並初始化爲NULL void (*origin_test_viewDidload)(id,SEL) = NULL; /// 準備2. 定義要交換的函數,裏面會調用系統的IMP static void swizzle_test_viewDidload(id self, SEL _cmd) { // 這裏打印的self爲UIViewController或者子類實例 NSLog(@"%@",self); if (origin_test_viewDidload) { origin_test_viewDidload(self, _cmd); } } /// 開始交換。startHook能夠是某個類的方法或實例方法或C函數均可以 + (void)startHook { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class target = [UIViewController class]; SEL oriSel = @selector(viewDidLoad); // 要交換的函數 IMP swiImp = (IMP)swizzle_test_viewDidload; Method origMethod = class_getInstanceMethod(target, oriSel); // 替換以前的先保留 origin_test_viewDidload = (void *)method_getImplementation(origMethod); if (origin_test_viewDidload) { // 最後替換,這裏用到了set method_setImplementation(origMethod, swiImp); } }); }
這種hook,沒有給類的MethodList新增Method,只是替換了實現,對原類改動最小。
和其它hook方式同樣,這種對第三方庫 的hook,也是不影響。若是第三方庫也交換了,均會獲得調用
最後,若是你想取消hook,很簡單,method_setImplementation爲原來的IMP便可。記着把origin_test_viewDidload也置爲NULL.
最後,大概三類hook,至於想用哪一種,其實無所謂了,看具體場景。可是原理必定要清楚,每次hook時,都要認真推演一遍,計算下可能產生的影響。