详解iOS Method Swizzling使用陷阱

在阅读团队一项目源码时,发现Method Swizzling的写法有些瑕疵。这篇文章主要就介绍iOS Method Swizzling的正确写法应该是什么样的。

下面是iOS Method Swizzling的一种实现:

  1. + (void)load {  
  2.   Class class = [self class];  
  3.   
  4.   SEL fromSelector = @selector(func);  
  5.   SEL toSelector = @selector(easeapi_func);  
  6.   
  7.   Method fromMethod = class_getInstanceMethod(class, fromSelector);  
  8.   Method toMethod = class_getInstanceMethod(class, toSelector);  
  9.   
  10.   method_exchangeImplementations(fromMethod, toMethod);  
  11. }  

这种写法在一些时候能正常工作,但实际上有些问题。那么问题在哪里呢?

一个例子

为了说明这个问题,我们先来假设一个场景:

  1. @interface Father: NSObject  
  2. -(void)easeapi;  
  3. @end  
  4. @implementation Father  
  5. -(void)easeapi {  
  6.   //your code  
  7. }  
  8. @end  
  9.   
  10. //Son1继承自Father  
  11. @interface Son1: Father  
  12. @end  
  13. @implementation Son1  
  14. @end  
  15.   
  16. //Son2继承自Father,并HOOK了easeapi方法。  
  17. @interface Son2: Father  
  18. @end  
  19. @implementation Son2  
  20. + (void)load {  
  21.   Class class = [self class];  
  22.   
  23.   SEL fromSelector = @selector(easeapi);  
  24.   SEL toSelector = @selector(new_easeapi);  
  25.   
  26.   Method fromMethod = class_getInstanceMethod(class, fromSelector);  
  27.   Method toMethod = class_getInstanceMethod(class, toSelector);  
  28.   
  29.   method_exchangeImplementations(fromMethod, toMethod);  
  30. }  
  31. -(void)new_easeapi {  
  32.   [self new_easeapi];  
  33.   //your code  
  34. }  
  35. @end  

看样子没什么问题,Son2的方法也交换成功,但当我们执行[Son1 easeapi]时,发现CRASH了。

'-[Son1 new_easeapi]: unrecognized selector sent to instance 0x600002d701f0''

这就奇怪了,我们HOOK的是Son2的方法,怎么会产生Son1的崩溃?

为什么会发生崩溃

要解释这个问题,还是要回到原理上。

首先明确一点,class_getInstanceMethod会查找父类的实现。

在上例中,easeapi是在Son2的父类Father中实现的,执行method_exchangeImplementations之后,Father的easeapi和Son2的new_easeapi进行了方法交换。

交换之后,当Son1(Father的子类)执行easeapi方法时,会通过「消息查找」找到Father的easeapi方法实现。

重点来了!

由于已经发生了方法交换,实际上执行的是Son2的new_easeapi方法。

  1. -(void)new_easeapi {  
  2.   [self new_easeapi];  
  3.   //your code  
  4. }  

可恶的是,在new_easeapi中执行了[self new_easeapi]。此时这里的self是Son1实例,但Son1及其父类Father中并没有new_easeapi的SEL,找不到对应的SEL,自然就会CRASH。

什么情况下不会有问题?

上面说了:「这种写法在一些时候能正常工作」。那么,到底什么时候直接执行method_exchangeImplementations不会有问题呢?

至少在下面几种场景中都不会有问题:

Son2中有easeapi的实现

在上例中,如果我们在Son2中重写了easeapi方法,执行class_getInstanceMethod(class, fromSelector)获取到的是Son2的easeapi实现,而不是Father的。这样,执行method_exchangeImplementations后,不会影响到Father的实现。

new_easeapi实现改进

  1. - (void) new_easeapi {  
  2.   //[self new_easeapi];//屏蔽掉这句代码  
  3.   //your code  
  4. }  

在这个场景中,由于不会执行[self new_easeapi],也不会有问题。但这样就达不到HOOK的效果。

改进优化

推荐的Method Swizzling实现:

  1. + (void)load {  
  2.   static dispatch_once_t onceToken;  
  3.   dispatch_once(&onceToken, ^{  
  4.     Class class = [self class];  
  5.   
  6.     SEL fromSelector = @selector(easeapi);  
  7.     SEL toSelector = @selector(new_easeapi);  
  8.   
  9.     Method fromMethod = class_getInstanceMethod(class, fromSelector);  
  10.     Method toMethod = class_getInstanceMethod(class, toSelector);  
  11.   
  12.     if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {  
  13.       class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));  
  14.     } else {  
  15.       method_exchangeImplementations(fromMethod, toMethod);  
  16.     }  
  17.   });  
  18. }  

可以看到,至少有两点变化:

dispatch_once
尽管dyld能够保证调用Class的load时是线程安全的,但还是推荐使用dispatch_once做保护,防止极端情况下load被显示强制调用时,重复交换(第一次交换成功,下次又换回来了...),造成逻辑混乱。

增加了class_addMethod判断
class_addMethod & class_replaceMethod
还是从定义上理解。

class_addMethod
给指定Class添加一个SEL的实现(或者说是SEL和指定IMP的绑定),添加成功返回YES,SEL已经存在或添加失败返回NO。

它有两个需要注意的点:

  • 如果该SEL在父类中有实现,则会添加一个覆盖父类的方法;
  • 如果该Class中已经有SEL,则返回NO。

执行class_addMethod能避免干扰到父类,这也是为什么推荐大家尽量先使用class_addMethod的原因。显然易见,因为iOS Runtime消息传递机制的影响,只执行method_exchangeImplementations操作时可能会影响到父类的方法。基于这个原理,如果HOOK的就是本类中实现的方法,那么直接用method_exchangeImplementations也是完全没问题的。

class_replaceMethod

    • 如果该Class不存在指定SEL,则class_replaceMethod的作用就和class_addMethod一样;
    • 如果该Class存在指定的SEL,则class_replaceMethod的作用就和method_setImplementation一样。
posted @ 2020-12-14 17:09  ios_福  阅读(67)  评论(0编辑  收藏  举报