最近鄙人在项目中接入了阿里云的移动数据分析功能,这个移动数据分析SDK中提供了统计页面出现与页面消失的接口,所以呢我就给UIViewController建了一个分类,然后在分类中复写load方法,并在该方法中勾住ViewController的 viewDidAppear 和 viewDidDisappear 这两个方法,并在勾住之后补充调用阿里云统计对应的接口。代码如下:
#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 这两个方法,如下:
#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方法。
如果某类在原类中有复写该方法,在分类中也复写了该方法,那么原类中的方法实现就会被分类中的方法实现覆盖。
如果多个分类中都复写了该方法,那么程序就会执行最后一个加载到程序中的分类中的方法。
场景介绍
接下来我们看几个场景,首先来看第一个场景(用于交换的方法名相同):
(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];}
此时交换了两次,方法实现被交换了回来,相当于没有交换。这很容易理解,接下来我们看看第二个场景(用于交换的方法名不同):
分类①中: (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。解释完毕。
接下来我们来看一个更加复杂的场景,该场景有三个分类,并且分类中用于交换的方法名都相同:
分类①中: (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。
总结:如果用于交换的方法名相同,当一个类中的方法被交换偶数次的时候,交换无效;当被交换奇数次的时候,最后执行的那个交换是有效的,其他的交换都无效。
代码规范
方法交换的时候,所要交换的方法命名必须关联业务,不要使用普世命名;并且在确定命名之后全局搜索一下该方法名,确保唯一。比如下面的例子:
(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。
以上