iOS全埋点实践

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

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

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全埋点实践)