Objc 黑科技 - Method Swizzle 的一些注意事项



- 作者: SwiftCafe


Method Swizzle 是 Objc Runtime 提供的几个黑科技之一, 它能够让我们在运行时替换已有方法来实现我们的一些需求。 但它在使用中也有一些需要注意的地方, 咱们来聊聊。

Method Swizzle 黑科技

相信有一些开发经验的同学,都用到过 Objc RuntimeMethod Swizzle。 它的应用场景也有很多,其中比较典型的一个场景就是进行一些非侵入性的能力注入。 这么说可能不够直观, 下面就用一个实际例子说明这个问题。 AFNetworking 大家应该比较熟悉。 这是它里面的一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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));
}
}

这是 AFNetworkingNSURLSessionTask 的一个 swizzle 替换。 af_swizzleSelectoraf_addMethod 这两个方法是对 swizzle 函数调用做了个封装。 主逻辑在 swizzleResumeAndSuspendMethodForClass 方法。 这个方法做的事情就是将 NSURLSessionTaskresumesuspend 方法做了替换。 替换的目的也很简单, 就是在这两个方法调用的时候发送通知。

首先调用 class_getInstanceMethod 得到我们自己的实例方法 afResumeMethodafSuspendMethod。 然后调用 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 方法中,其实是 Runtimemethod_exchangeImplementations 函数的一个封装。 这也是大家常用的一个 swizzle 函数, 但正是它,会带来一些副作用, 这个也是我们后面要讨论的主题。 先记住它吧。

容易被忽略的副作用

上面咱们演示了一个 Runtime Swizzle 的整体流程。 可能有一部分同学在使用 Swizzle 的时候,会用到 method_exchangeImplementations 方法。 刚才我也提到了,它会有一些副作用, 咱们继续来看看吧。

我们还是按照同样的方式进行方法替换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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 就有可能有问题了:

1
NSLog(@"original %i", [self my_quantity]);

大家想想, 这时候这个方法调用会输出什么结果呢? 肯定不是 12, 因为它的方法实现已经和 SKPayment 中的交换了。 那么是 1 吗?

在我实际运行中, 既不是 12 也不是 1。 而是程序执行到这里直接 Crash 了。 这时为什么呢?

我们不妨将 my_quantity 稍微修改一下:

1
2
3
4
5
6
- (int) my_quantity {
NSLog(@"%@", self);
return 12;
}

这里我们用 NSLog 输出了 self 的内容。 在调用这行代码的时候:

1
2
//输出 <SKPayment: 0x60000001e9b0>
NSLog(@"replaced %i", (int)payment.quantity); //输出: 12

命令行中还输出了 <SKPayment: 0x60000001e9b0>。 这个是我们刚刚加入的 NSLog 在起作用。 为什么这时候的 self 变成了 SKPayment 呢?

这就是 objc Runtime 的消息机制的原理。 简单来说,我们调用任何方法,在 runtime 时候, 都会被转换成 objc_msgSend() 调用。 我们上面的代码, 在运行时其实就是这样:

1
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 实例, 但我们是以这个方式调用的:

1
NSLog(@"original %i", [self my_quantity]);

在运行时, 它会被转换成这样:

1
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

还是以刚才实例来进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。




微信公众平台
更多精彩内容,请关注微信公众号


公众号:swift-cafe
邮件订阅
请输入您的邮箱,我们会把最新的内容推送给您: