美文网首页iOS开发(OC)
iOS无埋点SDK实现思路

iOS无埋点SDK实现思路

作者: skogt | 来源:发表于2019-04-12 15:50 被阅读75次

    18年下半年实现了无埋点SDK的技术调研,构思,实现。当然其中有很多细节上的问题,比如不同系统版本间兼容问题,手势之间问题等...
    经过大半年线上的验证,功能趋于稳定。今天写一下大致的技术方案和构思,仅供参考

    既然说到无埋点当然要分析无埋点的优劣势

    无埋点的优劣势

    优势

    • 可视化展示界面最基本度量,满足基本数据分析需求。无埋点可视化展现界面PVUV等最基本信息,告诉运营和产品人员每个控件被点击的概率是多大,哪些控件值得做更进一步的分析等。有助于企业了解用户行为,为进一步数据分析指明方向。
    • 无埋点极大程度避免了因需求变更、埋点错误等原因导致的重新埋点繁复工作
    • 大大减少运营和产品的试错成本
    • 减少了部分手动埋点的工作量,提升效率

    缺陷

    • 适用大部门、通用的场景,适合标准化的采集,有少部分需要埋点的场景覆盖不了,更深层次和多维度的分析还是需要手动埋点来辅助支持
    • 兼容性 部分第三方框架如RNTexture,因为特殊的实现机制,会出现部分事件无法采集的情况
    • 无埋点采集全量数据,给数据传输和服务器增加压力
    • 数据采集不全面(目前自动采集的是一些常用场景,覆盖面根据后面不断迭代来扩充),可靠性没有手动埋点那么高

    无埋点方案设计探讨

    方案主要涉及到如下两个点

    • hook delegate
    • 生成viewPath, viewId-唯一标识

    hook delegate

    SDK 整体采用了 AOP(Aspect-Oriented-Programming) 即面向切面编程的思想,就是动态的在函数调用的前后插入数据收集的代码。SDK 的数据收集功能的实现主要通过 Method Swizzlinghook 相应的方法。hook的方法大致可以分为3类:系统类的方法系统类的Delegate方法自定义类的方法

    系统类的方法

    系统类的方法是指系统框架中提供的基础类的方法,如 UIApplicationUIViewController 等。SDK 在实现某些功能时,需要hook这些类的方法。例如在实现对页面事件的收集时,可以hook UIViewController 的生命周期的方法:viewDidLoadviewDidAppearviewDidDisappeardealloc

    系统类的Delegate方法

    系统类的 Delegate 方法主要指 UIKit 框架中提供的 Delegate 中的方法,如 UIScrollViewDelegateUITableViewDelegateUIWebViewDelegate 等。

    自定义类的方法

    顾名思义,自定义类的方法是指开发人员在工程中自己定义的类,而非系统类的方法。SDK通过hook这些类的方法来实现。例如在实现对手势操作的事件收集时,需要hook手势对象所指定的target 中的 action 方法,而 target 通常都是自定义类。其实hook系统类的 delegate 方法也可以看成是 hook 自定义类的方法,因为系统类的 delegate 方法大多都是需要在自定义类中实现。

    这部分看起来是借助于 AOP 来添加数据收集的代码,但是实际实现中可能会涉及到很多问题。例如:如何将导航栏与系统弹窗的点击事件归属到合适页面中、hook手势操作是否会引起的性能问题等等。当然这些只是我想到的其中一部分,需要在版本迭代和实际使用中不断完善。

    SDK 对于用户的各类点击事件的收集,具体hook的类和方法如下图所示,其他的就不详细列举,有疑问的可在评论区交流探讨


    viewPath及viewId生成

    为了对 APP 中某个页面的某个 view 进行数据收集、统计与分析,首先就需要能够唯一的标识与定位这个视图。那么怎么生成唯一标识呢? 参考了目前主流产品的一些成熟方案,使用viewPathviewId 来完成。

    涉及到的一些技术要点:
    目前开发中提供了两种实现方式:第一种是每个ViewController-viewWillAppear的时候生成所有视图的关系链,然后根据点击的具体视图直接查询;第二种通过目标区域点击一直追溯父node。第一种方案优势就是消耗较大,不管是否需要采集,都会生成一份关系链映射表;第二种就是实现简单可靠

    1. 数据分析成邻接矩阵,生成树;进行dfs记录相应的深度和路径长度
    2. target递归父亲节点生成

    viewPath的组成

    整个APP的视图结构其实可以看成一棵树(viewTree),树的节点就是UIWindow,树的枝干由UIViewControllerUIView组成,树的叶节点都是由UIView组成.

    那么在viewTree中用什么信息来表示其中任意一个 view 的位置呢?很容易想到的就是使用目标 view 到根之间的每个节点的深度(层次)组成一个路径,而节点的深度(层次)是指此节点在父节点中的 index。这样确实能够唯一的表示此 view 了,但是有一个缺点:它的可读性很差。因此在此基础上又增加了每个节点的名称,节点的名称由当前节点的 view 的类名来表示。

    因此,在 viewTree 中,由一个 view 到根节点之间的每个节点的名称与深度(层次)共同组成的信息构成了此 viewviewPath。由于在做 view 的统计分析时,都是以页面为单位的,因此SDK 在生成 viewPath 时,只到 view 所在的 UIViewController 级别,而非根部的 UIWindow。这样做也在一定程度上减少了viewPath 的长度。

    viewPath的表示形式与示例

    viewPath是由各节点的类名与深度组成,那么接下来就使用这些信息来表示出 viewPath。下面一个具体的示例来简单说一下

    路径中各个节点的类名是:

    ViewController-UIView-UIView-UILabel
    

    UITableViewCell/UICollectionCell 的深度表示

    App 开发中,最常用而且最重要的控件就是UITableViewUICollectionView。针对这种可复用视图,里面会包含很多 Cell,而且 Cell 个数也不确定,那么里面的每一个 Cell 应该怎么去表示其深度呢?答案是indexPath。虽然每个 Cell 都可能被复用,但是不同的 Cell 都对应一个唯一的indexPath,因此完全可以使用indexPath值来表示其深度。

    viewId的生成

    viewPath 已经能够唯一标识某个 view 了,为何还需要viewId呢?其实主要原因是:viewPath 的长度不固定,而且一般都会比较长,不便于后台使用它作为 view 的唯一标识。因此 SDK 使用viewPath信息通过MD5加密生成一个固定长度的值作为viewId

    总结:以节点所在tree的路径作为pathpathmd5值作为viewId

    埋点

    基于上述的两点,我们基本可以满足自动采集的需求。比如要记录会员卡列表用户点击的是哪张会员卡时,通过一段时间的埋点日志就可以大致分析出哪张会员卡更受消费者青睐。我们可以把上述需求转义:点击某个UITableView中的某个Cell-indexPath

    1. hook UITableViewDelegate方法,获取点击的CellindexPath
    2. 根据步骤1获取到CellindexPath后,计算当前视图位于UIViewControllerPositionViewPath,再转换成埋点所需的唯一标识viewId
    3. 结合当前已存在的手动埋点SDK,记录触发的点,并上报

    思考项:
    1.由于无埋点涉及到许多事件的hook,此外项目中可能还包含其他埋点SDK,比如growingio,这样你就需要区分,避免无效多余的事件采集,解决方案:增加白名单,去除系统内部类和其他SDK的影响,只保留公司内部具体业务
    2.hook delegate 范围: 根据具体的业务需要,来确定hook区域

    1. 当一个父节点有多个同类型的子节点时是否区分,怎么区分,初期先不实行。因为不同场景不同情况可能都会不一样,还是需要产品结合具体场景来反向推动无埋点SDK完善。该点在下面提到的二期扩展中已支持,具体方案参考下方
    2. Texture因为有些方法无法hook,所以引用该框架实现的页面就更多的需要手动埋点来辅助支持

    二期埋点功能延伸

    1. 计算目标视图在父视图中的索引值

      目标视图在所在的父视图中unique,没有同一类别,无需索引值;存在同一类别,需要增加索引值来区分

      举个例子:UIButton所在的层级中有多个按钮,则需在计算viewPath时添加该按钮的序列值。类似LKMainViewController-UIView-UIButton(1)来表示触发的按钮是同一层级的第二个按钮,来具体定位按钮的位置。

    2. 提供解析映射表,供数据组解析并统计上报的数据

    3.和数据组确定具体的数据字段定义。
    举个例子:

    • 新增一个字段buried_type来区分是普通的手动埋点还是无埋点. {"手动埋点":1,"无埋点":2}
    • 新增一种事件类型6001来表示手势操作。无埋点产生的列表点击,按钮点击,手势操作的单击(Tap手势+Tap次数=1,不区分几根手指操作)都属于控件点击-3001,其他的手势操作属于6001这个事件类型
    • viewPath存储到args字段({"path":viewPath}),viewId存储到arg字段.
      因为每个公司的业务不一致,所以数据字段定义和阐述都会不一致,重要的还是要前期沟通,后期统一高效执行

    杂谈

    写了这么多内容了,没贴什么代码片段,只是写了下一些思路和想法,写的比较仓促,不足之处请多包涵。反正都看到这了,后悔也没用了。
    最后,发一波牢骚,在此期间,还是非常感谢测试组和数据组的同学给与反馈和帮助。技术项和业务密不可分,可谓相辅相成,如果脱离了任何一方都是耍流氓——同时也吐槽下目前很多公司无脑的技术项,说白了都是强kpi导向,当然我自己公司也不除外。人在江湖,身不由己...

    参考资料:
    https://neyoufan.github.io/2017/04/19/ios/%E7%BD%91%E6%98%93HubbleData%E6%97%A0%E5%9F%8B%E7%82%B9SDK%E5%9C%A8iOS%E7%AB%AF%E7%9A%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E7%8E%B0/
    https://www.jianshu.com/p/69ce01e15042

    相关文章

      网友评论

        本文标题:iOS无埋点SDK实现思路

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