有货iOS数据非侵入式自动采集探索实践

随着有货APP的不断迭代开发,数据和业务部门对于客户端用户行为数据的需求越来越多;为了更好的监控APP使用的状况,客户端团队对于APP自身的运行的数据需求也愈发迫切。迫切地需要一套客户端数据采集的工具,自动、全量采集用户行为数据,满足各个部门对于数据的需求。

\\

有货APP团队为此开发一套数据采集的SDK,主要的功能如下:

\\
  1. 页面访问流。用户在使用APP期间浏览了哪些页面。\\t
  2. 浏览数据曝光。用户在某个页面上浏览了哪些商品。\\t
  3. 业务数据自动采集。用户在使用APP期间点击了哪些位置,触发了哪些操作。\\t
  4. 性能数据自动采集。用户使用APP期间,页面加载时长是多少,图片加载时长多少,网络请求时长多少等。\

此外,所有的数据采集要自动化,无侵入,即不需要人工埋点,集成SDK即可使用,不改动或尽量少改动原有代码。

\\

基于以上需求,AOP是技术方案的最佳选择,而iOS上实现AOP则需要依靠Objective-C中runtime的黑魔法--Method Swizzle实现。漫漫的踩坑填坑的旅程由此开端,接下来我们一一品尝实现思路和方法吧。

\\

页面访问流

\\

用户访问页面统计需要解决的问题有两个:

\\
  1. 统计事件切入点,即何时统计。\\t
  2. 统计数据字段,即统计哪些数据。\

整体流程如下图:

\\

dca9ca4414db152785931e6f7d2d850b.png

\\

统计事件切入点

\\

用户访问页面统计的一般思路是在View Controller生命周期方法:

\\
  • viewDidAppear上报页面进入事件。\\t
  • viewDidDisappear上报页面退出事件,\

即可得出用户访问页面路径,两个事件时间戳之差即为用户在页面停留的时间。

\\

通常我们APP中的View Controller都会继承自某个基类,我们在基类的对应方法中进行统计即可,然而对于没有从基类继承的View Controller就无能为力了。

\\

借助于AOP,我们可以更优雅的完成这项工作:在UIViewController的load方法里swizzle viewDidAppear和viewDidDisappear方法,原有代码无需改动。

\\

统计数据字段

\\

根据数据需求,设置了如下的统计字段:

\\
  • PAGE_ID,当前页面的标识。\\t
  • SOURCE_ID,当前页面的前一个页面的标识。\\t
  • TYPE_ID,当前页面一些关键信息,如商品id,品牌id等。\\t
  • TIMESTAMP,当前事件生成的时间戳。\

页面进入和退出的事件,均上报上述的数据结构。

\\

其中还有几个问题是需要考虑的:

\\

1.PAGE_ID和SOURCE_ID如何定义

\\

因为需要统一iOS和Android的PAGE_ID,所以需要做配置下发。iOS端拿到的是一份plist的文件,文件的key的View Controller的类名的字符串表示,value则是PAGE_ID。

\\

2.PAGE_ID和SOURCE_ID如何获取

\\

PAGE_ID直接根据当前View Controller的class即可取到,SOURCE_ID稍显复杂,需要根据APP页面嵌套堆栈结构来确认具体的获取方法,通常是从UINavigationController的导航栈中取前一个View Controller的page id即可。

\\

至此,页面访问流统计已基本完成,根据页面进入退出的PAGE_IDSOURCE_ID串出一条完整的用户浏览路径,并得出用户在每个页面的停留时间。

\\

浏览数据曝光

\\

采集到用户的浏览路径,以及在每个页面的停留时间后,在某些特定的页面,如首页、商品列表页面,我们还想知道用户在页面上滑动了几屏,看了哪些活动、商品,以便于更好的为用户推荐喜欢的商品。

\\

用户看到的屏幕上的一块区域,认为是资源位,那么用户看到的内容是由一个个资源位组成。那么曝光的含义如下:

\\
  • 资源位从屏幕可视区域外,进入到可视区域内(任意部分可见即可),即是一次曝光。\\t
  • 资源位从可视区域移出后,再次进入可视区域,算做新的一次曝光。\\t
  • 页面切换和下拉刷新时,算作新的一次曝光。\

我们知道iOS中页面元素的基本组成单位是view,因此我们只需要判断view是否在可视区域,即可知悉当前view上的资源位是否需要曝光,从而做出相应的曝光操作,采集数据,上报接口等。

\\

由以上的分析可知,待解决的问题主要有两个:

\\
  1. view的可见性判断\\t
  2. view曝光数据采集\

view的可见性判断

\\

查询UIView Class Reference可以看到setFrame:layoutSubivews方法,可用于设置subview的frame。每次view fame更新均会调用此方法。因此,我们可以通过runtime swizzle此方法实现,添加一些数据采集相关的操作。

\\

我们为UIView添加了以下属性:

\\
  • yh_viewVisible:view是否可见,默认否。可见性由否-\u0026gt;是的时候,触发一次曝光数据采集的操作。\\t
  • yh_viewVisibleRect:view可见区域,默认CGRectZero。\\t
  • yh_visibleSubviews:view所有可见的subview。\

首先明确下几个术语的定义和规则:

\\

1.view的subview可见需要同时满足的3个条件:

\\
  • subview.hidden为false,即view没有被隐藏。\\t
  • subview.alpha大于等于0.01,即view是可见的。\\t
  • subview的frame是否在view的可见区域内。\

反之,只要以上任何一个条件不满足,我们就认为此subview当前是不可见的。

\\

2.设置view为可见

\\
  • 设置yh_viewVisibleRect为可见区域frame。\\t
  • 设置yh_viewVisible为true。\\t
  • 将view加入superview的yh_visibleSubviews数组。\

3.设置view为不可见

\\
  • 设置yh_viewVisibleRectCGRectZero。\\t
  • 设置yh_viewVisiblefalse。\\t
  • 将view自superviewyh_visibleSubviews数组移除。\

Swzzile setFrame:,执行以下操作:

\\

5b800619fe64353375ec511006c683a0.png

\\
  • 若view是UIScrollView,则根据当前frame和contentInset计算当前yh_viewVisibleRect。不是则将当前frame设置为yh_viewVisibleRect。\

Swzzile layoutSubivews,调用yh_updateVisibleSubViews方法,其中执行以下操作:

\\

3bd73af9fb5dd3d5197a02008b6ae769.png

\\
  • 判断view.yh_viewVisible与view自身的可见性,若view不可见,则迭代其subview的为不可见,并终止后续操作。\\t
  • 判断view. yh_visibleSubviews中view是否还是view的subview,不是则设置subview为不可见。\\t
  • 判断是否是UITableViewWrapperView,是则view的yh_viewVisibleRect的originy取其superview的bounds的origin y。这么做是因为实践中发现UITableView设置bounds的会使view的可见区域产生变化,需要重新设置。\\t
  • 遍历view的subview,若subview可见则设置其为可见,否则设置为不可见。\

经过以上的这些操作,我们就能知道某个view及其subview的是否可见。

\\

view曝光数据采集

\\

为了取到view对应的数据,同样为UIView添加了以下属性:

\\
  • yh_exposureData:字典类型,用来存储此view节点需要曝光的数据。\

那么还有两个问题存在:

\\
  1. view曝光数据的粒度\\t
  2. view及其subview的节点的曝光数据组装时机\

view曝光数据的粒度

\\

根据项目中的实践经验,一般以UITableViewCell或者UICollectionViewCell为最小粒度。同时,在最末节点的yh_exposureData字典中,增加一个key:isEnd,用来标识是否已经是最末的节点。

\\

view及其subview的曝光数据组装时机

\\

一般是在最末节点的可见性变化时,由下向上的遍历最末节点的superview,组装所有数据。

\\

因此我们覆写了setYh_viewVisible:的方法,即yh_viewVisible的set方法。执行以下操作:

