经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 移动开发 » Swift » 查看文章
OC方法交换swizzle详细介绍——不再有盲点
来源:cnblogs  作者:时间已静止  时间:2019/6/29 1:58:08  对本文有异议

原文链接: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函数入口地址,完成方法调用。

  1. typedef struct objc_method *Method;
  2. // oc2.0已废弃,可以作为参考
  3. struct objc_method {
  4. SEL _Nonnull method_name;
  5. char * _Nullable method_types;
  6. IMP _Nonnull method_imp;
  7. }

基于以上铺垫,那么有两种办法可以完成交换:

  • 一种是改变@selfector(test),不太现实,因为我们一般都是hook系统方法,我们拿不到系统源码,不能修改。即便是我们自己代码拿到源码修改那也是编译期的事情,并非运行时(跑题了。。。)
  • 所以我们一般修改imp函数指针。改变sel与imp的映射关系;

系统为我们提供的接口

typedef struct objc_method *Method;Method是一个不透明指针,我们不能够通过结构体指针的方式来访问它的成员,只能通过暴露的接口来操作。

接口如下,很简单,一目了然:

  1. #import <objc/runtime.h>
  2. /// 根据cls和sel获取实例Method
  3. Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
  4. /// 给cls新增方法,需要提供结构体的三个成员,如果已经存在则返回NO,不存在则新增并返回成功
  5. BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
  6. const char * _Nullable types)
  7. /// method->imp
  8. IMP _Nonnull method_getImplementation(Method _Nonnull m);
  9. /// 替换
  10. IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
  11. const char * _Nullable types)
  12. /// 跟定两个method,交换它们的imp:这个好像就是我们想要的
  13. method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

简单使用

假设交换UIViewController的viewDidLoad方法

  1. /// UIViewController 某个分类
  2. + (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
  3. Method originMethod = class_getInstanceMethod(target, originalSelector);
  4. Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
  5. method_exchangeImplementations(originMethod, swizzledMethod);
  6. }
  7. + (void)load {
  8. [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
  9. }
  10. /// hook
  11. - (void)swizzle_viewDidLoad {
  12. [self swizzle_viewDidLoad];
  13. }

交换本身简单:原理简单,接口方法也少而且好理解,因为结构体定义也就三个成员变量,也难不到哪里去!

但是,具体到使用场景,叠加上其它外部的不稳定因素,想要稳定的写出通用或者半通用交换方法,上面的"简单使用"远远不够的。

下面就详细介绍下几种常见坑,也是为啥网上已有很多文章介绍方法交换,为什么还要再写一篇的原因:不再有盲点

常见问题一、被多次调用(多次交换)

"简单使用"中的代码用于hook viewDidload一般是没问题的,+load 方法一般也执行一次。但是如果一些程序员写法不规范时,会造成多次调用。

比如写了UIViewController的子类,在子类里面实现+load方法,又习惯性的调用了super方法

  1. + (void)load {
  2. // 这里会引起UIViewController父类load方法多次调用
  3. [super load];
  4. }

为了没盲点,我们扩展下load的调用:

  • load方法的调用时机在dyld映射image时期,这也符合逻辑,加载完调用load。
  • 类与类之间的调用顺序与编译顺序有关,先编译的优先调用,继承层次上的调用顺序则是先父类再子类;
  • 类与分类的调用顺序是,优先调用类,然后是分类;
  • 分类之间的顺序,与编译顺序有关,优先编译的先调用;
  • 系统的调用是直接拿到imp调用,没有走消息机制;

手动的[super load]则走的是消息机制,分类的会优先调用,如果你运气好,另外一个程序员也实现了UIViewController的分类,且实现+load方法,还后编译,则你的load方法也只执行一次;(分类方法后编译的会“覆盖”之前的)

为了保险起见,还是:

  1. + (void)load {
  2. static dispatch_once_t onceToken;
  3. dispatch_once(&onceToken, ^{
  4. [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
  5. });
  6. }

继续扩展:多次调用的副作用是什么呢?

  • 根据原理,如果是偶数次

结果就是方法交换不生效,但是有遗留问题,这时手动调用

  1. - (void)swizzle_viewDidLoad {
  2. [self swizzle_viewDidLoad];
  3. }

会引起死循环。

其实,方法交换后,任何时候都不要尝试手动调用,特别是交换的系统方法。

  • 奇数次调用

奇数次之后一切正常。但是,奇数次之前,它会先经历偶数次。

比如,第一次交换,正常,第二次交换,那么相当于没有交换,如果你手动调用了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

没毛病!

问题二、被交换的类没有实现该方法

我们还是在分类里面添加方法来交换

情况一:父类实现了被交换方法

我们本意交换的是子类方法,但是子类没有实现,父类实现了class_getInstanceMethod(target, swizzledSelector);执行的结果返回父类的Method,那么后续交换就相当于和父类的方法实现了交换。

一般情况下也不会出问题,可是埋下了一系列隐患。如果其它程序员也继承了这个父类。举例代码如下

  1. /// 父类
  2. @interface SuperClassTest : NSObject
  3. - (void)printObj;
  4. @end
  5. @implementation SuperClassTest
  6. - (void)printObj {
  7. NSLog(@"SuperClassTest");
  8. }
  9. @end
  10. /// 子类1
  11. @interface SubclassTest1 : SuperClassTest
  12. @end
  13. @implementation SubclassTest1
  14. - (void)printObj {
  15. NSLog(@"printObj");
  16. }
  17. @end
  18. /// 子类2
  19. @interface SubclassTest2 : SuperClassTest
  20. @end
  21. @implementation SubclassTest2
  22. /// 有没有重写此方法,会呈现不同的结果
  23. - (void)printObj {
  24. // 有没有调用super 也是不同的结果
  25. [super printObj];
  26. NSLog(@"printObj");
  27. }
  28. @end
  29. /// 子类1 分类实现交换
  30. + (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
  31. Method originMethod = class_getInstanceMethod(target, originalSelector);
  32. Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
  33. method_exchangeImplementations(originMethod, swizzledMethod);
  34. }
  35. + (void)load {
  36. static dispatch_once_t onceToken;
  37. dispatch_once(&onceToken, ^{
  38. [self swizzleInstanceMethod:[SubclassTest1 class] original:@selector(printObj) swizzled:@selector(swiprintObj)];
  39. });
  40. }
  41. - (void)swiprintObj {
  42. NSLog(@"swi1:%@",self);
  43. [self swiprintObj];
  44. }

示例代码,实现了printObjswiprintObj的交换。

  • 问题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方法来避免。再次优化后的结果:

  1. + (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
  2. Method originMethod = class_getInstanceMethod(target, originalSelector);
  3. Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
  4. if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
  5. class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
  6. }
  7. else {
  8. method_exchangeImplementations(originMethod, swizzledMethod);
  9. }
  10. }

分步骤详细解析如下:

  • 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:父类也没有实现

尴尬了,都没有实现方法,那还交换个锤子???

先说结果吧,交换函数执行后,方法不会被交换,但是手动调用下面这些,同样会死循环。

  1. - (void)swiprintObj {
  2. NSLog(@"swi1:%@",self);
  3. [self swiprintObj];
  4. }

所以我们要加判断,然后返回给方法调用者一个bool值,或者更直接一点,抛出异常。

  1. /// 交换类方法的注意获取meta class, object_getClass。class_getClassMethod
  2. + (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
  3. Method originMethod = class_getInstanceMethod(target, originalSelector);
  4. Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
  5. if (originMethod && swizzledMethod) {
  6. if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
  7. class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
  8. }
  9. else {
  10. method_exchangeImplementations(originMethod, swizzledMethod);
  11. }
  12. }
  13. else {
  14. @throw @"originalSelector does not exit";
  15. }
  16. }

