iOS自动化埋点探索,无痕埋点

作者:计算机网络

iOS无埋点数据 SDK 实践之路 iOS无埋点SDK 之 RN页面的数据收集

由于无痕埋点涉及的方面比较多,而且具体使用中与外部代码产生冲突比较多,所以目前简单地将无痕埋点的开发分为三个阶段:

本篇文章是基于 网易乐得无埋点数据SDK 总结而成。负责无埋点数据收集 SDK 的开发已经有半年多了,期间在组内进行过相关分享,现在觉得是时候拿出去和同行们交流下了。本篇主要讲一下SDK的整体实现思路以及关键的技术点。

最近跟同事花了点时间来思考可视化埋点,并没有什么突破性的进展,不过市面上很多关于可视化埋点的技术文章都在讲达到的效果如何,没有把遇到的问题很清晰的表述出来。本文集中把几个核心问题梳理了一遍,同时也记录了思考的过程,算是一个总结。文章首发于手机京东技术团队公众号,这里贴出文章的原始链接 iOS自动化埋点探索,欢迎在微信上阅读,排版和体验要更好。

本篇文章是讲述 iOS 无埋点数据收集 SDK 系列的第三篇,之前的两篇文章都只是讲述了某一方面的内容,而本篇会详细介绍下 SDK 的整体设计以及各个模块的功能和实现思路。

  • 收集全量埋点中的点击事件数据,主要包括按钮点击、list点击、手势和弹窗等。
  • 收集全量埋点中的其他数据,主要包括H5页面数据、页面驻留、页面流转记录和list曝光等。
  • 现有业务代码中埋点相关代码解耦,开发圈选埋点功能。

SDK 已经具备不需要代码埋点就能 自动的动态可配的全面且正确 的收集用户在使用 App 时的所有事件数据。除此之外,还单独开发了与之配合的圈选SDK,能够在 App 端完成对界面元素的圈配以及 KVC 配置的上传。而界面元素圈配的工作完全可以交给用研与产品人员来做,减轻了开发人员的工作量。

随着公司业务的发展,数据的重要性越来越突出。大中型公司甚至一些小型互联网公司,都建立了自己的数据采集、上报和分析平台。而数据的采集是整个流程非常重要的一个环节,只有保证数据的采集的全面和精准,后面的分析才有意义。为了解决数据的正确性、维护难度和开发效率问题上,很多公司都提出了自己的技术方案。这些埋点方案大体可以分为三类:

先看一张 SDK 的整体设计图:

接下来,大致说一下各阶段的具体安排和实现思路。

SDK 已有的功能可以分为两大部分:

代码埋点

图片 1

第一阶段:收集各种点击事件数据

  • 基本事件数据的收集:基本事件的收集是指应用冷启动事件、页面事件、用户点击事件、ScrollView滑动事件等,这部分全部都是自动完成的,实现思路会在第一节中介绍。
  • 业务层数据的收集:业务层数据的收集是指对与业务功能相关的一些数据,例如:在用户点击提交订单按钮时,收集用户购买的物品以及订单总金额的数据。这种业务层数据的收集以往大多通过 代码埋点 的方式去做,本SDK则真正的实现了 无埋点 的去获取这些想要的业务数据。这部分的实现会在本文的第二节详细介绍。

由开发人员在触发事件的具体方法里,植入多行代码把需要的数据存下来,然后根据上报策略把前一个时间段收集的数据上传到后台。

从上图看出,SDK 整体上主要包含 4 个部分:AOPEvent CollectorEvent CacheEvent Upload。其中,每个部分是一个相对独立的功能模块,同时模块之间通过图中的方式进行通信。

1. 具体收集的数据以及规则介绍

图片 2

第一阶段捕获的数据

如上图,展示了第一阶段要收集的事件类型和对应的上报数据的生成规则。
这里,具体介绍一下viewPath的生成流程。

图片 3

Paste_Image.png

SDK 整体采用了 AOP(Aspect-Oriented-Programming)即面向切面编程的思想,就是动态的在函数调用的前后插入数据收集的代码。在 Objective-C 中的实现是基于 Runtime 特性的 Method Swizzling 黑魔法。

可视化埋点

SDK 中的这 4 个模块各自的主要功能如下:

2. 如何捕获各种点击事件

图片 4

各种事件hook的方法

SDK 的数据收集功能的实现主要通过 Method Swizzlinghook 相应的方法。hook的方法大致可以分为3类:系统类的方法、系统类的Delegate方法、自定义类的方法。

通过可视化工具圈选具体页面元素并生成配置,在用户操作时,自动根据配置判断是否需要采集该事件。

  • AOP:提供数据收集所需要的时机,即通过 Method Swizzlinghook 相应类的方法,然后以 Post Notification 的方式提供出去。
  • Event Collector:监听通知,针对当前事件执行相应的数据收集,并将收集的事件数据提交给缓存模块。
  • Event Cache:负责事件数据的缓存、序列化以及读取操作,其中包括内存缓存与磁盘缓存。
  • Event Upload:基于一定的上报策略执行对已收集的事件数据的上报。

3. 代码的模块设计

图片 5

  • 抓取事件模块:通过hook系统类拦截事件响应时的方法
  • 收集数据模块:被拦截的对象是否进行追踪,可以设置类型白名单,页面白名单等

系统类的方法

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

无埋点

接下来逐个介绍上述 4 个模块的具体实现细节。

4. 一些细节问题

系统类的 Delegate 方法

