自动采集数据(有货APP开发一套数据采集的SDK(一)_)
优采云 发布时间: 2021-09-03 16:02自动采集数据(有货APP开发一套数据采集的SDK(一)_)
随着有货APP的不断迭*敏*感*词*发,数据和业务部门对客户用户行为数据的要求越来越高;为了更好的监控APP的使用状态,客户端团队有APP本身的运行数据。需求越来越迫切。急需一套针对*敏*感*词*采集、自动完整采集用户行为数据的工具,以满足各部门的数据需求。
有火APP团队为此开发了一套数据采集SDK。主要功能如下:
页面访问流程。用户在使用APP时浏览了哪些页面。
浏览数据公开。用户在某个页面上查看了哪些产品。
业务数据自动为采集。用户在应用期间点击了哪些位置以及触发了哪些操作。
性能数据自动为采集。用户在使用APP的过程中,页面加载时间多长,图片加载时间多长,网络请求时间多长等
另外采集的所有数据都应该是自动化的、非侵入性的,即不需要人工埋葬,可以集成SDK,尽量不要改动或改动原代码。
基于以上需求,AOP是技术方案的最佳选择,而在iOS上实现AOP需要实现Objective-C中运行时的黑魔法Method Swizzle。踏坑填坑的漫漫征程从这里开始,让我们一一领略实现的思路和方法。
页面访问流程
用户访问页面统计需要解决两个问题:
统计事件的入口点,即何时统计。
统计数据字段,即统计哪些数据。
整体流程如下:
统计事件的入口点
用户访问页面统计的大致思路是在View Controller生命周期方法中:
可以获得用户访问页面的路径,两个事件时间戳的差值就是用户在页面停留的时间。
通常,我们APP中的View Controller继承自某个基类。我们可以在基类的相应方法中进行统计。但是,对于不继承自基类的视图控制器,我们无能为力。
借助AOP,我们可以更优雅地做到这一点:在UIViewController的load方法中混用viewDidAppear和viewDidDisappear方法,原代码无需改动。
统计字段
根据数据要求,设置如下统计字段:
页面进入和退出的事件报告在上述数据结构中。
还有几个问题需要考虑:
如何定义1.PAGE_ID 和 SOURCE_ID
因为要统一iOS和Android的PAGE_ID,所以需要配置下发。 iOS端得到的是一个plist文件,文件的key是View Controller类名的字符串表示,值为PAGE_ID。
如何获取2.PAGE_ID和SOURCE_ID
PAGE_ID 可以根据当前View Controller的class直接获取。 SOURCE_ID 稍微复杂一些。具体的获取方式需要根据APP页面的嵌套栈结构来确定。通常,前一个View Controller的页面是取自UINavigationController的导航栈。 id 没问题。
至此,页面访问流量统计基本完成。根据页面进入和退出的PAGE_ID和SOURCE_ID,串出一个完整的用户浏览路径,获取用户在每个页面的停留时间。
浏览数据暴露
采集到用户的浏览路径,在每个页面停留的时间之后,在某些页面,比如首页和产品列表页面,我们也想知道用户在页面上刷了多少屏查看选择了哪些活动和产品,以便更好地为用户推荐喜欢的产品。
用户看到的屏幕区域被认为是一个资源位,所以用户看到的内容是由资源位组成的。那么曝光的含义如下:
我们知道iOS中页面元素的基本单位是视图,所以我们只需要判断视图是否在可见区域,就可以知道当前视图上的资源位置是否需要暴露,然后进行相应的曝光操作,采集data,上报接口等
从上面的分析可以看出,主要有两个问题需要解决:
查看可见性判断
查看曝光数据采集
查看可见性判断
查询UIView Class Reference可以看到setFrame:和layoutSubivews方法,可以用来设置subview的frame。每次更新视图名望时都会调用此方法。因此,我们可以通过runtime swizzle来实现这个方法,并添加一些数据采集相关的操作。
我们为 UIView 添加了以下属性:
首先明确以下术语的定义和规则:
1.view的子视图可以同时看到3个需要满足的条件:
相反,只要不满足上述任一条件,我们认为该子视图当前不可见。
2.设置视图可见
3.设置视图不可见
Swzzile setFrame:,执行以下操作:
Swzzile layoutSubivews,调用yh_updateVisibleSubViews方法,执行如下操作:
经过以上操作,我们可以知道某个视图及其子视图是否可见。
查看曝光数据采集
为了获取view对应的数据,UIView还添加了如下属性:
那么还有两个问题:
查看曝光数据的粒度
视图及其子视图节点曝光数据的组装时序
查看曝光数据的粒度
根据项目中的实践经验,一般使用UITableViewCell或者UI采集ViewCell作为最小粒度。同时,在最后一个节点的yh_exposureData字典中,添加一个key:isEnd来标识是否是最后一个节点。
视图及其子视图的曝光数据的组装时序
一般来说,当最后一个节点的可见性发生变化时,从下到上遍历最后一个节点的superview,将所有数据组装起来。
所以我们覆盖了setYh_viewVisible:方法,也就是yh_viewVisible的set方法。执行以下操作:
到此为止,我们已经解决了视图可见性判断和曝光数据采集的问题。此处不再重复数据报告和策略。
这个方案有几个缺点
您需要手动设置曝光数据。
需要在合适的时候手动调用view.yh_viewVisible来触发数据采集,比如viewdidappear。
视觉区域计算和曝光数据采集需要消耗一定的资源。
还有两个问题值得注意:
UITableView在setBounds:时会改变视图框架,所以需要调配setBounds:方法,需要调用[self yh_updateVisibleSubViews];
UIScrollView在setContentInset:时会影响view的可见区域,所以需要调配setContentInset:方法,需要调用self.yh_viewVisibleRect = UIEdgeInsetsInsetRect(self.frame, contentInset);
业务数据自动采集
业务数据自动采集是业界热门数据,无埋点采集。
传统客户端用户点击数据采集是基于人工埋点。如果您对任何位置的数据感兴趣,只需单击此处。用户操作后,立即触发数据报表。人工掩埋的缺点很明显:错掩埋、漏掩。新版本发布后,经常有数据部的小伙伴反映某个点的问题没有报,某个点的问题报错了,开发同事也是惨了。
非埋点数据采集带来了新的变化。首先,基本避免人工埋葬,个别情况需要特殊处理。其次,选择性的采集数据变成了采集用户所有点击和触摸数据的全量。
新的变化也会带来新的挑战。没有隐藏数据采集成为现实的可能性仍然基于Objective-C的运行时特性。在实践过程中借鉴了iOS非埋点数据SDK的整体设计和技术实现,在实现中借鉴了Sensors Analytics iOS SDK和Mixpanel iPhone。接下来结合具体实践,介绍一下我们的实现思路和遇到的一些问题。主要分为以下三个方面:
如何保证自动采集的点的唯一性。
不同的点类型,swizzle需要哪些方法。
在混水过程中踩到了坑。
如何保证自动采集点的唯一性
自动采集与手动埋点分离开,所以点没有唯一标识。那么我们如何唯一定位自动采集的点呢?一种容易想到的解决方案是:基于页面视图的树状结构。这个解决方案可以分解为两个问题:
如何定义视图的唯一标识符。
视图如何唯一标识如何生成它。
视图唯一标识符(视图路径)的定义
我们规定一个典型的视图路径如下:
ViewController[0]/UIView[0]/UITableView[0]/UITableViewCell[0:2]/UIButton[0]
其中:
这个元素可以通过这个标识符在当前页面的视图树结构中唯一标识。
每个项目由
标识
由两部分组成:一是当前元素的class的字符串表示,二是当前元素在同级元素中的序号,从0开始计数。比如当前第二个UIImageView是UIImageView1.
标识不同项目之间的联系。
标识的顶层是当前视图所在的 ViewController。
对于UITableViewCell、UI采集ViewCell等类似的自定义组件,序号部分由section和row两部分组成,拼接在一起。
标签的最后是当前被点击或触摸的元素。
如何生成视图唯一标识符
视图路径生成过程:从触发操作的最末端元素向上查询,直到找到ViewController。假设当前点击的视图是A_View,从当前A_View开始,遍历视图树。每一层的数据存储在P_Array中。流程如下:
如果A_View是UI采集ViewCell类型,获取A_View所在UI采集View的indexPath,P_Array推送路径信息[NSString stringWithFormat:@"%@[%ld:%ld]",[NSString stringWithFormat:@"%@", NSStringFromClass( [A_View class])],(long)indexPath.section, (long)indexPath.row];
如果A_View是UITableViewCell类型,获取A_View所在UITableView的indexPath,P_Array推送路径信息[NSString stringWithFormat:@"%@[%ld:%ld]",[NSString stringWithFormat:@"%@", NSStringFromClass( [A_View class])],(long)indexPath.section, (long)indexPath.row];
遍历A_View.superview的所有子视图,得到A_View同层,同类型([A_View类])的编号(索引),P_Array推送路径信息[NSString stringWithFormat:@"%@[%d ] ",NSStringFromClass([A_View class]),index];
获取A_View所在的控制器A_VC。如果 A_View 是 A_VC.view,则遍历结束。如果 A_View 不等于 A_VC.view,A_View = A_View.superview,重复步骤 1-4,直到 A_View 等于 A_VC.view。
遍历P_Array拼接A_View的完整路径。
各种类型的点需要swizzle方法
我们将APP中的用户操作分为四类:
UI采集View 和 UITableView 的单元格点击事件。
UIControl(UISwitch、UIStepper、UISegmentedControl、UINavigationButton、UISlider、UIButton)控件的点击事件。
UIImageView 和 UILabel 上的 UITapGestureRecognizer 触摸事件。
UITabBar、UIAlertView、UIActionSheet等点击事件
这四类操作需要用到的swizzle方法如下表所示:
UI采集View、UITableView、UITabBar、UIAlertView、UIActionSheet的实现类似。它们都是 load 方法中的 swizzle setDelegate 方法。 setDelegate 后,执行代理回调方法的 swizzle 操作。在回调方法中,先执行原来的逻辑。 ,然后得到对应的viewPath。
UIControl组件回调目标时,会被UIApplication的sendAction:to:from:forEvent:调用,所以我们选择swizzle方法。实际中,先获取对应的视图路径,再执行原逻辑。原因是如果先执行原来的逻辑,页面可能会发生变化,得到的View Controller就会出错。
<p>UITapGestureRecognizer 事件仅在 UIImageView 和 UILabel 上处理。 swizzle addGestureRecognizer: 方法,先执行原来的逻辑,然后给view添加一个自定义的回调方法,这样在触发手势的时候也会调用自定义的回调,这个时候我们就得到了view的路径。