再加上 dispatch_once 上面已经算是比较完美了,但是并没有完美,主要是场景不同,情况就不同。我们只有理解原理,不同场景不同对待。

最后补充一点:新建类来交换系统方法

上面说的都是在分类里面实现交换方法,这里新建"私有类"来交换系统方法。

在写SDK时,分类有重名覆盖问题,编译选项还要加-ObjC。出问题编译阶段还查不出来。那么我们可以用新建一个私有类实现交换,类重名则直接编译报错。交换方法和上面的分类交换稍不一样

比如hook viewDidload,代码如下:

  1. @interface SwizzleClassTest : NSObject
  2. @end
  3. @implementation SwizzleClassTest
  4. + (void)load {
  5. /// 私有类,可以不用dispatch_once
  6. Class target = [UIViewController class];
  7. Method swiMethod = class_getInstanceMethod(self, @selector(swi_viewDidLoad));
  8. Method oriMethod = class_getInstanceMethod(target, @selector(viewDidLoad));
  9. if (swiMethod && oriMethod) {
  10. if (class_addMethod(target, @selector(swi_viewDidLoad), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))) {
  11. // 这里获取给UIViewController新增的method
  12. swiMethod = class_getInstanceMethod(target, @selector(swi_viewDidLoad));
  13. method_exchangeImplementations(oriMethod, swiMethod);
  14. }
  15. }
  16. }
  17. - (void)swi_viewDidLoad {
  18. // 不能调用,这里的self是UIViewController类或者子类的实例,调用test的话直接崩溃。或者做类型判断 [self isKindOfClass:[SwizzleClassTest class]],然后再调用
  19. // [self test];
  20. [self swi_viewDidLoad];
  21. }
  22. - (void)test {
  23. NSLog(@"Do not do this");
  24. }
  25. @end

这里也用到class_addMethod,给UIViewController新增了一个swi_viewDidLoad sel及其imp实现,共享了SwizzleClassTest 的imp实现。

另外系统发送viewdidload消息进而调用swi_viewDidLoad方法,里面的self是UIViewController,所以不能再[self test],否则崩溃。也不能在其它地方手动[self swi_viewDidLoad];会死循环,因为这时候self是SwizzleClassTest,而它的method是没有被交换的。

可以比较下交换前后,

交换前:

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 没有受影响,映射关系不变。

总结

  • 首先要知道方法交换的原理;
  • 熟悉它常用接口;
  • 被交换方法不存在引发的 父类、子类问题;
  • 以及oc中方法的继承、“覆盖”问题;
  • 可能引发重复交换的问题,以及后果;
  • 理解self只是个隐藏参数,并不一定是当前方法所在的类的实例对象

最后,每次使用方法交换时,都要认真推演一遍,计算下可能产生的影响。

原文链接:http://www.cnblogs.com/mddblog/p/11105450.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号