系统类的 Delegate 方法主要指 UIKit 框架中提供的 Delegate 中的方法,如 UIScrollViewDelegate、UITableViewDelegate、UIWebViewDelegate 等。SDK 中的大多数功能都是通过hook这些协议中的方法来完成的。例如在实现列表元素点击事件的收集时,主要 hookUITableViewDelegate 中的 tableView:didSelectRowAtIndexPath: 方法。

无埋点并不是不需要埋点,而是在应用页面的加载、点击等事件前自动嵌入监测代码来采集数据,它会采集所有感兴趣的事件类型的埋点。其实我们更愿意称它为全埋点。

AOP

这个模块的主要功能就是提供 SDK 执行数据收集所需的时机,在实现上又可以细分为 2 个方面:

  1. 实现 AOP 编程
  2. hook 类的方法

在 iOS 中实现 AOP 编程的技术就是基于 Objective-C Runtime 特性的 Method Swizzling。而在 Github 上已经有一个很不错的实现了 AOP 的开源库-Aspects,它的实现也是利用了 Objective-C 的消息转发机制与 Method Swizzling 黑魔法。

但是,SDK 最终并未使用 Aspects 库,虽然 Aspects 封装的很好而且很好用,但是它并不能完全满足项目的需要,主要表现在如下 2 个方面:

  1. Aspects 无法 hook 类中不存在的方法,或者未实现的方法。
  2. Aspects 不支持 hook 类的类方法。

因此,SDK 单独实现并封装了一个用于执行 hook 的类,其实现也是对 NSObject 的扩展,类似于 Aspects。

在 上篇文章 中简单提了一下,SDK 在实现对基本事件数据的自动收集时,主要 hook 的方法分为 3 类:

  • 系统类的方法
  • 系统类的 Delegate 方法
  • 自定义类的方法

那么,接下来就详细的介绍一下,SDK 在实现对事件的收集时,具体 hook 了哪些类的哪些方法。

第二阶段:完善全量埋点数据收集

自定义类的方法

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

这部分看起来是借助于 AOP 来添加数据收集的代码,但是在真正做的时候,也并没有想的那么简单,涉及到很多细节上的问题,例如:如何将导航栏与系统弹窗的点击事件归属到合适页面中、如何区分UIControlEventValueChanged事件、如何解决hook手势操作引起的性能问题等等。不过这部分内容并不是本篇文章的重点,因此这里不打算多说,之后会单独写一篇文章来讲述遇到的一些坑。

京东客户端现在主要使用第一种方案,即代码埋点。这种方案的好处是,用起来比较简单,在收集个性化数据时也比较灵活。但是也有一些问题,比如:

各类点击事件的拦截

对于 SDK 来说,收集用户的所有点击的行为数据是非常重要的一部分。另外,这部分数据对于用户行为分析以及统计路径转化率时,都是至关重要的。

那么 SDK 对于用户的各类点击事件的收集,主要 hook 了如下的一些系统类的方法:

图片 6

针对上图,做一些简要的说明:

  1. 所有的 UIControl 类型的控件、UITabBarButton 以及在导航栏上自定义添加的 UIBarButtonItem 的点击事件,都可以通过 hook 系统类UIApplicationsendAction:to:from:forEvent: 方法进行拦截。但是,这个方法并不能拦截到导航栏上系统自动添加的返回按钮的点击,因此 SDK 又 hookUINavigationControllernavigationBar:shouldPopItem: 方法来实现对它的点击的拦截。
  2. 针对与手势相关的事件,SDK 首先通过 hook 系统类 UIGestureRecognizerinitWithTarget:action:addTarget:action: 这 2 个方法拿到 target 对象与 action 方法,然后再去 hook target 的 action 方法,从而能够拦截到手势相关的事件。
  3. 对于 UITableView、UICollectionView 某一行的点击,首先 hook 它们的 setDelegate: 方法,从而拿到 delegate 对象,然后再去 hook delegate 的 didSelectRowAtIndexPath: 方法即可。
  4. 对于 RN 页面中的点击,是通过 hook RN 框架中的 RCTUIManager 类的 setJSResponder:blockNativeResponder: 方法,具体原因可以看 这篇文章 的详细讲解。另外,为了避免 SDK 对 RN 框架产生依赖,通过 NSClassFromString(@"RCTUIManager") 来判断当前主工程是否使用了 RN 框架,如果未获取到此类,则不执行 hook 操作。
  5. 对于系统弹窗的点击这块,需要拦截到 UIAlertViewUIActionSheet 以及 iOS8 上新增的 UIAlertController 这 3 个弹窗的点击。对于前2个,只需要 hook 它们的 delegate 方法。而对于 UIAlertController 是没有提供相应的 delegate 方法的,这里可以通过 hook UIAlertAction 类的 actionWithTitle:style:handler: 类方法来拦截到其点击事件。

1. 具体收集的数据

图片 7

第二阶段收集的数据

viewPath 及 viewId 的生成及优化

为了对 APP 中某个页面的某个 view 进行数据收集、统计与分析,首先就需要能够唯一的标识与定位这个视图,这可以说是数据收集 SDK 的一个重要前提。那么怎样去唯一的标识 APP 中的某个 view 呢?SDK 中使用了 viewPathviewId 来完成。

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

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

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

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

我们已经知道,viewPath就是由各节点的类名与深度组成,那么接下来就使用这些信息来表示出 viewPath。下面结合一个具体的示例来简单说一下,我随便从项目中找了一个:

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

