美文网首页iOS 开发 iOS精品文章移动端数据收集和分析
iOS无埋点数据SDK的整体设计与技术实现

iOS无埋点数据SDK的整体设计与技术实现

作者: zerygao | 来源:发表于2017-05-07 22:03 被阅读16680次

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

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

SDK 的整体设计

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

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

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

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

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

AOP

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

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

实现 AOP 编程

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

hook 的方法

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

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

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

各类点击事件的拦截

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

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

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

  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: 类方法来拦截到其点击事件。
页面事件的拦截

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

滑动事件 & UIWebView加载事件

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

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 的实现逻辑,具体如下图所示:

Event Cache

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

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

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

Event Upload

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

内存数据的实时上报

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

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

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

本地文件数据的上传

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

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

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

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

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

END

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

相关文章

  • iOS 面试 - 其他

    怎么防止反编译? iOS面试题:怎么防止反编译? - 简书 埋点方案的 iOS无埋点数据SDK的整体设计与技术实现...

  • 无痕埋点方案探究

    目前埋点的设计大致有以下几种:参考 网易HubbleData无埋点SDK在iOS端的设计与实现 1、代码埋点由开发...

  • iOS无埋点数据SDK的整体设计与技术实现

    iOS无埋点数据 SDK 实践之路 iOS无埋点SDK 之 RN页面的数据收集 本篇文章是讲述 iOS 无埋点数...

  • 面向过程/对象/切面编程

    面向过程编程,面向对象编程和面向切面编程理解 iOS无埋点数据 SDK 实践之路iOS无埋点SDK 之 RN页面的...

  • iOS无埋点SDK 之 RN页面的数据收集

    本篇文章是讲述 iOS 无埋点数据收集 SDK 系列的第二篇。在第一篇 中主要介绍了 SDK 整体实现思路以及...

  • iOS开发见闻-第21期

    欢迎加QQ群讨论:157672725 文章 1.iOS无埋点数据SDK实践之路 : 本篇文章是基于 网易乐得无埋点...

  • AOP无痕埋点技术

    使用AOP实现iOS应用内的埋点计数 - 简书 iOS用户行为追踪——无侵入埋点 - CSDN博客 iOS 无埋点...

  • 平安好房iOS开发团队技术周报(第三十六期)

    本期导读:本期周报主要包括 iOS 应用崩溃自动修复、数据统计无埋点技术、仿网易云音乐导航栏的技术实现等内容。 资...

  • iOS无埋点SDK实现思路

    18年下半年实现了无埋点SDK的技术调研,构思,实现。当然其中有很多细节上的问题,比如不同系统版本间兼容问题,手势...

  • IOS实现无埋点技术

    技术方案实现的背景: 因为在开发项目的时候,埋点都是手动埋的,每次业务需求的改变都要到处埋点,这就免不了会遗漏埋点...

