iOS全埋点实践

作者: kurt_wang | 来源:发表于2020-01-09 18:17 被阅读0次

    客户端埋点大概分为三类:

    • 代码埋点
    • 可视化埋点
    • 无埋点

    1、代码埋点


    代码埋点,即在需要埋点的节点调用接口直接上传埋点数据,第三方数据统计服务商也大都提供了代码埋点的 api,非常方便。

    但是带来一个问题,埋点代码散落在业务的各个地方,和业务耦合严重,尤其是在页面改版,业务变动的过程中,旧的埋点不知道怎么处理,新的埋点不知道需不需要,当埋点数量上来之后,对散落的埋点代码的维护是个灾难。
    当然你可以通过宏、工厂类去简化埋点代码,但并不能改变什么

    - (void)viewDidDisappear:(BOOL)animated {
        [super viewDidDisappear:animated];
    
        // 埋点
        APE(kEvent4001);
    }
    

    2、可视化埋点


    可视化埋点,即通过可视化工具配置采集节点,在前端自动解析配置并上报埋点数据,从而实现所谓的“无痕埋点”, 代表方案是已经开源的 Mixpanel

    3、无埋点


    无埋点或者叫做全埋点,它并不是真正的不需要埋点,而是采集全部事件上报。剩下交给服务器做过滤,筛选出有用的数据。

    无埋点进一步优化,可以通过服务器下发配置文件,直接由前端进行事件过滤。

    为了 kpi,基于无埋点的思想,造了一个轮子 WKTrackingData

    这里把 WKTrackingData 实现中碰到的一些问题与想法做一下记录。

    实现思想很简单,所有代码如下:


    Architecture.png

    其中 WKTrackingDataManager 负责所有的数据管理,事件追踪配置。

    Usage.png
    WKTrackingDataViewPathHelper 负责 event_path 生成。
    AOP 模块负责所有事件的追踪。

    详细的用法可以点击 github 查看。

    如何唯一标示某个事件?

    这里涉及两个问题,一是事件怎么表示?二是如何确保事件的唯一性?

    事件的表示

    对于事件的表示,使用了 event_path 实现。核心思想是对于触发了某个事件的 responder ,顺着其响应者链条,构建出其响应者链条的 path

    生成的响应者 path 如下:

    "event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate
    
    唯一标示某个事件

    这里有个问题是,某些业务场景下,同一个 button 或者其他控件,会因为其某些属性的改变,在业务上表示的是多种不同的事件。

    如,首页一个 button 在未登录时显示 点击登录,登录未实名时显示 去实名等等。
    那么对于这同一个 button 来说,它的视图树并未发生改变,生成的响应者 path 就是相同的。

    类似的业务场景还有 UISwitch 的开关,UISegmentedControlindexSelectUIStepper ,以及 UITableViewUICollectionViewcell 点击。

    针对于这种情况,WKTrackingData 在生成 event_path 时,有选择的将控件自身的不同属性也拼接上,生成的 event_path 就变成了这样:

    "event_path" = "#buttonClick:#UIButton#UIButton[0]#UIView[0]#ViewController#...#UIApplication#AppDelegate#currentTitle=Button#state=1#enabled=1#selected=0";
    
    业务扩展

    继续考虑另外一种业务场景,首页有一个 banner 轮播图,

    banner 每一个广告位的图片和跳转 url 都是由服务器下发的,且位置可配置。

    这时 banner 的 每一个 index,对应什么页面都是不固定的,0 位所对应的事件,由 event_path 是无法确定的。

    这时就需要拼接上具体的业务参数,才能够唯一标示某个事件,如 url

    WKTrackingData 也提供了业务方的参数扩展,允许业务方拼接上自定义参数:

    Additional parameters.png

    对于不希望进行事件追踪的控件,可以通过 wk_ignoreTracking 进行忽略:

    self.slider.wk_ignoreTracking = YES;
    

    其他问题

    在对于不同事件的追踪上,WKTrackingData 基于面向切面的思想,使用 runtime 直接做方法交换。

    但是对于 UIAlertViewUIActionSheetUITableViewUICollectionView 的统计,需要交换其 delegate 的方法。

    如果其 delegate class 已经实现了相应的方法,那么直接交换即可。

    如果其 delegate class 未实现相应的方法,这时仍然想要追踪到这些事件,那么就需要手动添加一下对应的 delegate method 的实现。

    - (void)wk_swizzleInstanceSelector:(SEL)origSel_ fromClass:(Class)fromClass replaceSelector:(SEL)replaceSel_ originNotImp:(SEL)notImpSel_ {
        
        Method originalMethod = class_getInstanceMethod([self class], origSel_);
        
        if (originalMethod) {
            [self wk_swizzleInstanceSelector:origSel_ fromClass:fromClass replaceSelector:replaceSel_];
        } else {
            Method notImpMethod = class_getInstanceMethod(fromClass, notImpSel_);
    
            // 如果delegateClass没有实现 origSel_ 方法
            // 则给delegateClass的 origSel_ 添加 orginReplaceMethod 的实现
            BOOL didAddNotImpMethod =
            class_addMethod([self class],
                            origSel_,
                            method_getImplementation(notImpMethod),
                            method_getTypeEncoding(notImpMethod));
            if (didAddNotImpMethod) {
                NSLog(@"%@ did add not imp method %@" , NSStringFromClass([self class]) , NSStringFromSelector(notImpSel_));
            }
        }
    }
    

    具体实现在这里

    但在实践过程中发现,UITableViewUICollectionViewdelegate 对象,在未实现相应方法时,手动给 tableView:didSelectRowAtIndexPath: 添加了 implementation,仍然不会触发。

    我们先来看一下,正常 tableView:didSelectRowAtIndexPath: 调用栈:

    2.png

    这里面,UITableView 前后会调用这四个方法,进行 cell 点击的响应。

    • 1、[UITableView _userSelectRowAtPendingSelectionIndexPath:]
    • 2、[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:]
    • 3、[UITableView _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect:]
    • 4、[TableViewController tableView:didSelectRowAtIndexPath:]

    根据调用顺序可以发现 UITableView 在真正调用 delegate classtableView:didSelectRowAtIndexPath: 前,会先触发自己的 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 方法。

    直接给 _selectRowAtIndexPath:animated:scrollPosition:notifyDelegate:isCellMultiSelect: 打一个断点进入汇编查看,发现:

    1.png

    如果 delegate class 如果有实现 tableView:didSelectRowAtIndexPath: ,则一切正常,会完成 tableView:didSelectRowAtIndexPath: 的调用。

    但如果 delegate class 未实现 tableView:didSelectRowAtIndexPath: ,即便手动给 tableView:didSelectRowAtIndexPath: 添加 implementation,也不会尝试响应 tableView:didSelectRowAtIndexPath: 了。

    跟随汇编调用发现,会直接 jump 到另外一个指令。

    WKTrackingData 这里换了种做法,直接对 [UITableView _userSelectRowAtPendingSelectionIndexPath:] 进行了交换(UICollectionView 类似)。
    实现了 UITableViewUICollectionViewdelegate 对象,在未实现相应方法时对 cell 事件的追踪。

    小结


    多个event_path对应同一个事件

    考虑另外一种情况,线上存在多个版本,不同版本的页面都都写细微的差别,那么对于同一个事件来说,它就可能存在多个 event_path

    在实践过程中,这一块的映射,就需要服务器同学的配合了,可以让服务器做成后台可配置的。

    事件统计优化

    业务扩展 中提到了如何添加业务参数,WKTrackingData 使用了分类实现。

    在实践过程中发现,全埋点的方案,造成了大量流量的浪费,有好多事件不需要啊[(ŏ_ŏ)]。

    进一步优化,可以由服务器下发配置文件,里面直接包含了,希望客户端上报的所有事件,只有和配置文件中符合的 event_path 才进行上报。

    这一步的业务参数获取,也可以不采用 WKTrackingData 的实现,直接使用 kvc 获取。

    形如:

    key_path = "viewController.banner.url"
    

    看到这里就点开 WKTrackingData 给个star吧,有问题可以在guthub上提issue,或者下方评论~

    相关文章

      网友评论

        本文标题:iOS全埋点实践

        本文链接:https://www.haomeiwen.com/subject/ldwsactx.html