HYGHallSlideViewController-UIScrollView-HYGHallProductTableView-UITableViewWrapperView-HYGHallProductCell-UITableViewCellContentView-HYGHallProductView。

路径中各个节点的深度是:0-0-1-0-0:2-0-1

接下来就是将这两者放到一起来构成 viewPath,SDK 的表示方式如下:

viewPath:HYGHallSlideViewController-UIScrollView-HYGHallProductTableView-UITableViewWrapperView-HYGHallProductCell-UITableViewCellContentView-HYGHallProductView & 0-0-1-0-0:2-0-1

其实就是使用 & 连接符简单的拼接到一起。这样做可以方便将两者组合与分离开,便于后面的viewPath匹配。另外,网上还有一种类似于 xPath 的表示方式:

HYGHallSlideViewController[0]/UIScrollView[0]/HYGHallProductTableView[1]/UITableViewWrapperView[0]/HYGHallProductCell[0:2]/UITableViewCellContentView[0]/HYGHallProductView[1]

不过个人觉得xPath的方式稍微复杂了点,在组合以及拆分上都相对麻烦些。不过话说回来,viewPath的形式是次要的,大家可以按照各自喜欢的方式去表示就行,无须纠结于哪种形式更好。

新增埋点依赖App发版,影响数据收集时机。

页面事件的拦截

对于页面事件的收集,主要通过 hook 系统类 UIViewController 的生命周期方法来实现,具体看下图:

图片 8

2. 如何捕获(暂时没有研究)

4.1 优化节点的深度的计算方式

上面提到在计算各节点的深度时,是采用当前 view 位于其父 view 中的所有子 view 中的 index 值。不过在实际的开发中,viewTree 有时候会根据用户的操作有所变动。仍然举个栗子:

  • 假设一个 UIView 中有三个子 view,先后加入的顺序是:label、button1、button2,按照之前的计算方式,这 3 个子 view 的深度依次是:0、1、2。这时候用户点击了一个按钮,label1 从父 view 中被移除了。此时 UIView 只有 2 个子view:button1、button2,而且深度变为了:0、1。如图所示:

图片 9

可以看出仅仅由于其中一个子view 被移除,却导致其它子 view 的深度都发生了变化。因此,SDK 为了在新增/移除某一 view 时,尽量减少对已有 view 的深度的影响,调整了对节点的深度的计算方式:采用当前 view 位于其父 view 中的所有 同类型 子 view 中的index 值。

我们再看一下上面的这个例子,最初 label、button1、button2 的深度依次是:0、0、1。在 label 被移除后,button1、button2 的深度依次为:0、1。可以看出,在这个例子中,label 的移除并未对 button1、button2 的深度造成影响,这种调整后的计算方式在一定程度上增强了 viewPath 的抗干扰性。

另外,调整后的深度的计算方式是依赖于各节点的类型的,因此,此时必须要将各节点的名称放到viewPath中,而不再是仅仅为了增加可读性。

App发版需要埋点工作完成,影响版本进度。

滑动事件 & UIWebView加载事件

对于 iOS 中的滑动事件、UIWebView 的加载事件的收集,SDK 主要 hook 了 setDelegate: 方法以及 UIScrollViewDelegate、UIWebViewDelegate 中的方法。其原理上与 UITableView 的类似。具体见下图:

图片 10

第三阶段:业务埋点解耦,开发圈选功能

4.2 viewPath 针对 Swift 的优化

众所周知,Swift文件在获取其类名时,会自动添加此文件所在的Module名前缀:如果Swift文件在主工程中,则会添加工程的名字;如果是在某个组件中,并且项目开启了 use frameworks! 选项,则会添加组件的名字。总的来说,在含有swift 的项目中(包括纯 swift/OC 与 swift 混编),viewPath中会包含各 Swift 文件的ModuleName,那么在如下情况下:

  • 某个 OC 文件被使用 Swift 重写了
  • 某个 Swift 文件被从主工程移至某个组件库中,或者从组件库移至主工程中
  • 主工程在引用组件库时,在开启与关闭use frameworks!之间进行切换

上述3种情况下,文件的类名都会由于ModuleName而发生变化,进而会导致 viewPath 的改变,工程文件在结构上的调整都可能会直接对viewPath造成影响。

实际开发中,特别是对于较老的OC项目,经常会对项目的OC文件使用Swift重写。因此 SDK 有必要去避免viewPath因为这类情况而发生变化。

其实这个问题的解决方案很简单,既然是由于类名中的ModuleName前缀的改变造成的,那么就干脆在生成viewPath时,去掉所有的SwiftModuleName前缀。这种做法能够解决对viewPath的影响,但是细心的人可能会意识到另一个隐藏的问题:如果在不同的组件库中,两个不同的视图或控制器具有相同的名字(在Swift中是允许的,因为有Module进行区分),这种情况下,viewPath是否存在无法区分的情况?

其实经过仔细考虑,这个担忧有点多余,因为就算两个Module中的视图或控制器名字一样,但是他们里面的视图结构会有所不同,进而深度也不一样,viewPath也不会完全相同。

埋点代码和业务代码耦合在一起,增加代码维护难度。

Event Collector

SDK 通过 AOP 层已经可以拿到执行各个事件的数据收集的时机,接下来就是执行真正的数据收集了,其中包括了对 点击事件的收集、页面事件的收集、滑动事件的收集等。

