GitHub项目地址
前言
最近业务需要加入一大批埋点统计事件,这个页面添加一点代码那个页面添加一点代码,各个页面内耦合了大量的无关业务的埋点代码使得页面杂乱不堪,所以想寻找一个比较好的方法来解决这个事情。
探索
经过一番考虑想到如下方案:
1、每个业务页面添加一个埋点类,单独将埋点的方法提取到这个类中。
2、利用runtime在底层进行方法拦截,从而添加埋点代码。
最后采用了第2种方案。
技术原理
一、Method-Swizzling
oc中的方法调用其实是向一个对象发送消息 ,利用oc的动态性可以实现方法的交换。
1、用 method_exchangeImplementations 方法来交换2个方法中的IMP
2、用 class_replaceMethod 方法来替换类的方法,
3、用 method_setImplementation 方法来直接设置某个方法的IMP
二、Target-Action
按钮的点击事件,UIControl会调用sendAction:to:forEvent:来将行为消息转发到UIApplication,再由UIApplication调用其sendAction:to:fromSender:forEvent:方法来将消息分发到指定的target上。
分析及实现
一、 需要添加埋点统计的地方:
1、button相关的点击事件
2、页面进入、页面推出
3、tableView的点击
4、collectionView的点击
5、手势相关事件
二、分析
1、对于用户交互的操作,我们使用runtime 对应的方法hook 下sendAction:to:forEvent:便可以得到进行的交互操作。
这个方法对UIControl及继承UIControl的子类对象有效,如:UIButton、UISlider等。
2、对于UIViewController,hook下ViewDidAppear:这个方法知道哪个页面显示了就足够了。
3、对于tableview及collectionview,我们hook下setDelegate:方法。检测其有没有实现对应的点击代理,因为tableView:didSelectRowAtIndexPath:及collectionView:didSelectItemAtIndexPath:是option的不是必须要实现的。
4、对于手势,我们在创建的时候进行hook,方法为initWithTarget:action:。
三、代码实现
1、UIControl+Track
@implementation UIControl (Track)
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzingSelector = @selector(dk_sendAction:to:forEvent:);
[DKMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
- (void)dk_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[self dk_sendAction:action to:target forEvent:event];
//埋点实现区域====
}
@end
2、UIViewController+Track
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalDidLoadSel = @selector(viewDidLoad);
SEL swizzingDidLoadSel = @selector(dk_viewDidLoad);
[DKMethodSwizzingTool swizzingForClass:[self class] originalSel:originalDidLoadSel swizzingSel:swizzingDidLoadSel];
});
}
- (void)dk_viewDidLoad {
[self dk_viewDidLoad];
//埋点实现区域====
}
3、UITableView+Track
@implementation UITableView (Track)
+ (void)initialize {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(setDelegate:);
SEL swizzingSelector = @selector(dk_setDelegate:);
[DKMethodSwizzingTool swizzingForClass:[self class] originalSel:originalSelector swizzingSel:swizzingSelector];
});
}
- (void)dk_setDelegate:(id)delegate {
[self dk_setDelegate:delegate];
SEL originalSel = @selector(tableView:didSelectRowAtIndexPath:);
SEL swizzingSel = NSSelectorFromString([NSString stringWithFormat:@"%@/%@", NSStringFromClass([delegate class]),@(self.tag)]);
//didSelectRowAtIndexPath不一定要实现,未实现在跳过
if (![DKMethodSwizzingTool isContainSel:originalSel class:[delegate class]]) {
return;
}
BOOL addMethod = class_addMethod([delegate class], swizzingSel, method_getImplementation(class_getInstanceMethod([self class], @selector(dk_tableView:didSelectRowAtIndexPath:))), nil);
if (addMethod) {
Method originalMetod = class_getInstanceMethod([delegate class], originalSel);
Method swizzingMethod = class_getInstanceMethod([delegate class], swizzingSel);
method_exchangeImplementations(originalMetod, swizzingMethod);
}
}
- (void)dk_tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = [NSString stringWithFormat:@"%@/%@", NSStringFromClass([self class]),@(tableView.tag)];
SEL sel = NSSelectorFromString(identifier);
if ([self respondsToSelector:sel]) {
IMP imp = [self methodForSelector:sel];
void (*func)(id, SEL,id,id) = (void *)imp;
func(self, sel,tableView,indexPath);
}
//埋点实现区域====
}
4、UICollectionView+Track同时拓展
5、UIGestureRecognizer+Track
结果
2019-05-07 15:29:57.725041+0800 DKDataTrackKitDemo[18913:1357822] eventName:button----eventParam:{
content = dictionary;
text = hahha;
tips = test;
}
2019-05-07 15:29:57.735695+0800 DKDataTrackKitDemo[18913:1357822] eventName:ViewController----eventParam:{
}
2019-05-07 15:29:59.830922+0800 DKDataTrackKitDemo[18913:1357822] eventName:tableView----eventParam:{
text = tableview;
}
2019-05-07 15:30:01.178838+0800 DKDataTrackKitDemo[18913:1357822] eventName:collectionview----eventParam:{
text = collectionView;
}
规则
其中用到的plist生成规则:
1、Action:
对应的是UIControl。
每一个Action统计事件的匹配规则:页面名称/方法名/tag
参数:EventName事件名、EventParam事件对应的参数
2、TableView
对应的是UITableView。
每一个TableView统计事件的匹配规则:页面名称/tag
参数:viewcontroller是否从viewcontroller中取参数、 EventName事件名、EventParam事件对应的参数
3、UICollectionView
规则同上。
4、UIGestureRecognizer
对应的是手势UIGestureRecognizer。
每一个UIGestureRecognizer统计事件的匹配规则:页面名称/方法名
参数:EventName事件名、EventParam事件对应的参数
5、UIViewController
规则同上。
写在最后
hook方式非常强大,几乎可以拦截你想要的全部方法,但是每次触发hook必然会置换IMP的整个过程,频繁的置换会造成资源的消耗,不到万不得已,建议少用。
GitHub项目地址
参考感谢:
https://blog.csdn.net/SandyLoo/article/details/81202105