前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一个Bug所引发的方法交换小讨论

一个Bug所引发的方法交换小讨论

作者头像
拉维
发布2020-06-05 09:53:16
5890
发布2020-06-05 09:53:16
举报
文章被收录于专栏:iOS小生活

最近鄙人在项目中接入了阿里云的移动数据分析功能,这个移动数据分析SDK中提供了统计页面出现与页面消失的接口,所以呢我就给UIViewController建了一个分类,然后在分类中复写load方法,并在该方法中勾住ViewController的 viewDidAppear 和 viewDidDisappear 这两个方法,并在勾住之后补充调用阿里云统计对应的接口。代码如下:

代码语言:javascript
复制
#import "UIViewController+MobileAnalytics.h"#import "WDAlicloudStatisticsService+MobileAnalytics.h"#import "MKRuntime.h"
@implementation UIViewController (MobileAnalytics)
+ (void)load {  [MKRuntime exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidAppear:) swizzledSel:@selector(wd_viewDidAppear:)];  [MKRuntime exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidDisappear:) swizzledSel:@selector(wd_viewDidDisappear:)];}
#pragma mark - Custom Exchanged Sel
- (void)wd_viewDidAppear:(BOOL)animated {  [self wd_viewDidAppear:animated];  [WDAlicloudStatisticsService mobileAnalyticsPageAppear:self];}
- (void)wd_viewDidDisappear:(BOOL)animated {  [self wd_viewDidDisappear:animated];  [WDAlicloudStatisticsService mobileAnalyticsPageDisappear:self];}
@end

如果说项目中只在这一个地方对ViewController的 viewDidAppear 和 viewDidDisappear 这两个方法进行方法交换,那么不会有任何问题。

但是我的项目中还接入了TalkingData,它在另一个地方也勾住了ViewController的 viewDidAppear 和 viewDidDisappear 这两个方法,如下:

代码语言:javascript
复制
#import "WDStatistService+PageView.h"#import "MKRuntime.h"#import "TalkingData.h"
@implementation WDStatistService (PageView)
@end
@implementation UIViewController (PageStatist)
+ (void)load { [MKRuntime exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidAppear:) swizzledSel:@selector(wd_viewDidAppear:)]; [MKRuntime exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidDisappear:) swizzledSel:@selector(wd_viewDidDisappear:)];}
#pragma mark - Custom Exchanged Sel
- (void)wd_viewDidAppear:(BOOL)animated { [self wd_viewDidAppear:animated]; [TalkingData trackPageBegin:[self pageName]];}
- (void)wd_viewDidDisappear:(BOOL)animated { [self wd_viewDidDisappear:animated]; [TalkingData trackPageEnd:[self pageName]];}
@end

实际上,我们是可以通过类目的方式在多个地方勾住ViewController的 viewDidAppear 和 viewDidDisappear 这两个方法,并进行方法交换的,只要交换的方法名不一样,就不会有任何问题。但是大家可以比较一下我上面发的两段代码,你会发现在两个不同的类目中用于交换的方法是同名的,这就有问题了。

在该例子中,体现出来的问题就是,这两个地方的方法交换都不会起作用。那么为什么不会起作用呢,且听我慢慢道来。

首先我先提出我的一个疑惑。通常而言,对于一个类中的方法,如果在该类的分类中有重写该方法,那么该方法在原类中的实现就会被分类中的实现覆盖;如果一个类中的方法,在该类的多个分类中都有重写,那么最终会执行最后一个加载到内存中的分类中的方法。但是为什么load方法在同一个类的不同分类中重写,在每一个分类中都会被调用呢

+load、+initialize和一般方法的区别

1,+load方法

应用程序在启动的时候就会加载所有的类,就会调用每个类的+load方法。

如果类有分类,分类中覆写了+load方法,那么先调用原类中的+load方法,再调用分类中的+load方法。

如果有多个分类,每个分类中都复写了+load方法,那么先调用原类中的+load方法,再按照文件加载顺序(Build Phases -> Compile Sources中查看,顺序可以手动调节)依次执行分类中的+load方法。

每一个+load方法都会被调用,无论+load方法是在原类中被复写,还是在类别中被复写。

一个类的+load方法会自动调用其父类的+load方法。

具体可以参考:initialize和load的调用时机

2,其他一般的需要手动调用的方法(无论是实例方法还是类方法)

在调用该方法的时候(运行时)查找。

如果分类中有复写该方法,那么原类中的方法实现就会被分类中的方法实现覆盖。

如果多个分类中都复写了原类中的同一个方法,那么程序就会执行最后一个加载到程序中的分类中的方法。

3,+initialize方法

一个类的+initialize方法会在第一次初始化这个类之前被调用,并且只被调用一次。

也就是说,当向该类发送第一个消息的时候,会触发该类的+initialize方法。在应用程序的生命周期内,某个类的+initialize方法最多只会被调用一次。