这些要收集的事件数据中包含一些基本信息,如:eventName、appKey、eventTime、sessionId、deviceId 等。除此之外,还有一些与特定事件相关的信息,例如对于 view 的点击事件,还需要收集与 view 的相关信息;对于列表行的点击,还需要收集点击行的 indexPath 信息;而对于 webView 加载事件则需要收集其 url 与 error 等信息。

接下来主要说一下 SDK 中点击事件的收集。

首先,对于 UIControl 控件与添加了 UITapGestureRecognizer 的 view,在收集它们的点击事件的数据时,重点收集了 2 部分内容:pageName、viewInfo。其中,pageName 是表明点击事件发生在哪个页面,一般用 viewController 的类名表示;viewInfo 是指当前被点击的view的一些相关信息,有:viewClass、viewPath、frame、title、viewId 等。而 viewPath 是最关键的一项信息,能够唯一标识当前 view。

其次,对于导航栏上的点击事件的收集,与上面要收集的信息几乎是一样的,只是在收集 pageName 的数据时不一样。导航栏的点击事件默认的 pageName 是 UINavigationController,但是为了能够更好的分析用户行为,这里将 App 当前正在显示的页面作为其 pageName。

同理,收集系统弹窗的点击事件时,也将 App 当前正在显示的页面作为其 pageName。除此之外,由于同一个页面中可能会出现多个弹窗,它们的按钮文字信息有可能一样,比如经常会用 “确定”、“取消” 等文字,这时单纯靠按钮的 title 无法区分这些不同的弹窗,为了解决这个问题,又加入了系统弹窗的标题(title、message)。

最后,讲一下 SDK 中获取 viewPath 的实现逻辑,具体如下图所示:

图片 11

1. 业务埋点解耦

现如今,业务代码中散落着很多业务无关的埋点代码,最直接的感受就是代码不好看,耦合高,不能复用,维护繁琐。

考虑在全量埋点的基础上,本地维护一个plist文件,当某个页面产生某个点击事件的时候,去这个plist文件里面查找该点击事件是否需要产生业务埋点,然后根据plist中的业务数据的信息获取业务数据,完成上报时机的判断和业务数据的获取。

尽可能通过plist配置来剥离原来的业务埋点,plist文件的具体规则大致与下面介绍的圈选配置文件一样。不过,可能会根据现有业务埋点的一些规律,添加一些更能提高匹配效率的规则。

图片 12

plist文件

4.3 在包含子VC时,优化VC的深度的计算

前面提到,viewPath只表示到距离 view 最近的一个 VC,VC 的深度的计算也是此 VC 的 view 所在的父 view 的所有子 view 中的深度。在实际的 iOS 开发中,可能会经常使用addChildViewController:添加多个子 VC 来实现复杂的页面,但是在包含子 VC 时,VC 的深度计算就有可能会存在问题。还是举一个简单的栗子:

  • 假设一个 containerVC 中包含4个子VC:VC1、VC2、VC3、VC4。在每个子VC首次被展示时,子VC会先被add进来,而子 VC 的 view 也会被 add 到一个scrollView 上。这时候这几个子VC首次的查看顺序的不同将会导致它们的深度的变化:如果查看顺序是:VC1、VC2、VC3、VC4,那么它们的深度依次为:VC1、VC3;如果查看顺序是:VC3、VC1、VC4、VC2,深度则变成了:VC1、VC3。这种情况导致 viewPath 不可靠且无法保证唯一性。

SDK 为了解决上述情况,调整了 VC 的深度的计算:不再采用其 view 的深度,而是直接使用固定的0。因为 VC 已经是viewPath的根级别了,它的深度信息已经不重要了。

不过这种方案会引起另一个小问题,如果上述子 VC 的 VC1 和 VC2 是同一个类的不同实例,那么他们内部的视图结构是完全一样的,这时候如果使用固定的 VC 深度,通过viewPath就无法区分具体是哪个子 VC 的 view 了。针对这种同一类的不同实例,如果想进一步区分它们,SDK 采用了另一个方案:页面别名。

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

经过对viewPath的优化,SDK 已经尽可能的保证了viewPath的稳定性。但是并不表示只依靠viewPath就能区分所有的点击事件。有时同一个viewPath的 view 具有不同的表现形式与作用,例如下面的情况:

  • 同一个按钮在不同的状态下,显示不同的文字。例如:一个按钮在未添加商品前显示“添加”;添加了商品之后,立刻显示成“清除”
  • 同一个view上具有多处点击事件,例如 SegmentControl、UISwitchUIStepper

上面的这2种情况,都是同一个viewPath对应多个事件,此时如果只使用viewPath无法区分出不同的状态或事件。

针对这类问题,SDK 的解决方案是:viewPath “其它信息” 。这里的 “其它信息” 是视不同情况而定的,比如: 在上面的情况1中,“其它信息” 就是按钮的 title。在情况2中,“其它信息” 是 SegmentControl 的 selectedIndex 和 UISwitch 的 isOn 属性的值。SDK 在进行数据收集时,会上传 view 的这些信息,再结合圈选SDK就能让后台在做统计时区分出这些不同的事件了。

关于“其它信息”,再补充一点,除了 SDK 事先知道要获取的信息之外,还有一类就是业务数据。例如:有一个商品列表页,每一行显示一个商品,如果后台想统计的不是列表中每一行的点击,而是每个商品的点击,那么此时的“其它信息”就应该是productId 了。关于 SDK 对业务层数据的获取与上报请看下面的介绍。

如果埋点错误只能更新版本解决(Apple在2017年初全面禁止使用HotFix来修复bug)。

Event Cache

