原文连接: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时,都要认真推演一遍,计算下可能产生的影响。