首页 >iOS开发

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

2017-01-05 06:12 编辑: AllanHou 分类:iOS开发 来源:swiftcafe

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

Method Swizzle 黑科技

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

命令行中还输出了 。 这个是我们刚刚加入的 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 相关的几个方法有任何的补充,也欢迎在留言中写出,一起分享相关知识。

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:如何用Xcode8解决多线程问题

相关资讯

我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部