这个模块主要负责所有事件数据的存取及序列化操作,具体可分为如下 3 部分:

  1. 采用双缓存的结构将数据存储在内存中。具体实现是,将新添加的事件数据先存储到全局数组 eventArray 中,等满足数据上报条件时,从 eventArray 中读出一部分数据并随机生成一个唯一的 eventsID,将其以 key-value 的形式存放到全局字典 popedEventDict 中,等这部分数据上传成功后再将 eventsID 对应项从 popedEventDict 中移除。
  2. 在某些情况下(App 即将被杀死、程序抛出异常),将内存中的数据以文件的形式持久化存储至磁盘中,以防数据丢失。
  3. 将从内存或文件中读取的数据执行 protobuf 序列化操作,以便后续的数据上传操作。

另外,为了确保对数据存取的多线程安全,上述操作全部都放到了同一个串行队列中执行。

2. 开发圈选功能

SDK无埋点业务数据收集的实现

讲完了 viewPath 之后,接下来详细介绍下 SDK 的另一个关键技术:基于 viewPathKVC 实现 SDK 的无埋点业务数据收集功能。首先,先简单分析一下传统的 代码埋点 存在的缺点,大致有以下几个:

  • 埋点代码与业务逻辑代码混合在一起,增加了代码的维护成本;
  • 埋点代码需要跟随APP版本一起发布,耽误数据的收集与统计;
  • 埋点时存在错埋、漏埋等情况,无法动态更新及添加;

为了解决上述的 代码埋点 的缺陷,SDK 实现了真正意义上的 无埋点 来对业务数据进行收集。

SDK 的无埋点功能的实现主要依赖于 viewPathKVCviewPath前面已经介绍了,它主要用于标识viewTree中的某个 view。而KVC对于 iOS 开发者也不陌生,堪称 iOS 开发中的黑魔法之一。通过KVC我们能够通过 key 或 keyPath 直接访问对象的属性,而不需要调用明确的存取方法。关于KVC如果不太了解,请自行学习,这里不再过多阐述。

那么如何实现不需要代码埋点就能随意获取想要的业务数据呢?先看一下 SDK 的无埋点技术的整体架构图:

图片 13

从上图可以看出,在实现 SDK 的无埋点数据收集时,主要分为3步:上传KVC配置、请求KVC配置、业务数据的收集与上报。

在上图中出现了 KVC配置,那么下面先简单介绍下什么是KVC配置。其实 KVC配置 就是一些用来描述 App 应该在什么时机去收集什么数据的信息,包含的主要信息有:

  • appKey:用来标识是哪个应用
  • appVersion:用来标识应用的版本号
  • viewEvent:标识某个事件类型,例如:ButtonClick、ListItemClick、ViewTap等
  • viewPath:目标 view 在viewTree中的信息
  • keyPath:目标 view 与要收集的业务数据间的关联路径,用于KVC取值
  • keyName:为要收集的业务数据定义一个key,最终组成 key-value 的形式上报。用于区分多个收集的数据
  • 上传KVC配置

    • 利用 圈选SDK 上传 KVC配置 的操作对于用户是透明的,主要由开发人员进行上传与管理。此操作可以在任何时候进行,在想要收集某个或某些版本的 App 中的业务数据时,上传相应的KVC配置信息至后台即可,达到了根据需要动态可配的效果。
  • 请求KVC配置

    • SDK 在初始化时会触发 KVC配置 的请求操作,从后台拉取 App 当前版本对应的所有KVC配置,并将请求结果缓存起来,以提供给下一步使用。

这一部分是 SDK 无埋点技术的核心,接下来详细介绍这部分的实现逻辑。它的实现流程如下:

图片 14

这个环节的核心是基于viewPath的 view 匹配,主要实现是通过循环遍历viewPath的每个节点的信息与当前 view 及其父view 依次进行匹配。因此这一步会产生一定的时间与性能消耗。为了尽可能减少这部分的操作,SDK 中使用了一些方式进行优化,其中一个就是基于缓存view的优化。

为了解决这些问题,我们调研了市面上的方案,在调研过程中,我们发现很多公司都看到了这些问题,他们也提出了自己的解决方案,基本上都是围绕可视化埋点方案来做的。这种方案好处是,埋点提报方式和数据后台基本不需要修改,风险也比较可控。而无埋点方案由于全部数据都收集,造成数据量巨大,这给服务器和网络传输带来较大负载,另外数据清洗难度也非常大,基于这些原因,大部分公司都没有选择这种方案。基于这些这些原因和结合我们的场景后,我们选择了可视化埋点方案来解决代码埋点的问题。

Event Upload

这个模块的主要功能就是根据一定的数据上报策略,上报已收集的所有事件数据。数据上报主要包括对内存数据和本地文件这2部分,下面分别介绍一下它们的上报策略与实现思路。

首先,针对内存数据的上报策略有 2 个:

  1. 每隔 30 秒
  2. 每累积 10 条数据。

当满足上述条件之一时,会触发从内存中读取数据,并执行上传操作。对于内存数据的上传,单独创建了一个并发队列,并限制其最大并发数为 10,以防由于数据频繁时上报引起开启的线程数太多。

为了尽早的上传本地文件,以防用户卸载 App 造成本地数据的丢失,针对本地文件的上传策略有如下 3 个:

  1. App 冷启动
  2. App 进入前台
  3. App 进入后台

