iOS无埋点数据SDK实践之路

作者: zerygao | 来源:发表于2017-02-21 19:41 被阅读25719次

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

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

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

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

    SDK的整体实现思路

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

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

    系统类的方法

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

    系统类的 Delegate 方法

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

    自定义类的方法

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

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

    SDK的关键技术的实现

    viewPath 及 viewId 的生成及优化

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

    1. viewPath 的组成

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

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

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

    2. UITableViewCell/UICollectionCell 的深度表示

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

    3. viewPath 的表示形式与示例

    我们已经知道,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的形式是次要的,大家可以按照各自喜欢的方式去表示就行,无须纠结于哪种形式更好。

    4.针对 viewPath 的优化

    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。如图所示:

    可以看出仅仅由于其中一个子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中,而不再是仅仅为了增加可读性。

    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也不会完全相同。

    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(0)、VC2(1)、VC3(2)、VC4(3);如果查看顺序是:VC3、VC1、VC4、VC2,深度则变成了:VC1(1)、VC2(3)、VC3(0)、VC4(2)。这种情况导致 viewPath 不可靠且无法保证唯一性。

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

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

    5. viewId 的生成

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

    6. viewPath 与 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 对业务层数据的获取与上报请看下面的介绍。

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

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

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

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

    1. 无埋点的实现架构

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

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

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

    2. 什么是 KVC 配置

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

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

    3. KVC配置的上传与下发

    • 上传KVC配置

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

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

    4. 业务数据的收集与上报

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

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

    4.1 基于缓存view的优化

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

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

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

    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步。

    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通配的情况的处理,由于文章篇幅这里不再详细讲解。

    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
    

    其它关键技术

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

    END

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

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

    Q & A

    关于对本文内容提出的一些问题,将全部记录在这里(简书评论里的除外),并进行统一解答。

    Q1: SDK 都使用KVC配置获取业务数据,是否会增加维护KVC配置的工作?

    A1: 会有对 KVC配置 的维护与管理工作,不过 SDK 也简化了这块的管理工作。

    一般来说,上传的所有的 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配置

    Q2: 对于 “内容与位置” 可能会随时间而变动时,如何实现数据收集与统计?

    A2: 使用圈选SDK与数据SDK共同完成动态数据的收集与统计

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

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

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

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

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

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

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

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

    • 圈选SDK上传“关心位置”的KVC配置。KVC配置中指定获取活动的urlkeyPath
    • 数据SDK在活动发生点击时,收集当前活动对应的url,并跟随点击事件一起上报。
    • 圈选SDK上传“关心位置” + “关心内容”的圈选配置,关心的内容指定为想要统计的活动的url值。
    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 上报的原始数据中进行正则匹配,进而统计出相应数据。

    相关文章

      网友评论

      • 镜头下的涂鸦:突然想到一个问题请教下,你去每次匹配配置文件信息的时候,这时配置文件中的信息肯定是读取到内存中的,这块数据怎么处理的,一直在内存中呢,还是怎么处理。如果一直在内存中的话是不是太占资源,如果不在内存中的话每次的IO 是不是也要好资源,请问下这块要怎么处理
      • 黄成:hook从本质上来说就不是一个完美的方案
        击水湘江:有更好的解决方案嘛?
      • 开飞机的叔客:厉害了我的哥,看了好多遍😆懂了个大概了
      • 黄成:问一个问题,如果在项目里面用到了rx,uitableview的delegate拿到之后没有实现tableviewdidselected,是使用forwardingInvocation做的,请问,如何拿到他的点击事件?
        GhostClock:@奔小康 那如果想在block的回调里面加埋点,应该怎么hook?
        f75dfaf80e43:@黄成 hook tableview的delegate
      • js丶:又重新读了一遍,viewPath + “其它信息”,【“其它信息” 按钮的 title】,viewId只要是包含按钮信息的版本迭代统计埋点数据可能会出现不准确的问题,并且按钮信息不适用于局部变量,还有个问题想请教下,无埋点SDK能做到像有埋点一样为控件设置固定的viewId吗,如果不能的话完全无埋点的方案基本不可能实现吧,还是无埋点数据收集方面有啥奇淫技巧
      • js丶:请问楼主目前实现的方案,埋点方面需要测试吗,面对版本迭代比较快的一些项目,收集到的数据准确率如何
      • js丶:文章很棒,有个问题想请教下,假设1.0版本UIView有两个子view,分别是Button1 Button2,1.1版本UIView有三个子view,分别是Button0 Button1 Button2,
        如果是根据同类型子view中的index值来标识viewPath,版本迭代收集到的埋点数据是否会不准确
      • iOS排头兵:如果业务数据不是对象的属性能获取到吗
        zerygao:获取不到:joy:
      • 18b27226d0a8:想请问下,服务端是怎么根据上传的viewPath来关联对应的业务事件,难道服务端也有套相应的解析策略?
        zerygao:@CZP_3186 需要有一个相对应的圈选工具对关心的元素进行圈选配置,这样后台就知道如何进行匹配与统计了
      • 雲_在晴天:楼主你好,请问,需要传参数的点,是怎么统计到的,怎么拿到这个参数的,这个参数可能有一些逻辑。在传统的手动打点时,这个参数就是一个局部变量,这种值,无埋点方案是怎么获取到这个变量的?
      • 太多太多黄子豪:你好 我想问下 对于dealloc方法上试过简单的hook可是在别的地方重写dealloc就会发生死循环 对于这个问题能提供一下思路吗 谢谢~~
      • MccReeee:你好,文章受益匪浅.有个核心问题想请教,我在我的SDK中已经拿到了当前屏幕中的viewTree和每个控件的deep信息. 请问这个viewTree和深度信息组合后作为控件唯一标识后上传给服务器, 我再其他设备上再获取到这个唯一标识后该如何还原并再次定位到这个控件呢?
        MccReeee:@zerygao 你好这个问题已经解决了,现在很想知道 圈选SDK的实现大致原理
        zerygao:@马上码 没太明白你的问题,根据控件的viewTree信息就能定位或者判断是否为同一控件了吧
      • c1482b09ec8a:楼主,请问这种方式对游戏来说是否通用?有点疑惑就是统计游戏行为,比如升级,任务,虚拟币、充值这些行为貌似并不能用无埋点来获取吧,还是得由cp通过u3d的c#来调用(代码埋点),是这样么?求解惑。
      • 5e2596f93fba:iOS端可以通过KVC拿业务数据,Android端呢?
        zerygao:@colordance_820c android端也实现了一套类似的机制
      • wokenshin:感觉好高大上!我现在做的太low了,就是直接在接口内部做了最简单的埋点。感谢分享!
      • 乐视薯片:你好,请教个问题,视图的viewPath是点击的时候遍历获取的吗?
      • ifelseboyxx:思路棒极了,希望有开源的一天!怒赞!
      • jamesLoveCode:你好,想问一下,关于一些逻辑判断你是怎么处理的?比如说点击了一个按钮时,要判断当前的登录状态,如果登录了就发送,否则不是发生该次埋点,你是怎么处理这种逻辑的?还有一更复杂的逻辑需要判断时怎么处理?
      • 乐视薯片:你好,我想请问一下,这算是可视化埋点吗?
      • 偏偏就是祢:也就是你们圈选SDK如何自动生成需要埋点控件的路径,并且如何更新呢?望指点一二
        偏偏就是祢:你的圈选sdk在生成path的时候需要每种情况都点到吗?这个工作是谁处理,并且有的场景不是那么容易模拟出来,请问你们又是怎么处理的?
      • 偏偏就是祢:要是能提供一个简单的demo描述一下就完美了!
        zerygao:@一颗浮躁的心想静下来 每种情况都点到是什么意思?是指在测试这个功能的时候?
        zerygao:@一颗浮躁的心想静下来 我前一段时间写的另一篇文章中介绍了 path 的生成流程,你可以去看一下 http://www.jianshu.com/p/5f16e1de6d5a
        偏偏就是祢:能讲一下圈选部分的path是怎么自动生成的吗?多谢🙏
      • 阁子菌:大大,请教下一种情况下的埋点记录。
        我定义了一个按钮,并添加了一个testClickAction的点击事件,testClickAction方法中调用了testBLockOperation:的方法,我需要在testBLockOperation:方法中的block执行完成后记录一个埋点数据。那我该怎么去配置KVC呢?
        [button addTarget:self action:@selector(testClickAction) forControlEvents:(UIControlEventTouchUpInside)];

        - (void)testClickAction {
        [self testBLockOperation:^{
        NSLog(@"回调成功");
        //需要记录这个时候的埋点
        }];
        }

        - (void)testBLockOperation:(void(^)())testBlock {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (testBlock) {
        testBlock();
        }
        });
        });
        }
        zerygao:@阁子菌 我建议这种情况就使用代码埋点的方式吧,不能因为要收集数据就丢弃 block 的方式…你觉得呢
        阁子菌:@zerygao 我也没有找到方法在这种情况下收集数据,我在想能不能通过代理方式来代替Block的形式去收集业务数据。
        zerygao:@阁子菌 应该拿不到这个时机吧?只能拿到 testClickAction 执行前后的时机去收集业务数据,是吧?
      • Bob林:hi,cell 的 indexPath , 当圈选触摸到的 view 的时候,你们是通过 indexPathForCell 或者 indexPathForRowAtPoint 来获取的吗?
        UIView *tempView;
        if ([subView isKindOfClass:[UITableViewCell class]]) {
        tempView = subView;
        while ((subView = [subView superview])) {
        if ([subView isKindOfClass:[UITableView class]]) {
        NSIndexPath *indexPath = [(UITableView *)subView indexPathForCell:(UITableViewCell *)tempView];
        obj.index = [NSString stringWithFormat:@"%ld:%ld",indexPath.section,indexPath.row];
        }
        }
        }那么对于tableView section 自定义的 view ,这块的 indexPatch,是怎么获取到的的?-(UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section{
        UIButton *btn = [[UIButton alloc]init];
        btn.frame = CGRectMake(0, 0, 0, 100);
        btn.backgroundColor = [UIColor darkGrayColor];
        [btn setTitle:[NSString stringWithFormat:@"%ld",section] forState:UIControlStateNormal];
        return btn;
        }回复的排版不太好看~
      • godLoveYao:请教一个问题,请问下你们的SDK支持的埋点控件有哪些,对于通过UITapGestureRecognizer实现的点击按钮能否做到支持呢,谢谢。
        godLoveYao:@zerygao 这两天刚把手势功能支持了,有一个问题,对于一个大view(比如view1),view1添加单击手势,然后在view1上添加view2、view3(假设位置分别在view1的左右各半部分),在view1的手势触发函数里通过point判断点击的是view2还是view3,然后处理对应的逻辑,那么对于view2,和view3这种可以做到支持埋点吗。假如可以,可否提供下思路。谢谢,
        zerygao:@godLoveYao 支持所有 UIControl 控件与添加了点击收拾的 UIView
      • 7cebd2f7f570:希望可以早日开源
        zerygao:@chaoimei 多谢支持:smile:
      • jessyZu:请问下你们ios 和 android 的控件的唯一标识 怎么在服务端统一的?
        zerygao:@jessyZu 这两个平台并未在服务端统一,是区分平台管理的
      • 轰炸机上调鸡尾酒:突然想到一个问题,你这里的其他信息,还不能是局部变量,怎么得是一个成员变量或者属性吧。不然KVC拿不到啊:flushed:
        Dawn_wdf:我看到这里的时候想的也是@轰炸机上调鸡尾酒的这个方案。如果这样做,感觉有埋点需求的对象就必须要有一个专门用来收集埋点信息的属性,如果一个VC的埋点需求多,或者需要埋点的view只是一个临时的,仅供内部使用的简单view,那是不是要专门为了埋点来给它添加一个属性。感觉这样的实现方式对原代码的侵入性更大。不知道作者实现思路是什么样的?
        轰炸机上调鸡尾酒:@zerygao 感谢作者回复,没想到回复这么快。不过,我说的详细一点吧,“其他信息”我指的是 “在用户点击提交订单按钮时,收集用户购买的物品以及订单总金额的数据”这中业务中的上下文数据(类似orderId,orderName,oderAmount)。我理解的是,将orderId,orderName,oderAmount组装成一个字典,这个字典必须是某个具体View或者controller的属性。可以这样理解么?:stuck_out_tongue:
        zerygao:@轰炸机上调鸡尾酒 是的
      • 3efefee71c8c:SDK开源么,大神。。非常想看看实现
        zerygao:@听_说 很遗憾,目前公司还未有开源计划
      • 卟师:大腿,我能转载分享吗?我会标注上作者和出处的
        zerygao:@卟师 好的,谢谢!
        卟师:@zerygao 有好文我可以帮你推推,我放在微信公众号:iOS面向编码
        你可以去看看
        zerygao:@卟师 可以,欢迎转载!
      • ampire_dan:你好,有个问题请教。iOS 的视图架构里面有很多是苹果自己的私有视图,我要如何剔除这些呢?(比如 ViewController 的 wrapperView ,Tabeview 的 wrapper view,UINavigationTransitionView 等等这些)想过手动剔除,但是这样的话,苹果自己更新 UIKit 的时候 SDK 也要专门测试,而且对苹果私有视图不熟悉的话,可能做出来问题会比较多。求问作者是怎么处理这种的?
        ampire_dan:@zerygao 求问一下,method swizzle 和 aspect 的冲突要怎么解决呢?我网上找了好些,但是作为 SDK 的解决方法没有找到呢
        ampire_dan:@zerygao 好的,多谢回复
        zerygao:@ampire_dan 这个问题很不错!对于这个我们这边也没找到好的方案,所以目前并未做剔除工作,如果你找到好的方案了记得分享下啊:stuck_out_tongue_winking_eye:
      • 我就叫Tom怎么了:节点树的0-0-0-0-2是不可靠的,当界面发生跳转时.所谓的节点树(谓词),就会发生变化.造成无法唯一识别.
      • 余温夏暖:有很认真的看完,感谢分享,学到了不少。
        zerygao:@余温下暖 感谢支持!
      • BigBossZhu:很棒
      • hard_coder:请问楼主可以提供demo吗
        csxfno21:@hard_coder 可以使用消息转发机制来进行消息转发,原理也是通过hook代理方法
        zerygao:@hard_coder tableView 的代理方法在运行时是能够替换的,具体实现上可分为2步:(1)先 hook UITableView 的 setDelegate: 方法;(2)hook delegate 对象的方法,比如:tableView:didSelectRowAtIndexPath: 方法。
        hard_coder:你好,你的tableView的代理方法也是使用的运行时进行替换的吗,tableView的代理方法运行时是切不到的
      • Bob林:楼主可以在有空的时候出个简单 demo 吗
      • 熊猫人和熊猫君:@ zerygao 为什么 viewId 不直接拼接 其他信息作为一个整体字符串来md5 计算呢
      • 熊猫人和熊猫君:页面别名?是这么实现的呢 能不能聊下呢?
        hard_coder:你好,你的viewPath是什么意思?还有树的深度,viewID是怎么设置的
        熊猫人和熊猫君:@zerygao 期待 希望 博主早日放出
        zerygao:@ganvinalix 这个会在下篇文章中详细介绍,请关注
      • 萧城x:准备使用一下 就是不知道相比传统的埋点
        维护kvc 的成本会不会对应的下降
        zerygao:@低调做事 会的,而且相比传统埋点,能够做到动态可配、统一管理,优势是很大的!:stuck_out_tongue_winking_eye:
      • 三十一_iOS:期待后续
      • 大脸猫xiao3:同样在公司做了可视化圈选和侵入式数据收集,楼主非侵入的数据收集方式和获取唯一viewPath的思路很值得学习,之前非侵入式的方案由于时间问题也没深入探究,在这里学习到了好的思路。期待更多更细的分享。有个问题,想知道楼主获取当前view的思路。
        zerygao:@大脸猫xiao3 在 hook 的方法中能够拿到响应当前事件的view
      • 624e69be3ebb:文章中的思路很赞!干货!期待后续的文章!:+1:🏻
        zerygao:@一片丹妮 感谢支持:grin:
      • hitlerstar:请问楼主,什么是圈选SDK,不太理解,还有路径中各个节点的深度是:0-0-0-0-0:2-0-1是什么意思,望指导,还有方便加一下联系方式么,最近在做这方面的需求,可以请教一下么
        MTSu1e丶:您需要根据业务需求,将需要分析的关键元素告诉我们。这个过程,就是圈选
        Carson_26a5:HYGHallSlideViewController[0]/UIScrollView[0]/HYGHallProductTableView[1]/UITableViewWrapperView[0]/HYGHallProductCell[0:2]/UITableViewCellContentView[0]/HYGHallProductView[1] 这个你应该看得懂 。。层级关系。0:2表示cell的第0组第二行。indexpath的写法。
      • 恋猫月亮:请问下,SDK是在哪里?有官网或者github么?有对应的android项目吗?谢谢~
        zerygao:@恋猫月亮 SDK 中已经实现了对RN页面的点击事件、页面事件等的收集,具体的实现方案会再写一篇文章单独讲述,敬请关注。
        恋猫月亮:@zerygao 你好,本人现在想再React Native上实现数据统上报(点击,页面跳转等),希望有类无埋点的实现,不知道您这边有什么建议没有??
        zerygao:@恋猫月亮 目前还未对外提供 SDK,也未创建专门的网站。另外,android 也已经有对应的SDK。
      • Jason_Hu:想问下关于viewID的采集和匹配的时机,是如何考虑的?
        Jason_Hu:@zerygao 谢谢
        zerygao:@Jason_Hu 我在文章最后更新了Q2的解答,你可以去看看,如果还有疑问欢迎提出
      • carlSQ:kvc 配置是怎么圈选生成的?看的不是很明白
        hard_coder:你的viewpath生成是有问题的吧,如果一个类中有多个相同层次的结构,例如两个button有相同的层次,那么这两个button生成的viewPath就会相同,这就确定不了唯一性
        carlSQ:@zerygao 最近在想怎么能让产品能圈选出业务层数据
        zerygao:@carlSQ 关于这部分,文章中没有详说,这里我简单说一下吧。KVC配置的生成与上传是圈选SDK提供的功能,在项目中集成圈选SDK后,就能够基于点击去自动选取view,并自动获取viewEvent、viewPath等信息,不过keyPath部分需要手动填写,还无法做到自动生成。
      • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/zpzfyd 欢迎点赞支持!
        欢迎订阅《Android&iOS工程师之路》https://toutiao.io/subjects/4829
      • 乐天派大星晴:《网易乐得无埋点数据收集SDK》 这篇文章在哪?内网ks上也没看到。。
        Joy___:@摆一茶几 这里的意思是根据sdk总结的,不是根据文章:relieved:作者首发于此
      • 霖溦:和growingIO比孰优孰略?会不会和JSPatch、AOP冲突?
        霖溦:@zerygao 感谢详尽的回答,目前这类基于Method Swizzling的库一直都比较忌惮,因为之前遇到过一次很严重的冲突问题,也看过一篇解决JSPatch和Aspects之间的冲突的解决方法的文章,不过,总觉得每次都要提防这个问题比较心累。如果库本身解决了,那就太完美了,期待相关的解决方案的介绍文章。
        zerygao: 第一个问题,说实话不太好回答,不过我还是尽量客观的简单说一下吧。如果单从SDK上来说,本 SDK 功能上与GrowingIO相差无几,甚至还稍微比它强大一点,例如:SDK 已经能够支持对 RN 页面的数据收集,包括纯RN开发以及混合开发的项目。如果从平台上来说,当然还是GrowingIO做的更好一些,不过公司内部的大数据平台也在不断的完善中。

        第二个问题,由于 Method Swizzling 与 Aspects、JSPatch 等在实现 AOP 的思路上的不同,确实会存在冲突。不过 SDK 已经解决了冲突的问题,这个就是我在 其他关键技术 章节中提到的 Method Swizzling 与 Aspects 的兼容。关于这个问题的解决方案会在以后的文章中阐述。
        霖溦:写错了,应该是AOP库Aspects

      本文标题:iOS无埋点数据SDK实践之路

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