\\
  • 若当前self.yh_viewVisible变化为false-\u0026gt;true,且self.yh_exposureData包含最末节点的标记,则由下向上的遍历最末节点的superview,组装所有数据。\\t
  • 设置self.yh_viewVisible的值。\

至此,我们已经解决了view的可见性判断和曝光数据采集的问题。数据上报及策略不在赘述。

\\

此方案有几个缺点

\\
  1. 需要手动设置曝光数据。\\t
  2. 需要在合适时机手工调用view.yh_viewVisible触发数据采集,如viewdidappear等。\\t
  3. 需要消耗一定的资源进行可视区域计算和曝光数据采集。\

还有两个问题是值得注意的:

\\
  1. UITableViewsetBounds:时会对view的frame造成改变,因此需要swizzle setBounds:方法,需要在设置bounds后,调用[self yh_updateVisibleSubViews];\\t
  2. UIScrollView在setContentInset:时会影响view的可见区域,因此需要swizzle setContentInset:方法,需要在设置contentInset后,调用self.yh_viewVisibleRect = UIEdgeInsetsInsetRect(self.frame, contentInset);\

业务数据自动采集

\\

业务数据自动采集即业界流行的无埋点数据采集。

\\

传统的客户端用户点击数据采集是基于手工埋点的,对哪个位置的数据感兴趣,就在这打个点,用户操作之后,随即触发数据上报。手工埋点的缺点很明显:错埋、漏埋。新版本发布后,经常有数据部门的小伙伴来反馈说,某某点位没有上报,某某点位上报错误的问题,开发的同事也苦不堪言。

\\

无埋点数据采集带来了新的改变。首先基本上避免了手工埋点,个别情况需要特殊处理。其次由选择性的采集数据,变成了全量采集用户的所有点击触摸数据。

\\

新的改变也会带来新的挑战,无埋点数据采集的成为现实的可能性仍然是基于Objective-C的runtime特性。实践过程中,思路上我们借鉴了iOS无埋点数据SDK的整体设计与技术实现,实现上借鉴了Sensors Analytics iOS SDK和Mixpanel iPhone。接下来,结合具体实践,介绍下我们的实现思路和遇到的一些问题。主要分以下三方面:

\\
  1. 自动采集的点位如何确保唯一性。\\t
  2. 不同的点位类型,需要swizzle哪些方法。\\t
  3. swizzle过程中踩到的坑。\

自动采集的点位如何确保唯一性

\\

自动采集脱离了手工埋点,因此也没了点位的唯一标识。那我们要怎么唯一定位到自动采集的点位呢?很容易想到的一个方案是:基于页面view的树形结构。此方案可以分解为两个问题:

\\
  1. view唯一标识如何定义。\\t
  2. view唯一标识如何生成。\

view唯一标识(view path)的定义

\\

我们规定,一个典型的view path如下:

\\
\ViewController[0]/UIView[0]/UITableView[0]/UITableViewCell[0:2]/UIButton[0]
\\

其中:

\\
  1. 通过此标识可以在当前页面view树形结构中唯一的确定此元素。\\t
  2. 标识的每一项由两部分组成:一是当前元素的class的字符串表示,二是当前元素在同级元素中的序号,自0开始计算。如当前第二个UIImageView,则是UIImageView1。\\t
  3. 标识不同项之间以/拼接。\\t
  4. 标识的最顶层是当前view所在的ViewController。\\t
  5. 对于UITableViewCell和UICollectionViewCell及类似的自定义组件,序号部分由两部分组成:section和row,并以:拼接。\\t
  6. 标识的最末端是当前被点击或触摸的元素。\

view唯一标识如何生成

\\

view path生成过程:由触发操作的最末端元素向上查询,一直查到ViewController为止。假设当前点击view为A_View,从当前的A_View入手遍历view树,每一级的数据存入P_Array中,过程如下:

\\

b957410bf16c5195e0bc18a99162088e.png

\\
  1. 如果A_ViewUICollectionViewCell类型,获取A_View所处UICollectionView的indexPath,P_Array push路径信息[NSString stringWithFormat:@\"%@[%ld:%ld]\
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值