这里创建了一个单独的串行队列,来实现对本地文件进行逐个上传,即等上一个文件上传成功后,再触发下一个文件的上传。因此,上述 3 个触发时机并不会造成文件的重复上传,并以较小的代价完成本地文件的上传。

2.1 圈选功能的大致流程

图片 15

圈选的大致流程

4.1 基于缓存view的优化

SDK 采用缓存上一次匹配成功的 view 信息的方式,来减少一些不必要的viewPath匹配操作。这里主要缓存的 view 信息有:

  • targetView:上一次通过viewPath匹配成功的 view 对象。
  • indexPath: 上一次通过viewPath匹配成功的 view 的indexPath,如果没有则为nil。

可视化埋点并不是摈弃了代码埋点,而是在代码埋点的上层封装的一套逻辑来代替手工埋点,大体上架构如下图:

数据存取与上传的实现流程

其实上面已经讲了大致的实现思路,里面设计到了使用 GCD 队列来控制数据上传与保证多线程安全。为了更清晰的展示出这 2 部分的实现逻辑,简单画了一个流程图展示出来:

图片 16

本篇文章主要介绍了无埋点数据 SDK 的整体设计,以及各个模块的功能和实现思路,其中重点介绍了执行事件收集所需 hook 的具体方法,和事件数据的存取与上报功能的实现流程。如果对本文有问题,请留言评论。

2.2 圈选成功得到的配置文件

{
  @"xx1ViewController" : @{ // 所在page类名
    @"buttonClick" : @[ // 事件类型
          @"viewPath1" :  @{order.orderID} // key为view对应的viewPath,value为业务数据的KVC path
          @"viewPath2" :  @{dataPath}
    ],
    @"tableSelect" : @[...],
    @"alertClick" :  @[...],
      ...
  },
  @"xx2ViewController" : @{...}, 
}
1. viewEvent 匹配

第一步先进行事件类型的匹配。如果KVC配置信息指定的 viewEvent 是 ButtonClick,那么可以轻松的过滤掉 ListItemClick、ViewTap 等其它事件。这一步能够过滤一大部分事件,只有事件类型匹配成功才继续进行下一步。

图片 17

2.3 如何使用这个配置文件呢?

图片 18

匹配配置文件

这部分有两个问题要关注:

  • 对viewPath匹配的优化。
  • 对KVC获取业务数据的健壮性处理。

  • iOS无埋点数据SDK的整体设计与技术实现
  • Android无埋点数据收集SDK关键技术
  • SDK无埋点技术在百分点的探索和实践
  • 网易HubbleData无埋点SDK在iOS端的设计与实现
2. targetView 匹配

接下来就是将缓存的 targetView 与当前 view 进行比较。如果两者指向同一对象,则进行第3步,否则直接进入第4步

不过要实现可视化埋点也有很多问题需要解决,比如如何确定页面元素的唯一标识、如何携带业务参数、如何添加有判断逻辑的埋点和配置信息的版本管理。下面我们会整体介绍可视化埋点的使用方式和技术细节,另外针对上面的问题我们会尝试给出解决方案和一些思考。

3. indexPath 匹配

有人可能不明白为何要添加这一步呢?其实这一步也很重要,是对第2步的补充,主要是用来处理 Cell 可复用性的情况。

如果第2步中缓存的 targetView 是 Cell 或 Cell 中的某个 subview,那么第2步的匹配成功,并不能保证当前 view 就是我们真正想匹配的 view。这个可能不太容易理解,还是举个简单的例子来说明一下:

  • 假如一个 Cell 中有一个 button,在第1行的 button 被点击时,通过viewPath匹配成功了,那么这时 targetView 缓存了第1行的 button 对象。接下来向下滑动列表,第一行被划出屏幕,第10行划入屏幕,同时第10行复用了第1行的 Cell,这时再点击 button 去匹配时,由于 Cell 复用的原因,targetView 与当前 button 肯定指向同一个对象,但是却不是我们真正想匹配的第1行的 button。可以看出:在有 Cell 复用的情况下,无法确定第2步的结果一定正确。

因此,在第2步的基础上又增加了indexPath匹配。indexPath的匹配逻辑为:如果缓存的indexPath不为nil并且与当前view的indexPath不相等,则进入第4步;否则表明当前的 view 就是上次刚刚匹配成功的,也就没必要进行viewPath匹配,可以直接进入第5步。

整体概览的介绍分为2个部分:产品原型概览和技术原理概览。首先介绍产品原型概览,可以更直观了解可视化埋点的基本运作流程。

4. viewPath 匹配

这一步就是对当前的 view 及其父view 与KVC配置中的viewPath的各个节点进行逐个匹配。由于是一个循环操作,因此会有一定的时间消耗,其实在这部分的匹配中,也做了一些简单的优化。在真正进入循环匹配之前,先进行如下3步判断:

  • 判断 view 类名是否相等;
  • 判断 view 所在的 viewController 类名是否相等;
  • 判断 view 所在的 window 类名是否相等;

上述的3个判断也能过滤很多不必要的匹配。只有这3个判断均通过后,才进行viewPath循环匹配。

产品原型概览

5. KVC 取值与上报

到了这一步,就已经验证了数据收集的时机是正确的。接下来就可以直接使用 KVC配置信息中的keyPath调用 valueForKeyPath: 方法获取对应的值。如果值不为nil,就与 keyName 组成一个键值对,放到当前的事件数据中一起上报上去。这样后台就可以通过key去查找到相应的业务数据了。

上面只是简要介绍了一下匹配时的逻辑,在实际开发中还会添加对 cell 的indexPath通配的情况的处理,由于文章篇幅这里不再详细讲解。