网友评论

  • 骁驰:我只关心啥时候像yykit一样开源
    代码哥:@zerygao 能学习一下吗?微信18510911512
    zerygao:不用等了,应该不会开源了,我已经从原公司离职了:joy:
  • seongbrave:大牛您好,我们公司有个需要时需要采集用户的点击坐标点,包括touch的point以及滑动的points,刚开始我以为通过hook UIResponder 的touches事件,结果导致很多问题,tableview 的didSelected回调不调用了,可以给下这种需求怎么hook 为好呢,谢谢:smile:
  • 端木易华:看了以后才知道自己的编程水平就是个战五渣:+1::+1:
  • 叶子sir:请教一下,hook了Application的sendAction:to:from:forEvent: 事件, UITabbarButton获取之后无法区分点击了哪一个,你们是怎么解决的? 多谢!
    markss:我试了一下,sendAction:to:from:forEvent 可以拦截到导航栏上系统自动添加的返回按钮的点击。
    _UIButtonBarButton __backButtonAction:
    zerygao:通过其 viewPath 信息进行区分
  • 江上雨寒舟:做过类似这种,出现一个问题,就是如果一个VC里面多处调用同一个方法名的方法,那么一个调用进行了swizzing之后,后面继续调用,又会swizzing,很容易在调用的时候造成死循环,这里是怎么处理的?
    Metros:hook分类中,将method swizzing调用全部放到load()函数中,就不会出现循环交替
  • 上升的羽毛:你好,请问在什么地方hook这些方法?谢谢
    不作不死不舒服斯基:根据作者列的hook的类,写对应类的分类,然后用runtime就可以了
  • 梦想驻唱:作者的思路很棒,细节考虑也很出色,不知道作者的SDK有没有考虑到多端统一的问题,比如说,这个下发配置安卓是不是能直接拿过去使用,因为如果不统一的话,为了安卓还要再配置一份类似的文件,如果path计算再不一样的话,那么使用成本就很大了
  • c75359c57b8c:您的上篇文章中:业务层数据的收集是指对与业务功能相关的一些数据,例如:在用户点击提交订单按钮时,收集用户购买的物品以及订单总金额的数据。这种业务层数据的收集以往大多通过 代码埋点 的方式去做,本SDK则真正的实现了 无埋点 的去获取这些想要的业务数据。这部分的实现会在本文的第二节详细介绍。
    请问这是第二节么?
    如果是的话,怎么实现业务数据收集的呢?并未发现在此文中提到。
    zerygao:不是这篇,还是在那篇文章中,SDK无埋点业务数据收集的实现-这个章节中有详细介绍
  • Hunter琼:赞!为埋点而来 感叹楼主强大 我看过阿里大数据 上面写的客户端数据采集,好像也是封装的SDK ,Native和H5处理方案,写的太过简单
    我们公司做的客户端数据采集,太过原始!我想请教下,采集数据上传服务器的时机除了考虑文件大小,时间点,还需要考虑什么呢?
    zerygao:谢谢支持!在数据上传时机上,一方面需要保证采集到的数据能够及时上传,另一方面需要保证数据完整性,防止数据丢失。所以具体上传时机可以根据项目实际需要自己制定一套。
  • a3fa355c82e1:有一个专门的圈选工具,让用研或产品去圈选配置关心的控件,并指定一个中文名
    请问下你们的圈选工具圈选会产生什么样的数据呢,统计时怎么根据圈选的内容上报,每个页面圈选的都不同,很多可能是页面特殊状态才出现的,还有怎么兼容安卓
    a3fa355c82e1:@zerygao 还有请问这种方法支不支持h5页面的无码埋点?
    a3fa355c82e1:@zerygao 圈选控件时是在一般的视觉效图上勾选,还是有专门的工具?如果是列表页,效果图只有五个,真实是更多个,圈选的时候能统计到?还有控件所在的页面,iOS和安卓的页面不一样,是需要写两个?圈选完的这些信息是在app启动的时候app下载的?
    zerygao:通过圈选工具圈选控件时,会将控件所在的页面、viewPath 信息上报、配置的中文名一起上报;在后台统计时,主要是根据 viewPath 信息进行匹配。
  • NSBug:👍
  • 没事蹦蹦:使用touchesEnded:withEvent:这种形式实现点击,你们是怎么实现自动打点呢,看文章好像没提到
    zerygao:@没事蹦蹦 嗯,这种方式实现的点击没有捕捉,目前主要针对的手势和UIControl的控件的点击事件
  • daixunry:针对与手势相关的事件,SDK 首先通过 hook 系统类 UIGestureRecognizer 的 initWithTarget:action: 与 addTarget:action: 这 2 个方法拿到 target 对象与 action 方法,然后再去 hook target 的 action 方法,从而能够拦截到手势相关的事件。

    想问下,hook了target和action,但是如何跟viewPath关联起来?这个时候已经失去了view的信息了吧?
    5ee14485e803:@zerygao 你这里说的系统类,指的是系统的私有类?如何判断的呢,谢谢
    zerygao:@小雨的名字 针对这个问题,可以在 hook 之前判断添加手势的这个类是否为系统的类,如果是系统类就不执行接下来的 hook
    5ee14485e803:有些控件系统会主动添加一些手势事件 比如 UISwitch,系统会为UISwitchModernVisualElement添加 _handlePan:,如何把这些过滤掉呢?毕竟有太多未知的类似UISwitchModernVisualElement这样的类
  • b3c6db784269:您好,请教一个问题,自定义类方法的hook怎么去做呢。
  • 26be7632f73e:您好,请问圈选工具实现思路大概是怎么实现的,基于webdriveragent了么
  • ad99c93f2499:UIGestureRecognizer的Hook,如果多个Gesture添加到一个UIView上并且Selector不一样,由于多次Hook最后的方法调用会乱掉吧?
    5ee14485e803:确实有这个问题,你后面如何解决的?
  • xx_Coding:有一个问题,请教你,看你做了一层cache内存到磁盘,当app突然之间退出,你是怎么处理,缓存的。这样不就造成了,数据的丢失嘛?
    zerygao:@shaoqiu 用户直接杀死进程是能够保存内存数据的,对于发生Crash或者OOM这种可能会丢数据,解决方案可以采用mmap
    xx_Coding:@zerygao 有对应的处理方案嘛?这个不应该说crash,还有一种就是用户直接杀掉进程
    zerygao:@shaoqiu 嗯,如果此时发生了crash,可能会来不及保存就丢失了
  • louisly:hi,楼主。想请教一下,关于hook类方法的一些实现细节。是在resolveClassMethod等这些消息转发的地方统一处理吗?还是动态添加方法、添加IMP实现,然后方法替换?
    zerygao:@louisly 不是的,没有采用这种机制
    louisly:@zerygao 是和aspect一样 通过元类替换forwardInvocation这个方法吗
    zerygao:@louisly 使用的动态方法替换
  • 当红辣椒炒肉:我想问下在app被杀死和异常退出的时候,能确保数据一定被保存吗?还有就是所有的异常都能被截获吗?
  • ceabed53d3f0:楼主按照你的思路,我这边实现hook手势的时候出现一点问题你们那边是怎么解决的呢,我在hook手势事件时如果这个手势的action方法在手势所在的target类中如果有自动调用并不是通过手势触发的话会崩溃,求教?
  • Liberalism:您好,我想问一下,你hook手势的方法addtarget : action 这个方法.如果一个手势添加了多个addtarget:action ,请问一下怎么避免多次上报埋点的问题?
  • 乐视薯片:你好,想问个问题,拦截手势事件initWithTarget:action:,在自定义实现方法里一直获取不到对象和action,怎么回事?
    zerygao:@初心_媛 应该是你的 hook 实现有问题吧?
  • zhang789:文章思路清晰,特别好,但是有点欠缺,上传时机,和上传环境考虑的不是太好。
  • ampire_dan:请问一下 protobuf 在项目中有什么优缺点吗?
    zerygao:@ampire_dan 对于数据量较大的项目来说,其优点还是很明显的,能减少数据体积,大大提升传输效率,减少用户流量消耗。不过它也有一个明显的缺点就是可读性差以及.proto的维护成本。
  • 写自己的代码:大神,RN的页面埋点是怎么处理的?
    zerygao:@grace1470 我另一篇文章有介绍,你可以去看一下
  • 4f1bd451738b:谢谢分享,会持续关注,再次谢谢
  • jdong:swizzle系统类的 Delegate 的方法,能介绍下吗?
  • s_在路上:对于Event Collector,没有看太明白,能加下好友了解一下么?
  • f94cd0d60740:原理都说清楚了, 方法基本都写了, 需要hook的事件也都列了, 还是有人只会要demo, demo的真心烦.
    我们曾经也做了类似的SDK, 但在实用中发现如下问题:
    1. 产品看不懂: 产品关注的就那几个关键事件, 报上来的东西他们根本看不懂, 还是要开发帮忙一个个标出哪些是他们需要的事件.
    2. 接入方有抵触: SDK最终是要给业务侧去用的, 接入的同事比较抵触hook系统方法的行为, 以前确实发生过第三方库hook系统方法导致crash.
    3. 上报数据量太大: 我们日活有百万, 上线后发现上报的数据量远超预期, 后台准备不足出现了性能问题.
    综上, 我们最后还是放弃了这种自动上报的方式, 用回了传统的埋点.
    不知道楼主的SDK有没有上线, 遇到这些问题是怎么处理的?
    plantAtree_dAp:@zerygao ,现在知道圈选是 什么意思了
    f94cd0d60740:@zerygao 谢谢回复, 做这种SDK还是挺不容易的, 有时候还吃力不讨好~各方都要协调好才能起到应有的作用.
    zerygao:@iimgal 对于问题1,我们这边有一个专门的圈选工具,让用研或产品去圈选配置关心的控件,并指定一个中文名。问题2,在业务方推动确实比较困难,当然也可能会由于hook引起一些问题,前期我们是先以放量的方式去在线上测试,后期没大问题了就全量放开了,这是需要一个过程的。问题3,如果量很大,可以对上报数据精简一下,只上报更有价值的数据
  • wsj2012:说了半天 没个demo
    zerygao:@wsj2012 不好意思,由于公司保密规定,不便提供demo,望理解
  • 6a948902fef0:再求问一下,是否有hook处理网络请求呢,比如NSURLSession的请求错误。因为网络请求有可能使用delegate的方式处理请求结果,也可能是直接用block处理请求结果,这个怎么处理比较好呢。谢谢
    Joy___:这个你可以使用 NSURLProtocol 来做网络拦截,或者可以使用hook NSURLsession,有的是delegte、有的是 block 你可以都 hook 掉。如果要用delege方法的话,可以自己造一个delegate
  • 37a000f66394:大神,收徒吗 ?:joy:
    最近公司也提了,大数据的无埋点这个SDK需求,那个愁啊。
    看了您的文章确实受益匪浅,如果能给份demo,感激不尽啊。
    如果可能的话跪求发一份到 1056916315@qq.com 邮箱:joy:
    zerygao:@追求小小的梦 抱歉,由于公司保密协议,不便提供Demo,可以借鉴文章的实现思路,去编写代码吧:stuck_out_tongue_winking_eye:
  • 6a948902fef0:求问一下,如何从UIEvent中识别出对应的UI操作呢,比如 如何和按钮的TouchUpInside对应上,或者如何与其他的滑动等对应上
    6a948902fef0:@zerygao 恩,滑动是另外处理。那么在sendAction中的事件都当做是点击事件对么
    zerygao:@wangKy 按钮点击和滑动应该是分别拦截的吧,不需要去区分吧
  • 开发者头条_程序员必装的App:感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/j4639h 欢迎点赞支持!
    欢迎订阅《移动前沿》https://toutiao.io/subject/199192
  • 李大戮::+1:
    zerygao:@tridonlee hook这块的实现应该不难,可以通过 method swizzling 或者参考Aspects库的实现
    tridonlee:能给一个demo,看看怎么hook的吗?
  • 940ce980cb08:SDK哪里下载?
    zerygao:@RunningEagle 目前仅提供给公司内部产品使用,尚未开放出去
  • TEASON:牛比
    zerygao:@TEASON 多谢支持:blush:
  • 另一个理瑜:那张获取viewpath的实现逻辑的流程图,不是点击上报数据时获取viewpath的流程吧?而是匹配服务端下发的配置的流程图?
    zerygao:@2yPeace 不是通过响应者链获取的VC,是通过尝试获取 viewDelegate 得到的VC
    另一个理瑜:@zerygao 不太懂,为什么点击的view的vc存在的时候,就可以直接输出,vc和index,这边的view所在的vc是通过next responder循环得到的吗?index只是当前view在父视图的索引吧?
    zerygao:@2yPeace 是在点击view时获取被点击view的viewPath的流程,不是匹配服务端配置的流程
  • 另一个理瑜:请问,获取viewpath的过程是在主线程吧,你们平均这个过程多久,我这边是0.5~0.03ms
    zerygao:@2yPeace 嗯,是在主线程,具体耗时还没测过
  • RubyAhooo:学习
  • 624e69be3ebb:👍🏻

本文标题:iOS无埋点数据SDK的整体设计与技术实现

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