iOS 无侵入埋点方案探索

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事件对应的参数

iOS 无侵入埋点方案探索_第1张图片
屏幕快照 2019-05-07 下午4.34.05.png

2、TableView

对应的是UITableView。
每一个TableView统计事件的匹配规则:页面名称/tag
参数:viewcontroller是否从viewcontroller中取参数、 EventName事件名、EventParam事件对应的参数

iOS 无侵入埋点方案探索_第2张图片
屏幕快照 2019-05-07 下午4.39.48.png

3、UICollectionView

规则同上。

4、UIGestureRecognizer

对应的是手势UIGestureRecognizer。
每一个UIGestureRecognizer统计事件的匹配规则:页面名称/方法名
参数:EventName事件名、EventParam事件对应的参数

屏幕快照 2019-05-07 下午4.45.02.png
5、UIViewController

规则同上。

写在最后

hook方式非常强大,几乎可以拦截你想要的全部方法,但是每次触发hook必然会置换IMP的整个过程,频繁的置换会造成资源的消耗,不到万不得已,建议少用。

GitHub项目地址

参考感谢:
https://blog.csdn.net/SandyLoo/article/details/81202105

你可能感兴趣的:(iOS 无侵入埋点方案探索)