3

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

 3 years ago
source link: http://www.swiftcafe.io/post/swizzle
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Method Swizzle 的一些注意事项

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

swift 发布于 2021年09月10日 Method Swizzle 黑科技

相信有一些开发经验的同学,都用到过 Objc RuntimeMethod 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));
}
}

这是 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 方法。 刚才我也提到了,它会有一些副作用, 咱们继续来看看吧。

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

@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,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
qrcode.jpg 关注微信公众号
发现更多精彩
swift-cafe


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK