原文鏈接:http://www.itdecent.cn/p/e95ca2e14731
如果對(duì)方法交換已經(jīng)比較熟悉,可以跳過(guò)整體介紹,直接看常見(jiàn)問(wèn)題部分
整體介紹
方法交換是runtime的重要體現(xiàn),也是"消息語(yǔ)言"的核心。OC給開發(fā)者開放了很多接口,讓開發(fā)者也能全程參與這一過(guò)程。
原理
oc的方法調(diào)用,比如[self test]會(huì)轉(zhuǎn)換為objc_msgSend(self,@selfector(test))。objc_msgsend會(huì)以@selector(test)作為標(biāo)識(shí),在方法接收者(self)所屬類(以及所屬類繼承層次)方法列表找到Method,然后拿到imp函數(shù)入口地址,完成方法調(diào)用。
typedef struct objc_method *Method;
// oc2.0已廢棄,可以作為參考
struct objc_method {
SEL _Nonnull method_name;
char * _Nullable method_types;
IMP _Nonnull method_imp;
}
基于以上鋪墊,那么有兩種辦法可以完成交換:
- 一種是改變
@selfector(test),不太現(xiàn)實(shí),因?yàn)槲覀円话愣际莌ook系統(tǒng)方法,我們拿不到系統(tǒng)源碼,不能修改。即便是我們自己代碼拿到源碼修改那也是編譯期的事情,并非運(yùn)行時(shí)(跑題了。。。) - 所以我們一般修改imp函數(shù)指針。改變sel與imp的映射關(guān)系;
系統(tǒng)為我們提供的接口
typedef struct objc_method *Method;Method是一個(gè)不透明指針,我們不能夠通過(guò)結(jié)構(gòu)體指針的方式來(lái)訪問(wèn)它的成員,只能通過(guò)暴露的接口來(lái)操作。
接口如下,很簡(jiǎn)單,一目了然:
#import <objc/runtime.h>
/// 根據(jù)cls和sel獲取實(shí)例Method
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
/// 給cls新增方法,需要提供結(jié)構(gòu)體的三個(gè)成員,如果已經(jīng)存在則返回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)
/// 跟定兩個(gè)method,交換它們的imp:這個(gè)好像就是我們想要的
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
簡(jiǎn)單使用
假設(shè)交換UIViewController的viewDidLoad方法
/// UIViewController 某個(gè)分類
+ (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];
}
交換本身簡(jiǎn)單:原理簡(jiǎn)單,接口方法也少而且好理解,因?yàn)榻Y(jié)構(gòu)體定義也就三個(gè)成員變量,也難不到哪里去!
但是,具體到使用場(chǎng)景,疊加上其它外部的不穩(wěn)定因素,想要穩(wěn)定的寫出通用或者半通用交換方法,上面的"簡(jiǎn)單使用"遠(yuǎn)遠(yuǎn)不夠的。
下面就詳細(xì)介紹下幾種常見(jiàn)坑,也是為啥網(wǎng)上已有很多文章介紹方法交換,為什么還要再寫一篇的原因:不再有盲點(diǎn)
常見(jiàn)問(wèn)題一、被多次調(diào)用(多次交換)
"簡(jiǎn)單使用"中的代碼用于hook viewDidload一般是沒(méi)問(wèn)題的,+load 方法一般也執(zhí)行一次。但是如果一些程序員寫法不規(guī)范時(shí),會(huì)造成多次調(diào)用。
比如寫了UIViewController的子類,在子類里面實(shí)現(xiàn)+load方法,又習(xí)慣性的調(diào)用了super方法
+ (void)load {
// 這里會(huì)引起UIViewController父類load方法多次調(diào)用
[super load];
}
又或者更不規(guī)范的調(diào)用,直接調(diào)用load,類似[UIViewController load]
為了沒(méi)盲點(diǎn),我們擴(kuò)展下load的調(diào)用:
- load方法的調(diào)用時(shí)機(jī)在dyld映射image時(shí)期,這也符合邏輯,加載完調(diào)用load。
- 類與類之間的調(diào)用順序與編譯順序有關(guān),先編譯的優(yōu)先調(diào)用,繼承層次上的調(diào)用順序則是先父類再子類;
- 類與分類的調(diào)用順序是,優(yōu)先調(diào)用類,然后是分類;
- 分類之間的順序,與編譯順序有關(guān),優(yōu)先編譯的先調(diào)用;
- 系統(tǒng)的調(diào)用是直接拿到imp調(diào)用,沒(méi)有走消息機(jī)制;
手動(dòng)的[super load]或者[UIViewController load]則走的是消息機(jī)制,分類的會(huì)優(yōu)先調(diào)用,如果你運(yùn)氣好,另外一個(gè)程序員也實(shí)現(xiàn)了UIViewController的分類,且實(shí)現(xiàn)+load方法,還后編譯,則你的load方法也只執(zhí)行一次;(分類同名方法后編譯的會(huì)“覆蓋”之前的)
為了保險(xiǎn)起見(jiàn),還是:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
});
}
繼續(xù)擴(kuò)展:多次調(diào)用的副作用是什么呢?
- 根據(jù)原理,如果是偶數(shù)次
結(jié)果就是方法交換不生效,但是有遺留問(wèn)題,這時(shí)手動(dòng)調(diào)用
- (void)swizzle_viewDidLoad {
[self swizzle_viewDidLoad];
}
會(huì)引起死循環(huán)。
其實(shí),方法交換后,任何時(shí)候都不要嘗試手動(dòng)調(diào)用,特別是交換的系統(tǒng)方法。實(shí)際開發(fā)中,也沒(méi)人會(huì)手動(dòng)調(diào)用,這里我們只討論這種場(chǎng)景的技術(shù)及后果,幫助理解
- 奇數(shù)次調(diào)用
奇數(shù)次之后一切正常。但是,奇數(shù)次之前,它會(huì)先經(jīng)歷偶數(shù)次。
比如,第一次交換,正常,第二次交換,那么相當(dāng)于沒(méi)有交換,如果你手動(dòng)調(diào)用了swizzle_viewDidLoad,很明顯死循環(huán)了,然后你又在其它線程進(jìn)行第三次交換,又不死循環(huán)了。哈哈,好玩,但你要保重,別玩失火了玩到線上了!??!
這種情況還是有可能發(fā)生的,比如交換沒(méi)有放在load方法,又沒(méi)有dispatch_once,而是自己寫了個(gè)類似start的開始方法,被自己或者他人誤調(diào)用。
最后:為了防止多次交換始終加上dispatch_once,除非你清楚你自己在干啥。
再次擴(kuò)展:常見(jiàn)的多次交換
這里說(shuō)的多次交換,和上面說(shuō)的不一樣,交換方法不一樣,比如我們開發(fā)中經(jīng)常遇到的。
我們自己交換了viewDidLoad,然后第三方庫(kù)也交換了viewDidLoad,那么交換前(箭頭代表映射關(guān)系):
sysSel -> sysImp
ourSel -> ourImp
thirdSel -> thirdImp
第一步,我們與系統(tǒng)交換:
sysSel -> ourImp
ourSel -> sysImp
thirdSel -> thirdImp
第二步,第三方與系統(tǒng)交換:
sysSel -> thirdImp
ourSel -> sysImp
thirdSel -> ourImp
假設(shè),push了一個(gè)VC,首先是系統(tǒng)的sysSel,那么調(diào)用順序:
thirdImp、ourImp、sysImp
沒(méi)毛??!
多次交換這種場(chǎng)景是真實(shí)存在的,比如我們監(jiān)控viewDidload/viewWillappear,在程序退到后臺(tái)時(shí),想停止監(jiān)控,則再進(jìn)行一次(偶數(shù))交換也是一種取消監(jiān)控的方式。當(dāng)再次進(jìn)入前臺(tái)時(shí),則再次(奇數(shù))交換,實(shí)現(xiàn)監(jiān)控。(通過(guò)標(biāo)志位實(shí)現(xiàn)用的更多,更簡(jiǎn)單)
問(wèn)題二、被交換的類沒(méi)有實(shí)現(xiàn)該方法
我們還是在分類里面添加方法來(lái)交換
情況一:父類實(shí)現(xiàn)了被交換方法
我們本意交換的是子類方法,但是子類沒(méi)有實(shí)現(xiàn),父類實(shí)現(xiàn)了class_getInstanceMethod(target, swizzledSelector);執(zhí)行的結(jié)果返回父類的Method,那么后續(xù)交換就相當(dāng)于和父類的方法實(shí)現(xiàn)了交換。
一般情況下也不會(huì)出問(wèn)題,可是埋下了一系列隱患。如果其它程序員也繼承了這個(gè)父類。舉例代碼如下
/// 父類
@interface SuperClassTest : NSObject
- (void)printObj;
@end
@implementation SuperClassTest
- (void)printObj {
NSLog(@"SuperClassTest");
}
@end
/// 子類1
@interface SubclassTest1 : SuperClassTest
@end
@implementation SubclassTest1
- (void)swiprintObj {
NSLog(@"printObj");
}
@end
/// 子類1 分類實(shí)現(xiàn)交換
@interface SubclassTest1(swiTest)
@end
@implementation SubclassTest1(swiTest)
+ (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];
}
@end
/// 子類2
@interface SubclassTest2 : SuperClassTest
@end
@implementation SubclassTest2
/// 有沒(méi)有重寫此方法,會(huì)呈現(xiàn)不同的結(jié)果
//- (void)printObj {
// // 有沒(méi)有調(diào)用super 也是不同的結(jié)果
// [super printObj];
// NSLog(@"printObj");
//}
@end
示例代碼,實(shí)現(xiàn)了printObj與 swiprintObj的交換。
- 問(wèn)題1:父類的實(shí)例對(duì)象調(diào)用selector(printObj),也會(huì)造成imp(swiprintObj)優(yōu)先調(diào)用,但是父類并沒(méi)有實(shí)現(xiàn)swiprintObj,因此會(huì)造成找不到方法崩潰;
- 問(wèn)題2:假設(shè)sub2(子類2)沒(méi)有實(shí)現(xiàn)printObj,但它的實(shí)例對(duì)象也調(diào)用了selector(printObj),正常應(yīng)該是能夠調(diào)用父類的imp(printObj)方法,但是由于被交換,會(huì)嘗試調(diào)用[sub2 swiprintObj],因?yàn)?swiprintObj的實(shí)現(xiàn)里面有
[self swiprintObj],這里的self是sub2,sub2和super都沒(méi)有實(shí)現(xiàn)swiprintObj的,所以方法找不到崩潰。 - 問(wèn)題3:sub2子類重寫了printObj,一切正常,sub2實(shí)例對(duì)象調(diào)用正常,但是如果在sub2的imp(printObj)里面調(diào)用super方法還是會(huì)崩潰,原因是selector(printObj)會(huì)嘗試調(diào)用imp(swiprintObj),但是sub2 和 super都沒(méi)有實(shí)現(xiàn)該方法
那么如何避免這種情況呢?
使用class_addMethod方法來(lái)避免。再次優(yōu)化后的結(jié)果:
+ (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);
}
}
分步驟詳細(xì)解析如下:
- class_addMethod 執(zhí)行前
superSel -> superImp
sub1SwiSel -> sub1SwiImp
- class_addMethod 執(zhí)行后,給子類增加了sel,但是對(duì)應(yīng)的imp實(shí)現(xiàn)還是swizzledMethod的imp即交換方法的imp
superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> sub1SwiImp
被交換的方法sub1Sel已經(jīng)指向了交換方法的imp實(shí)現(xiàn),下一步將交換方法的sel 指向被交換方法的imp即可。被交換方法不是沒(méi)有實(shí)現(xiàn)嗎??? 有的,OC繼承關(guān)系,父類的實(shí)現(xiàn)就是它的實(shí)現(xiàn)superImp
- class_replaceMethod,將sub1SwiSel的實(shí)現(xiàn)替換為superImp
superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> superImp
系統(tǒng)在給對(duì)象發(fā)送sel消息時(shí),執(zhí)行sub1SwiImp,sub1SwiImp里面發(fā)送sub1SwiSel,執(zhí)行superImp,完成hook。
我們說(shuō)的給子類新增method,其實(shí)并不是一個(gè)全新的,而是會(huì)共享imp,函數(shù)實(shí)現(xiàn)沒(méi)有新增。這樣的好處是superSel對(duì)應(yīng)的imp沒(méi)有改變,它自己的以及它的其它子類不受影響,完美解決此問(wèn)題;但是繼續(xù)往下看其它問(wèn)題
情況2:父類也沒(méi)有實(shí)現(xiàn)
尷尬了,都沒(méi)有實(shí)現(xiàn)方法,那還交換個(gè)錘子???
先說(shuō)結(jié)果吧,交換函數(shù)執(zhí)行后,方法不會(huì)被交換,但是手動(dòng)調(diào)用下面這些,同樣會(huì)死循環(huán)。
- (void)swiprintObj {
NSLog(@"swi1:%@",self);
[self swiprintObj];
}
所以我們要加判斷,然后返回給方法調(diào)用者一個(gè)bool值,或者更直接一點(diǎn),拋出異常。
/// 交換類方法的注意獲取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 上面已經(jīng)算是比較完美了,但是并沒(méi)有完美,主要是場(chǎng)景不同,情況就不同。我們只有理解原理,不同場(chǎng)景不同對(duì)待。
新建類來(lái)交換系統(tǒng)方法
上面說(shuō)的都是在分類里面實(shí)現(xiàn)交換方法,這里新建"私有類"來(lái)交換系統(tǒng)方法。
在寫SDK時(shí),分類有重名覆蓋問(wèn)題,編譯選項(xiàng)還要加-ObjC。出問(wèn)題編譯階段還查不出來(lái)。那么我們可以用新建一個(gè)私有類實(shí)現(xiàn)交換,類重名則直接編譯報(bào)錯(cuò)。交換方法和上面的分類交換稍不一樣
比如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 {
// 不能調(diào)用,這里的self是UIViewController類或者子類的實(shí)例,調(diào)用test的話直接崩潰?;蛘咦鲱愋团袛?[self isKindOfClass:[SwizzleClassTest class]],然后再調(diào)用
// [self test];
[self swi_viewDidLoad];
}
- (void)test {
NSLog(@"Do not do this");
}
@end
這里也用到class_addMethod,給UIViewController新增了一個(gè)swi_viewDidLoad sel及其imp實(shí)現(xiàn),共享了SwizzleClassTest 的imp實(shí)現(xiàn)。
另外系統(tǒng)發(fā)送viewdidload消息進(jìn)而調(diào)用swi_viewDidLoad方法,里面的self是UIViewController,所以不能再[self test],否則崩潰。也不能在其它地方手動(dòng)[self swi_viewDidLoad];會(huì)死循環(huán),因?yàn)檫@時(shí)候self是SwizzleClassTest,而它的method是沒(méi)有被交換的,好處是我們可以通過(guò)self的類型判斷來(lái)避免。
可以比較下交換前后,
交換前:
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 沒(méi)有受影響,映射關(guān)系不變。
這種想取消的話,也很簡(jiǎn)單method_exchangeImplementations
最后補(bǔ)充一點(diǎn):C函數(shù) 實(shí)現(xiàn)交換
這里講的是用C函數(shù)交換系統(tǒng)類的方法。而不是fishhook的hook C的函數(shù),目標(biāo)不一樣。原理也不一樣
還以hook UIViewController的viewDidLoad為例
上面說(shuō)到,oc方法調(diào)用會(huì)轉(zhuǎn)換為objc_msgSend(self,_cmd,param)這種形式,這里再補(bǔ)充一點(diǎn),objc_msgSend找到imp函數(shù)指針后,最終會(huì)是imp(self,_cmd,param)調(diào)用C函數(shù),imp其實(shí)就是個(gè)C函數(shù)指針。
那么我們可以定義一個(gè)C函數(shù),讓sel和我們新建的C函數(shù)(imp)形成映射。另外還需要記錄之前的imp實(shí)現(xiàn),可以定義一個(gè)函數(shù)指針來(lái)保存sel之前的imp實(shí)現(xiàn);大概示意:
之前:
pOriImp = NULL
vcSel -> vcImp
Cfun(){if(pOriImp) pOriImp()}
之后:
pOriImp = vcImp;
vcSel -> cFun;// 函數(shù)名即為函數(shù)指針
詳細(xì)如下:
/// 準(zhǔn)備1. 定義一個(gè)函數(shù)指針,用于記錄系統(tǒng)原本的IMP實(shí)現(xiàn),并初始化為NULL
void (*origin_test_viewDidload)(id,SEL) = NULL;
/// 準(zhǔn)備2. 定義要交換的函數(shù),里面會(huì)調(diào)用系統(tǒng)的IMP
static void swizzle_test_viewDidload(id self, SEL _cmd)
{
// 這里打印的self為UIViewController或者子類實(shí)例
NSLog(@"%@",self);
if (origin_test_viewDidload) {
origin_test_viewDidload(self, _cmd);
}
}
/// 開始交換。startHook可以是某個(gè)類的方法或?qū)嵗椒ɑ駽函數(shù)都可以
+ (void)startHook {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class target = [UIViewController class];
SEL oriSel = @selector(viewDidLoad);
// 要交換的函數(shù)
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,沒(méi)有給類的MethodList新增Method,只是替換了實(shí)現(xiàn),對(duì)原類改動(dòng)最小。
和其它hook方式一樣,這種對(duì)第三方庫(kù) 的hook,也是不影響。如果第三方庫(kù)也交換了,均會(huì)得到調(diào)用
最后,如果你想取消hook,很簡(jiǎn)單,method_setImplementation為原來(lái)的IMP即可。記著把origin_test_viewDidload也置為NULL.
總結(jié)
- 首先要知道方法交換的原理;
- 熟悉它常用接口;
- 被交換方法不存在引發(fā)的 父類、子類問(wèn)題;
- 以及oc中方法的繼承、“覆蓋”問(wèn)題;
- 可能引發(fā)重復(fù)交換的問(wèn)題,以及后果;
- 理解self只是個(gè)隱藏參數(shù),并不一定是當(dāng)前方法所在的類的實(shí)例對(duì)象
最后,大概三類hook,至于想用哪種,其實(shí)無(wú)所謂了,看具體場(chǎng)景。但是原理一定要清楚,每次hook時(shí),都要認(rèn)真推演一遍,計(jì)算下可能產(chǎn)生的影響。