前言
最近跟同事花了点时间来思考可视化埋点,并没有什么突破性的进展,不过市面上很多关于可视化埋点的技术文章都在讲达到的效果如何,没有把遇到的问题很清晰的表述出来。本文集中把几个核心问题梳理了一遍,同时也记录了思考的过程,算是一个总结。文章首发于手机京东技术团队公众号,这里贴出文章的原始链接 iOS自动化埋点探索,欢迎在微信上阅读,排版和体验要更好。
随着公司业务的发展,数据的重要性越来越突出。大中型公司甚至一些小型互联网公司,都建立了自己的数据采集、上报和分析平台。而数据的采集是整个流程非常重要的一个环节,只有保证数据的采集的全面和精准,后面的分析才有意义。为了解决数据的正确性、维护难度和开发效率问题上,很多公司都提出了自己的技术方案。这些埋点方案大体可以分为三类:
代码埋点
由开发人员在触发事件的具体方法里,植入多行代码把需要的数据存下来,然后根据上报策略把前一个时间段收集的数据上传到后台。
可视化埋点
通过可视化工具圈选具体页面元素并生成配置,在用户操作时,自动根据配置判断是否需要采集该事件。
无埋点
无埋点并不是不需要埋点,而是在应用页面的加载、点击等事件前自动嵌入监测代码来采集数据,它会采集所有感兴趣的事件类型的埋点。其实我们更愿意称它为全埋点。
京东客户端现在主要使用第一种方案,即代码埋点。这种方案的好处是,用起来比较简单,在收集个性化数据时也比较灵活。但是也有一些问题,比如:
+ 新增埋点依赖App发版,影响数据收集时机。
+ App发版需要埋点工作完成,影响版本进度。
+ 埋点代码和业务代码耦合在一起,增加代码维护难度。
+ 如果埋点错误只能更新版本解决(Apple在2017年初全面禁止使用HotFix来修复bug)。
为了解决这些问题,我们调研了市面上的方案,在调研过程中,我们发现很多公司都看到了这些问题,他们也提出了自己的解决方案,基本上都是围绕可视化埋点方案来做的。这种方案好处是,埋点提报方式和数据后台基本不需要修改,风险也比较可控。而无埋点方案由于全部数据都收集,造成数据量巨大,这给服务器和网络传输带来较大负载,另外数据清洗难度也非常大,基于这些原因,大部分公司都没有选择这种方案。基于这些这些原因和结合我们的场景后,我们选择了可视化埋点方案来解决代码埋点的问题。
可视化埋点并不是摈弃了代码埋点,而是在代码埋点的上层封装的一套逻辑来代替手工埋点,大体上架构如下图:
不过要实现可视化埋点也有很多问题需要解决,比如如何确定页面元素的唯一标识、如何携带业务参数、如何添加有判断逻辑的埋点和配置信息的版本管理。下面我们会整体介绍可视化埋点的使用方式和技术细节,另外针对上面的问题我们会尝试给出解决方案和一些思考。
整体概览
整体概览的介绍分为2个部分:产品原型概览和技术原理概览。首先介绍产品原型概览,可以更直观了解可视化埋点的基本运作流程。
产品原型概览
首先在App中嵌入可视化埋点SDK。当开启圈选开关之后,会在屏幕上始终悬浮一个圈选开关,用于埋点维护人员采集埋点配置信息。如下图所示:
圈选开关的按钮一共有3个,当选择圈选按钮的时候,点击页面上的元素,SDK会拦截点击事件,弹出一个用于收集配置信息的视图。检测按钮用于版本管理,下文再详细介绍。关闭按钮用于关闭圈选功能,可以正常的操作App页面元素。
开启圈选开关,选择页面元素进行埋点配置采集。例如,点击上图所示页面右下方的加入购物车按钮,弹出配置视图,如下图所示:
视图会展示一些信息,其中最重要的是SDK生成的唯一标识符,用于对埋点进行标识。埋点维护人员需要填写eventId,选择一些要上报的数据字段等操作。上图左上角的增加按钮,用于一个点击事件有多个埋点的需求,点击增加,会在下方新增一个信息采集视图,以供埋点采集。上报类型是跟我们具体业务相关的,可以忽略。页面参数、事件参数的选择,会在下文说明携带上报数据部分的思路介绍。总之,采集完毕会形成一条配置信息,上传到服务器。采集完成全部的配置信息,形成一个埋点配置列表。
在用户启动App时,埋点配置列表会被下载下来。当用户点击加入购物车按钮时,SDK会使用和上文中配置采集阶段相同的方法,生成唯一标识符,用于在埋点配置列表查找相关配置项,如果匹配成功,则利用这些配置数据,自动的进行埋点上报。
整体的产品原型概览先介绍到这,下面看一下技术原理概览。
技术原理概览
采用AOP(Aspect-Oriented-Programming)即面向切面编程的思想,基于 Runtime 的 Method Swizzling能力,来 hook 相应的方法,从而在hook方法中进行统一的埋点处理。例如所有的按钮被点击时,都会触发UIApplication的sendAction方法,我们hook这个方法,即可拦截所有按钮的点击事件。
这里的处理分为2个部分:采集埋点配置信息,和真实的埋点数据上报。这个和上文产品原型概览部分介绍的处理流程相对应。
以按钮点击事件的处理为例,大致的流程如下图所示:
这里仅仅是以按钮为例说明,UITableView、UICollectionView、UIView的手势等等,都是同样的处理逻辑,对可视化埋点有过研究的人应该都了解这个过程,这里不再过多阐述。下面来详细的探讨SDK的关键模块的技术实现思路。
关键模块实现思路
我们要讨论的SDK的关键模块分为3个部分:生成唯一标识符、埋点数据携带、版本管理。其余部分,例如hook的具体实现、数据的上传、下载匹配、圈选工具的交互等,虽然也都是需要解决一些技术问题,但是都有比较清晰的实现方案,这些方面不作讨论。下面来看第一个问题。
唯一标识符
市面上可视化埋点方案,大多都使用viewPath生成唯一标识符。我们知道App的视图层次是一个树状结构。一个 view可以被认为是一个节点,处于视图树的某一个位置,从根节点到这个view节点的深度信息构成了一个path,用来唯一标识该view。
如下图所示,
^ view1的viewPath形如:0-0,
^ view2的viewPath形如:0-1,
^ view3的viewPath形如:0-1-0,
^ view4的viewPath形如:0-1-1。
这种方式有诸如可读性、数据计算量、系统视图干扰等一系列的麻烦要处理。除此之外,最关键的问题是这种方式仅仅适用于静态视图。还是拿上图举例,假如某一时刻,view1被移除,那么view2的viewPath变成了0-0,它的子视图的viewPath也相应发生变化,这种情况下,viewPath无法用来唯一标识某个视图,唯一标识符就不再唯一了。尽管也有相应的优化措施,例如在viewPath中引入className,但是这种方式只是很轻微的缓解了问题。在强调页面配置化的场景,整个页面的元素的位置、顺序、是否展示,几乎都要依靠服务端下发,引入className的优化恐怕并没有明显效果,而且增加了复杂度。所以这种方案还需要很大程度的提升和优化才行。
位置信息是可变的,所以viewPath这种方式是从可变的要素来生成唯一标识符,我们并没有在研究viewPath上花费太多时间,而是换一种角度思考,引入相对不变的要素来生成唯一标识符:target+action。
获取target+action的方式非常简单高效,可以直接获取一个UIButton的target和action,UIView可以通过UIRecognizeGesture获取target和action、UITableview的delegate和didSelectedRowAtIndexPath等等。可以发现,无论一个view显示在任何位置,它的target和action都不会变化(除非某一个特殊情况下,功能发生变化,target和action才会变,不过显然这个时候原始的埋点应该也及时废弃或者添加新的埋点)。这样去除了可变要素,利用不可变要素来生成唯一标识符,相对来说会更加可靠。
但是现实场景下,并不会总是一个按钮对应着一个单一逻辑,在某种条件下进行区分埋点非常常见,例如在一个按钮的处理事件中,可能会需要在condition1的情况下,需要执行A逻辑,然后埋A点,在condition2的情况下,执行B逻辑,埋B点。这时,无论使用viewPath还是target+action,都不能解决唯一标识问题。特别是condition多种多样,增加了问题的复杂度,比如有些地方使用某一个对象的属性或者服务端下发的字段作为条件判断,有些地方使用视图的状态等信息作为条件判断。从这里我们也能发现代码埋点的优势,你可以利用一切编程的灵活和便利性来达到目的,这恰恰成为了可视化埋点要面临的困难。而使用target+action的方式还有一个麻烦需要处理,比如2个view的target+action是一样的,但是要求不一样的埋点。这两种情况增加了生成唯一标识符的困难。本质上这两种情况可以归并为一种,多条件埋点或者有条件埋点。
我们的方案是增加一个protocol如下:
举个例子:假如有一个按钮,在condition1的情况下要执行doSom1这件事情,在condition2的情况下要执行doSom2这件事情
那么开发者要让target在实现点击事件的同时,还要实现上面的协议方法。
SDK会自动调用这个方法,把返回的标识追加到target+action的后面。protocol这种做法虽然解决了唯一标识问题,但是其实是把问题的复杂度抛出去,把区分condition的工作交给了开发者。所以这种方式也只是折衷的处理方案。
总是有一些方案,使用覆盖率来衡量可视化埋点方案的适用情况。就是说,使用可视化埋点,可以替换百分之多少的代码埋点。但是有条件埋点问题不解决的话,这个覆盖率是没有意义的。因为在埋点采集(圈选)阶段,采集者可能压根不知道这个按钮是单一埋点还是带条件的,如果采集错误的话,上报的埋点数据就会不准确。也有些人推荐多种组合的埋点方式,即代码埋点、可视化埋点、无埋点等组合使用,这看起来是个很不错的想法,不过在此之前,我们首先要解决明确边界问题,让多种方式协同工作。
埋点数据携带
一个具体的埋点上报内容,可能还要求携带一些页面或者业务数据。埋点携带数据的问题,其实并不仅仅是可视化埋点需要面对,这是一个普遍的问题,跟埋点方式无关。还有前端埋点、后端埋点之争,如果采取后端埋点,有些数据可能只能从前端获取到。大家都觉得代码埋点带来代码耦合,而且也比较繁琐,但是采取无埋点的话,如何解决巨大的数据流量和后端数据清洗的问题。总之,自动化埋点的技术探索还处于蛮荒阶段,各家自成一体,没有一个成熟的解决方案。
由于这么多的问题难以解决,现在市面上主要还是依赖代码埋点的方式。代码埋点繁琐,需要跟业务逻辑紧紧耦合在一起;但是这种方式特别灵活,在应用开发中,一个复杂页面可能包含许多模块、视图、业务处理类等,数据也可能从多个接口下发,分布在这些零散的地方。代码埋点跟随业务逻辑各自分布在这些零散模块中,所以可以精准的获取这些模块中的数据,这就成为了代码埋点的优势。我们提出可视化埋点的解决方案,其实还是站在代码埋点的视角看待问题,希望能用统一的方式解决代码埋点的繁琐、耦合问题,又不能牺牲代码埋点的优势和功能。
许多的可视化埋点方案,都是把埋点数据携带环节一笔带过。仅仅指出用KVC等方式进行取值上报,并没有实际的技术方案。其实这里存在非常多的疑点。譬如,数据存在什么地方?绑定在view上,还是在controller上,数据需要集中堆放到某个地方么。最基本的数据有服务端下发的字段和object本身的属性,key是怎么规定的?KVC的方式是运行时特性,如果字段疏忽大意写错了,或者发生了变化但是key没有及时更新,KVC的方式如何给出提示?
KVC要处理和面对如此多的问题,所以我们认为这种方式来存取数据并不合适。在上面,我们也引出了埋点数据上报方案的各种问题,总之由于各种各样技术和现实问题的制约,各个公司可能都发展了自己的一套埋点方案,经过了很长时间的发展,各端都趋于稳定。如果修改数据上报方案,还要兼顾考虑各端需要改造的成本和风险,在彻底解决这些问题之前,我们提出的方案还是要基于当前的数据上报方案。
既然是基于我们当前的数据上报方案,还是先看看携带的埋点数据的一些特征:埋点携带的业务相关的数据主要分为3类:页面参数、事件参数、扩展参数。页面参数,顾名思义是跟页面相关的数据,一般情况下,一个页面下的所有埋点,页面参数应该都是一致的,或者说就固定的几种类型。事件参数类型非常多,一般都是跟具体的点击事件的业务相关。而扩展参数,我们这边是作为一个补充,用来上报一些额外的参数,例如商品详情页面,扩展参数可能会有店铺id,商品的分类等等信息,一般也都是几种固定的类型。
上报的这些数据,并不都是服务端下发什么,然后原样传回去,而是会经过客户端的一系列处理。比如可能会写一大堆逻辑,if某种业务,采集字段A,else某种业务,采集字段B;而且也不仅仅是条件判断,许多的字段会抽象成0、1这样的数字来表示,比如并不会直接使用字段A,而是用0来替代,B用1来替代等等,最后许多个这样的字段会使用下划线拼接起来,形如:0_1_1_0_1_0_0_0_1。这些数据上报到后台,如果需要提数,会有专门的程序来解析这些数字拼接的字符串。
前面我们说过,不会改变数据上报方案,上面介绍的方式改造成本很高,我们还是用上述的方式来处理数据。但是需要提供一个统一的入口,以便于可视化埋点SDK可以访问。这里,我们提供一个protocol,可以让target或者是controller实现协议,这个方法返回一个字典,把之前处理数据的逻辑迁移到这里作为value,key用来标识数据。protocol如下所示:
target或者是controller实现协议,如下所示:
这种方式还便于采集配置,在圈选的时候,SDK会自动调用target和controller的这个方法,并且把所有的key值显示出来,采集字段的时候,直接选择某一个key即可,如下图所示:
当选择某一个key的时候,会同时增加一个source字段,记录下这个key是来自于target还是controller。这些信息都作为配置信息被采集。当用户在使用App,进行真实的埋点上报的时候,会根据source决定调用target还是controller的方法,同样返回的字典,使用key来获取对应的数据即可。
版本管理
生成控件的唯一标识符是可视化埋点的一个重要环节,无论是viewPath也好,还是target+action的方式,标识符都会包含一些跟控件本身相关的信息。假如采用的是target+action的方式:在1.0版本,有一个按钮Button,它的处理方法是actionA,在采集埋点配置信息的时候,生成的唯一标识符是target+actionA。如果在2.0版本,他的处理方法被修改为actionB。如果拿着这个2.0版本的target+actionB去1.0版本采集过的配置表中进行查找,就会找不到对应的配置项。那么在2.0版本中,是否还需要重复采集在1.0版本采集过的埋点信息?如果重复采集的话,这可意味着可能会有非常大的工作量,一个大型App,可能全部的埋点个数有几千个不等,每一个版本都把之前采集过的埋点在重新采集一次的话,工作量非常的可怕,也没有必要。
还有就是,如果在2.0版本,干脆删除了这个按钮,那么这个按钮的埋点自然也就不再需要了。但是和这个按钮相关的埋点配置却不能自动从配置表中删除。长此以往,配置表中会冗余越来越多的无效埋点配置项,增加配置管理的成本,无论对于网络还是系统的性能都是一个越来越严重的问题。
一个很有趣的现象是,目前市面上的可视化埋点方案,大多数没有提到版本管理。其实版本管理,是一个必须要面对的问题。我们必须要能在版本迁移的时候,指出哪些埋点是继续有效的,而哪些埋点已经失效了,以供采集人员及时的更新处理。一开始我们设想,通过代码来模拟点击页面的所有元素,触发了元素的处理事件,一定会走到自动埋点上报的逻辑。设置一个标识,当在版本检测过程中时,查找到配置项之后,不再进行上报逻辑,而是把该配置项标识为有效。代码来模拟点击页面的元素,需要通过调用UIControl的sendAction,或者是直接调用target的action方法等。这种方式理论上可行,但是特别麻烦,要处理的问题也特别多。
其实有一个很取巧的方式,不需要代码模拟点击事件。循环遍历页面的所有元素,直接利用SDK生成这些元素的唯一标识符,然后用唯一标识符去配置列表中查找,查找到配置项之后该配置项标识为有效。这种方案非常简单轻巧,但是也有一些问题要处理,比如有些视图是在controller下面,有些视图在window下面,还有有一些视图是延迟加载的,比如点击了某个按钮,然后页面中增加一些新的元素。针对这两个问题,我们通过设计版本检测的交互方式来解决。在上文的SDK整体概览的产品原型概览章节,我们提到了一个检测按钮。当选择检测功能的时候,点击页面的某一个元素,SDK会向上寻找这个元素处于的根视图。然后从这个根视图出发,递归遍历这个根视图的所有子视图。这样无论视图是在controller,window,navigationBar下面,只要点击这些地方,都可以被检测到。对于延迟加载的视图,可以先关闭检测按钮,操作app把相关视图加载出来之后,在用同样的方式来进行检测。
检测完毕后,会弹出一个版本管理的视图。按照所有埋点、有效埋点、无效埋点三种类型列举出所有的配置项。然后针对无效埋点进行确认和相关处理即可。
总结与展望
我们的可视化埋点探索的技术方案先介绍到这里。对可视化埋点进行过深入研究之后,会发现上面介绍的这些问题处理起来比较困难,上面仅仅是介绍我们的方案和思考。除了支持App发版后新增埋点的能力,我们特别希望通过这种方式得到埋点效率提升,把开发者从体力活中解放出来。从当下来看,如果数据携带依然需要一系列逻辑处理的话,想通过自动化埋点的方式获得效率提升还是比较有限的。同时,把从前代码实现的埋点,替换成可视化圈选的方式,虽然解决了埋点代码和业务逻辑耦合的问题,但似乎像是一种人力成本的迁移,毕竟圈选采集信息还是依赖于人工处理。
计算机本身的目的之一就是解决一些重复的繁琐的事务。当下代码埋点的成本很高,不仅仅是开发者,许多方面都要投入大量的时间和精力维护。所以我们相信自动化埋点这个需求,会驱动更多人持续不断地研究,不断地提出新的思路和解决方案,最后有一天实现真正高效的自动化埋点。