首先在App中嵌入可视化埋点SDK。当开启圈选开关之后,会在屏幕上始终悬浮一个圈选开关,用于埋点维护人员采集埋点配置信息。如下图所示:

5. 增加对 KVC 的异常处理

SDK 的无埋点功能的实现其实主要依赖于KVC,但是众所周知,KVC是非常危险的,很容易造成程序崩溃。例如一旦 key 或 keyPath 所对应的属性名不存在,立刻会导致程序抛出一个NSUndefinedKeyException异常,如果应用没有处理此异常,程序就会Crash。

因此,为了避免程序Crash,SDK 内部增加了对KVC异常的处理。具体实现是给 NSObject 增加一个 Category ,重写 valueForUndefinedKey: 方法,并在方法中return nil

@implementation NSObject (KVCExceptionHandler)- (nullable id)valueForUndefinedKey:(NSString *)key{ return nil;}@end

图片 19

其它关键技术

当然,SDK 的实现中还有很多关键技术点,比如:SDK 对 RN 页面的数据收集、页面别名方案的实现、Method SwizzlingAspects的兼容等。由于本文的篇幅已经很长了,而且考虑到大家读文章的耐性都不会太长,所以这里就先不讲解了,后续会再写文章单独介绍。

圈选开关的按钮一共有3个,当选择圈选按钮的时候,点击页面上的元素,SDK会拦截点击事件,弹出一个用于收集配置信息的视图。检测按钮用于版本管理,下文再详细介绍。关闭按钮用于关闭圈选功能,可以正常的操作App页面元素。

END

文章写了这么多,其实主要介绍了 SDK 中的两个关键技术点,希望对你们能有一些参考价值。另外,如果有人对本文的方案有更好的建议,欢迎一起讨论学习。

最后,要特别感谢我的同事王佳乐,由于他对文章的排版与校对工作,才使得本文能更好的展示给大家。同时也要感谢组内的所有同事,在我开发遇到困难时,给予了我很多的帮助。

开启圈选开关,选择页面元素进行埋点配置采集。例如,点击上图所示页面右下方的加入购物车按钮,弹出配置视图,如下图所示:

Q & A

关于对本文内容提出的一些问题,将全部记录在这里,并进行统一解答。

一般来说,上传的所有的 KVC配置 需要与 App 的版本相对应,因为 App 版本不同会直接导致keyPath可能不一样。所以与 KVC配置 相关的工作有如下2个:

  1. 针对当前 App 版本上传相应的 KVC配置,以获取想要的业务数据
  2. 当 App 新版本发布时,需要对之前版本上的 KVC配置 逐一验证,是否仍然适用于新版本。如果仍然适用,则直接在管理后台上把新的版本号添加到此 KVC配置;如果不再适用,则对新版本再上传一个新的KVC配置。

从上面可以看出,在 App 版本不断迭代的过程中,KVC配置 会越来越多,相应的维护与管理工作也相当繁琐。

为了解决这个痛点,SDK 中增加了一种方案来避免这种重复且繁琐的工作。具体的方案是:

  • 在上传 KVC 配置时,指定某个区间的版本,或者不指定具体的版本(即应用到当前所有版本上);
  • SDK 在使用KVC配置获取业务数据失败时,添加相关的错误日志,并上报上去。其中错误日志里包含了appKeyappVersionkeyPath等信息,这样就能在后台清晰的看到哪些 KVC配置 在哪个 App 版本上存在问题;
  • 使用脚本监控与KVC相关的错误日志。如果监控到有错误日志上报,则发送邮件通知给相关人员;

因此,SDK 采用此方案优化之后,KVC配置 的管理工作就只有1个了:

  • 根据Log信息快速找到对应的 KVC配置,并上传一个针对新版本的 KVC配置

这个问题在实际产品中也比较常见,比如 App 首页的内容大多是通过后台配置的。这个问题其实可以转化或分解成如下的2个情况:

  • 同一位置会显示不同的内容
  • 同一内容会显示在不同的位置

注意,这2个并非同一个,它们分别对应于不同的场景,同时数据收集的方案也有所不同。

另外,“位置” 可以是在列表中,也可以是非列表中的,不过这个对整体的方案没有太大影响,仅仅是在不关心位置时viewPath中的通配符位置不同。

图片 20

A2.1 同一位置显示不同的内容

例子:在 App 首页有一个展示最近活动的位置,先展示活动1的图片,过一段时间运营人员又配成活动2的图片。如何统计活动1、活动2各自的点击量?

针对这种场景,SDK 的解决方案是:“关心位置” “关心内容”“关心位置” 的意思是只使用当前的位置,具体表现是viewPath中不包含任何通配符;“关心内容” 的意思是指定一个想要统计的内容。

整个过程可以分解为如下3个环节:

  • 圈选SDK上传“关心位置”的KVC配置。KVC配置中指定获取活动的urlkeyPath
  • 数据SDK在活动发生点击时,收集当前活动对应的url,并跟随点击事件一起上报。
  • 圈选SDK上传“关心位置” “关心内容”的圈选配置,关心的内容指定为想要统计的活动的url值。

视图会展示一些信息,其中最重要的是SDK生成的唯一标识符,用于对埋点进行标识。埋点维护人员需要填写eventId,选择一些要上报的数据字段等操作。上图左上角的增加按钮,用于一个点击事件有多个埋点的需求,点击增加,会在下方新增一个信息采集视图,以供埋点采集。上报类型是跟我们具体业务相关的,可以忽略。页面参数、事件参数的选择,会在下文说明携带上报数据部分的思路介绍。总之,采集完毕会形成一条配置信息,上传到服务器。采集完成全部的配置信息,形成一个埋点配置列表。

