Method Swizzle 是 Objc Runtime 提供的几个黑科技之一, 它能够让我们在运行时替换已有方法来实现我们的一些需求。 但它在使用中也有一些需要注意的地方, 咱们来聊聊。
相信有一些开发经验的同学,都用到过 Objc Runtime
的 Method Swizzle
。 它的应用场景也有很多,其中比较典型的一个场景就是进行一些非侵入性的能力注入。 这么说可能不够直观, 下面就用一个实际例子说明这个问题。 AFNetworking
大家应该比较熟悉。 这是它里面的一段代码:
static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getInstanceMethod(theClass, originalSelector); Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector); method_exchangeImplementations(originalMethod, swizzledMethod); } static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) { return class_addMethod(theClass, selector, method_getImplementation(method), method_getTypeEncoding(method)); } + (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass { Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume)); Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend)); if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) { af_swizzleSelector(theClass, @selector(resume), @selector(af_resume)); } if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) { af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend)); } }
这是 AFNetworking
对 NSURLSessionTask
的一个 swizzle 替换。 af_swizzleSelector
和 af_addMethod
这两个方法是对 swizzle 函数调用做了个封装。 主逻辑在 swizzleResumeAndSuspendMethodForClass 方法。 这个方法做的事情就是将 NSURLSessionTask
的 resume
和 suspend
方法做了替换。 替换的目的也很简单, 就是在这两个方法调用的时候发送通知。
首先调用 class_getInstanceMethod
得到我们自己的实例方法 afResumeMethod
和 afSuspendMethod
。 然后调用 af_addMethod
尝试将我们的实例方法添加到 NSURLSessionTask
中(注:这里的 theClass 在实际运行时,就是 [NSURLSessionTask class])。
如果是第一次执行, af_addMethod
就会返回 YES, 然后分别将 af_resume 和 af_suspend 这两个 Selector 添加到 theClass 方法列表中。 添加好方法后,再调用 af_swizzleSelector
方法, 分别将 af_resume 和 resume, 以及 af_suspend 和 suspend 的方法实现进行互换。
这样,我们在调用 [NSURLSessionTask resume] 的时候, 其实调用的是 [NSURLSessionTask af_resume], 就是这么个情况~
af_swizzleSelector
方法中,其实是 Runtime
的 method_exchangeImplementations
函数的一个封装。 这也是大家常用的一个 swizzle
函数, 但正是它,会带来一些副作用, 这个也是我们后面要讨论的主题。 先记住它吧。
上面咱们演示了一个 Runtime Swizzle 的整体流程。 可能有一部分同学在使用 Swizzle 的时候,会用到 method_exchangeImplementations
方法。 刚才我也提到了,它会有一些副作用, 咱们继续来看看吧。
我们还是按照同样的方式进行方法替换:
@implementation MyObject - (int) my_quantity { return 12; } - (void)main { SKPayment *payment = [[SKPayment alloc] init]; NSLog(@"payment %i", payment.quantity); //输出:1 Method myQuantity = class_getInstanceMethod([self class], @selector(my_quantity)); Method originalQuantity = class_getInstanceMethod([payment class], @selector(quantity)); method_exchangeImplementations(myQuantity, originalQuantity); NSLog(@"replaced %i", (int)payment.quantity); //输出: 12 } @end
我们这里将我们自己的 my_quantity
方法与 [SKPayment quantity]
进行替换, 并且两次使用 NSLog 进行输出。 这次我们两次 NSLog 都得到了预期的结果。 在替换方法之前 payment.quantity
输出的是 1。 在替换之后,输出的是 my_quantity
的 12。
到此为止,看起来都没有任何问题。 但是如果在方法替换后, 我们显示的调用 my_quantity
就有可能有问题了:
NSLog(@"original %i", [self my_quantity]);
大家想想, 这时候这个方法调用会输出什么结果呢? 肯定不是 12, 因为它的方法实现已经和 SKPayment 中的交换了。 那么是 1 吗?
在我实际运行中, 既不是 12 也不是 1。 而是程序执行到这里直接 Crash 了。 这时为什么呢?
我们不妨将 my_quantity 稍微修改一下:
- (int) my_quantity { NSLog(@"%@", self); return 12; }
这里我们用 NSLog 输出了 self 的内容。 在调用这行代码的时候:
//输出 <SKPayment: 0x60000001e9b0> NSLog(@"replaced %i", (int)payment.quantity); //输出: 12
命令行中还输出了 <SKPayment: 0x60000001e9b0>
。 这个是我们刚刚加入的 NSLog 在起作用。 为什么这时候的 self
变成了 SKPayment
呢?
这就是 objc Runtime 的消息机制的原理。 简单来说,我们调用任何方法,在 runtime 时候, 都会被转换成 objc_msgSend() 调用。 我们上面的代码, 在运行时其实就是这样:
objc_msgSend(payment, @selector(quantity))
而大家知道,我们传入的 @selector(quantity) 已经被刚才的 Swizzle 替换成了 @selector(my_quantity), 这个好理解。 但还有一点要强调, 就是每个方法中对 self 的引用, 其实引用的就是 objc_msgSend 的第一个参数。
也就是说,虽然我们的 Selector 被 Swizzle 过程替换掉了, 但 self 实例是没有替换过来的。 这点对于我们的 my_quantity
的实现不会有影响, 因为 my_quantity
方法里面只是简单的返回了一个数字而已。
但对于 SKPayment 对应的 quantity
方法的实现就有可能有问题了。 因为 [SKPayment quantity]
的实现会认为 self 是一个 SKPayment 实例, 但我们是以这个方式调用的:
NSLog(@"original %i", [self my_quantity]);
在运行时, 它会被转换成这样:
objc_msgSend(MyObject, @selector(my_quantity))
还是因为 @selector(my_quantity) 和 @selector(quantity) 被 Swizzle 了, 所以我们这次实际调用的方法是 [SKPayment quantity]
。 但 objc_msgSend 传入的第一个参数是我们自己的 MyObject 实例, 而不是 SKPayment 的实例。
也就是说, 虽然我们通过 Swizzle 将方法调用映射到了 [SKPayment quantity]
上, 但我们给他的 self 实例是不对的。 就会产生这种非预期的结果了。
总结一下, method_exchangeImplementations
来达成的 Swizzle, 会有双向效果。 除了我们的目标方法, 还需要注意我们自己被替换的方法的安全性。 否则就非常容易出现这种意料之外的结果。
刚才说了 method_exchangeImplementations
的一些弊端之后, 咱们再来看看是不是有其他的替代方案呢? 答案是肯定的。 Runtime 还提供了另一种 Swizzle 函数 method_setImplementation
。
还是以刚才实例来进行:
int my_quantity(id self, SEL _cmd) { return 12; } - (void)viewDidLoad { [super viewDidLoad]; SKPayment *payment = [[SKPayment alloc] init]; NSLog(@"payment %i", payment.quantity); // 输出 1 Method originalQuantity = class_getInstanceMethod([payment class], @selector(quantity)); method_setImplementation(originalQuantity, (IMP) my_quantity); NSLog(@"replaced %i", (int)payment.quantity); //输出 12 }
这次我们把 my_quantity
定义成了 C 函数。 method_setImplementation
接受两个参数,第一个还是我们要替换的方法。 而第二个参数是一个 IMP 类型的。 其实 IMP 就是一个 C 函数了。 我们定义的 my_quantity
接受两个参数, self 和 _cmd。 这两个参数是 Runtime 消息转发传递进来的。
method_setImplementation
可以让我们提供一个新的函数来代替我们要替换的方法。 而不是将两个方法的实现做交换。 这样就不会造成 method_exchangeImplementations
的潜在对已有实现的副作用了。
不知道大家是否注意到过 method_exchangeImplementations
所带来的这个副作用。这种问题如果发生,调试起来会非常困难。 至少这次了解了之后, 就可以帮你减少很多潜在的隐患, 帮你节约调试问题的时间。 当然,大家如果对 Swizzle 相关的几个方法有任何的补充,也欢迎在留言中写出,一起分享相关知识。
如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~