美文网首页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 的具体方法,和事件数据的存取与上报功能的实现流程。如果对本文有问题,请留言评论。

    相关文章

      网友评论

      • 骁驰:我只关心啥时候像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计算再不一样的话,那么使用成本就很大了
      • Ukenn:您的上篇文章中:业务层数据的收集是指对与业务功能相关的一些数据,例如:在用户点击提交订单按钮时,收集用户购买的物品以及订单总金额的数据。这种业务层数据的收集以往大多通过 代码埋点 的方式去做,本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怎么去做呢。
      • ppwwyy:您好,请问圈选工具实现思路大概是怎么实现的,基于webdriveragent了么
      • Dokay: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
      • 追求小小的梦:大神,收徒吗 ?: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