与+load方法一样,一个类的+initialize方法也会自动调用其父类的+initialize方法。

如果某类在原类中有复写该方法,在分类中也复写了该方法,那么原类中的方法实现就会被分类中的方法实现覆盖。

如果多个分类中都复写了该方法,那么程序就会执行最后一个加载到程序中的分类中的方法。

场景介绍

接下来我们看几个场景,首先来看第一个场景(用于交换的方法名相同):

代码语言:javascript
复制
+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidAppear:) swizzledSel:@selector(mk_viewDidAppear:)];  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidAppear:) swizzledSel:@selector(mk_viewDidAppear:)];}
- (void)mk_viewDidAppear:(BOOL)animated {  [self mk_viewDidAppear:animated];  [MKAlicloudStatisticsService mobileAnalyticsPageAppear:self];}

此时交换了两次,方法实现被交换了回来,相当于没有交换。这很容易理解,接下来我们看看第二个场景(用于交换的方法名不同):

代码语言:javascript
复制
分类①中:+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(play) swizzledSel:@selector(play1)];}
- (void)play1 {  [self play];  [MKAlicloudStatisticsService mobileAnalyticsPageAppear:self];}
分类②中:+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(play) swizzledSel:@selector(play1)];}
- (void)play1 {  [self play];  [MKAlicloudStatisticsService mobileAnalyticsPageAppear:self];}

此时,假设先加载分类①,后加载分类②。

当加载分类①的时候,会将play的方法实现playIMP与play1的方法实现play1IMP_category1进行交换。

所以当加载完分类①之后,play的方法实现是play1IMP_category1,play1的方法实现是playIMP。

当加载分类②的时候,会将play的方法实现play1IMP_category1与play1的方法实现playIMP进行交换。

二者交换之后,play的方法实现回到原始值playIMP,也就相当于没有交换。

也许有人这时会有疑问,当加载到分类②的时候,分类②中paly1的方法实现难道不应该是play1IMP_category2吗,为啥是playIMP啊?且听我解释。在加载分类①的时候,已经将当时play的方法实现与play1的方法实现进行了交换,也就是说,加载完分类①,开始加载分类②的时候,此时play这个SEL所对应的方法实现就是play1IMP_category1,而play1这个SEL所对应的方法实现就是playIMP。解释完毕。

接下来我们来看一个更加复杂的场景,该场景有三个分类,并且分类中用于交换的方法名都相同:

代码语言:javascript
复制
分类①中:+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(play) swizzledSel:@selector(play1)];}
- (void)play1 {  [self play];//分类1Hook实现}
分类②中:+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(play) swizzledSel:@selector(play1)];}
- (void)play1 {  [self play];//分类2Hook实现}
分类③中:+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(play) swizzledSel:@selector(play1)];}
- (void)play1 {  [self play];//分类3Hook实现}

此时,假设先加载分类①,后加载分类②,最后加载分类③。

当加载分类①的时候,会将play的方法实现playIMP与play1的方法实现play1IMP_category1进行交换。

所以当加载完分类①之后,play的方法实现是play1IMP_category1,play1的方法实现是playIMP。

当加载分类②的时候,会将play的方法实现play1IMP_category1与play1的方法实现playIMP进行交换。

二者交换之后,play的方法实现回到原始值playIMP,也就相当于没有交换。

当加载分类③的时候,会将play的方法实现playIMP与play1的方法实现play1IMP_category3进行交换。

二者交换之后,play的方法实现是play1IMP_category3,play1的方法实现是playIMP。

总结:如果用于交换的方法名相同,当一个类中的方法被交换偶数次的时候,交换无效;当被交换奇数次的时候,最后执行的那个交换是有效的,其他的交换都无效。

代码规范

方法交换的时候,所要交换的方法命名必须关联业务,不要使用普世命名;并且在确定命名之后全局搜索一下该方法名,确保唯一。比如下面的例子:

代码语言:javascript
复制
+ (void)load {  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidAppear:) swizzledSel:@selector(mk_viewDidAppear:)];  [MKRuntimeService exchangeInstanceMethodImpForClass:[self class] originalSel:@selector(viewDidDisappear:) swizzledSel:@selector(mk_viewDidDisappear:)];}
#pragma mark - Custom Exchanged Sel
- (void)mk_viewDidAppear:(BOOL)animated {  [self mk_viewDidAppear:animated];  [MKAlicloudStatisticsService mobileAnalyticsPageAppear:self];}
- (void)mk_viewDidDisappear:(BOOL)animated {  [self mk_viewDidDisappear:animated];  [MKAlicloudStatisticsService mobileAnalyticsPageDisappear:self];}
@end

该例中,mk_viewDidAppear和mk_viewDidDisappear就是一种普世命名,可以根据业务改为如下:

mobileAnalytics_viewDidAppear和mobileAnalytics_viewDidDisappear。

以上

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-06-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档