A2.2 同一内容显示在不同的位置

例子:App 首页有4个固定的入口,假设其中一个叫“热门推荐”,那么根据后台配置的顺序不同,“热门推荐”可能被显示在4个位置中的任何1个,即一段时间显示在第1个,过一段时间可能显示在第2个位置。这时如何统计出“热门推荐”的点击量?

针对这种场景,SDK 的解决方案是:“不关心位置” “关心内容”“不关心位置” 是指viewPath中含有通配符,用于表示viewTree中的多个位置。例如想要匹配列表所有行时,则将viewPath中的indexPath替换为通配符。

这个问题的解决过程也分为如下3步:

  • 圈选SDK上传“不关心位置”的KVC配置。KVC配置中指定获取入口的 title 的keyPath
  • 数据SDK在4个中任何一个入口被点击时,都去收集入口的 title,并跟随点击事件一起上报。
  • 圈选SDK上传“不关心位置” “关心内容”的圈选配置,关心的内容指定为“热门推荐”。

到这里,数据收集与圈选配置的工作都已经做完了,接下来就是后台的数据统计了。上述2种情况对后台进行统计没有区别,都使用一个统计方案,这里也介绍一下后台大概的统计思路:

  • 拿到第3步中上传的圈选配置,根据viewPath“关心的内容” 生成一个正则表达式,然后从数据 SDK 上报的原始数据中进行正则匹配,进而统计出相应数据。

在用户启动App时,埋点配置列表会被下载下来。当用户点击加入购物车按钮时,SDK会使用和上文中配置采集阶段相同的方法,生成唯一标识符,用于在埋点配置列表查找相关配置项,如果匹配成功,则利用这些配置数据,自动的进行埋点上报。

整体的产品原型概览先介绍到这,下面看一下技术原理概览。

技术原理概览

采用AOP(Aspect-Oriented-Programming)即面向切面编程的思想,基于 Runtime 的 Method Swizzling能力,来 hook 相应的方法,从而在hook方法中进行统一的埋点处理。例如所有的按钮被点击时,都会触发UIApplication的sendAction方法,我们hook这个方法,即可拦截所有按钮的点击事件。

这里的处理分为2个部分:采集埋点配置信息,和真实的埋点数据上报。这个和上文产品原型概览部分介绍的处理流程相对应。

以按钮点击事件的处理为例,大致的流程如下图所示:

图片 21

这里仅仅是以按钮为例说明,UITableView、UICollectionView、UIView的手势等等,都是同样的处理逻辑,对可视化埋点有过研究的人应该都了解这个过程,这里不再过多阐述。下面来详细的探讨SDK的关键模块的技术实现思路。

关键模块实现思路

我们要讨论的SDK的关键模块分为3个部分:生成唯一标识符、埋点数据携带、版本管理。其余部分,例如hook的具体实现、数据的上传、下载匹配、圈选工具的交互等,虽然也都是需要解决一些技术问题,但是都有比较清晰的实现方案,这些方面不作讨论。下面来看第一个问题。

唯一标识符

市面上可视化埋点方案,大多都使用viewPath生成唯一标识符。我们知道App的视图层次是一个树状结构。一个 view可以被认为是一个节点,处于视图树的某一个位置,从根节点到这个view节点的深度信息构成了一个path,用来唯一标识该view。

如下图所示,

图片 22

^ 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如下:

图片 23

举个例子:假如有一个按钮,在condition1的情况下要执行doSom1这件事情,在condition2的情况下要执行doSom2这件事情

图片 24

那么开发者要让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如下所示:

图片 25

target或者是controller实现协议,如下所示:

图片 26

这种方式还便于采集配置,在圈选的时候,SDK会自动调用target和controller的这个方法,并且把所有的key值显示出来,采集字段的时候,直接选择某一个key即可,如下图所示:

图片 27

当选择某一个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把相关视图加载出来之后,在用同样的方式来进行检测。

检测完毕后,会弹出一个版本管理的视图。按照所有埋点、有效埋点、无效埋点三种类型列举出所有的配置项。然后针对无效埋点进行确认和相关处理即可。

图片 28

总结与展望

我们的可视化埋点探索的技术方案先介绍到这里。对可视化埋点进行过深入研究之后,会发现上面介绍的这些问题处理起来比较困难,上面仅仅是介绍我们的方案和思考。除了支持App发版后新增埋点的能力,我们特别希望通过这种方式得到埋点效率提升,把开发者从体力活中解放出来。从当下来看,如果数据携带依然需要一系列逻辑处理的话,想通过自动化埋点的方式获得效率提升还是比较有限的。同时,把从前代码实现的埋点,替换成可视化圈选的方式,虽然解决了埋点代码和业务逻辑耦合的问题,但似乎像是一种人力成本的迁移,毕竟圈选采集信息还是依赖于人工处理。

计算机本身的目的之一就是解决一些重复的繁琐的事务。当下代码埋点的成本很高,不仅仅是开发者,许多方面都要投入大量的时间和精力维护。所以我们相信自动化埋点这个需求,会驱动更多人持续不断地研究,不断地提出新的思路和解决方案,最后有一天实现真正高效的自动化埋点。

本文由新葡京8455发布,转载请注明来源

关键词: