Android无埋点数据收集SDK关键技术

前言

  鉴于日益强烈的精细化运营需求,网易乐得从去年开始构建大数据平台,<<无埋点数据收集SDK>>所以立项,用于向大数据平台提供全量,完整,准确的客户端数据.
  <<无埋点数据收集SDK>>Android端从着手,到经历重构,逐步完善到如今已经有快一年的时间了.期间从开源社区以及同行中获得了一些颇有意义的技术参考,所以在这个SDK趋于完善的今天,咱们也考虑将这一路在技术上的探索经历和收获分享出来.javascript

  1. 4月16-18日,QCon北京2017全球软件开发大会上有同事表明Android/IOS两端进行统一的技术分享,欢迎你们前去交流
  2. 咱们会逐渐整理一些技术文章出来

  以前关于Android端的<<无埋点数据收集SDK>>使用的技术,写了一篇文章Android AOP之字节码插桩,这个是Android端进行一切收集的起点,咱们就是用这个方法轻松拿到各类"Hook"点的.
  本篇文章则接着讲一下关于收集SDK内部收集逻辑的一些关键技术.java


目录

1、概述
1.1 SDK数据收集能力现状
1.2 关键技术点概述
2、View的惟一标识(ID)
2.1 调研
2.2 利用ViewTree构建ViewID
2.3 ViewPath的生成
2.4 ViewPath的优化
3、页面的划分
3.1 合理划分页面的重要性
3.2 Android中的页面
3.3 页面名组成
4、无需埋点轻松收集定制的业务数据
4.1 配置示例
4.2 无埋点收集流程
4.3 数据路径(DataPath)
5、结语android


1、概述

  本部分首先简要介绍一下咱们的收集方案目前能够收集到哪些数据,而后对于本文重点介绍的三个技术点进行概述.程序员

1.1 SDK数据收集能力现状

  目前咱们的SDK进行数据收集时基本有两个能力:数组

a. 通用数据全量收集
  通用数据指的是与业务无关的用户行为数据,不管是电商应用仍是社区应用,接入SDK后通用数据的收集上都是无差的,这些通用数据大体有:缓存

事件 描述
冷启动事件 App第一次启动时的,版本号、设备ID、渠道、内存使用状况,磁盘使用状况等信息
先后台事件 App进入前台或者后台
页面事件 页面(Activity或Fragment)显示(Show)/隐藏(Hide)
控件点击事件 某个控件(包括页面上控件和弹窗中控件)被用户点击
列表浏览事件[可选] 某个列表的哪些条目被用户浏览了
位置事件[可选] 上报用户地理位置信息
其它事件 省略描述

b. 业务相关数据需求经过下发配置进行无埋点定制收集
  除了上述通用数据,与具体业务相关的数据收集。拿网易贵金属的首页举个例子:架构

图1-1 无埋点收集业务数据示例

  假使须要在用户点击上图红框区域时,把“粤贵银”这个交易品的ID(或者下方显示的指数等,只要在内存中存在的数据均可以)一块儿报上来。
  对于此种需求,数据收集SDK作到了无需埋点不依赖开发周期,经过线上下发一些配置信息,便可即时进行数据收集。具体原理第四节叙述。app

1.2关键技术点概述

a. View的惟一标识(ID),(详见本文第二节)
  当咱们收集控件数据时碰到的第一个问题就是:如何把界面上的任何一个View与其余View区分开来.iphone

好比:某个Button被点击了
咱们在上报数据的时候须要把这个Button和其余全部控件(好比另外一个Button,另外一个ImageView等)区分开来,这样这条上报的数据才能表示"就是那个Button被点击了一下".ide

  这就须要为界面上的每个控件生成一个惟一的ID. 此ID除了具备区分性,还须要用于一致性一致性是同一个View不管界面布局如何动态变化,或者说屡次进入同一页面,此ID须要保持不变.

b. 页面的划分,(详见本文第三节)
  除了Activity有些Fragment也须要看做页面,这就要求:

  • 在Fragment show/hide时上报相关页面事件.
  • 页面Fragment中发生的用户交互事件也须要归于此Fragment页面,即点击某个View须要上报页面Fragment的信息(从View中怎么获取Fragment信息?)

c. 无需埋点轻松收集定制的业务数据,(详见本文第四节)
  如前面所述,默认状况下数据收集SDK会收集全量的用户交互数据,对于定制的业务收集需求,数据收集SDK也作到了无需代码埋点,经过线上下发一些配置进行即时收集


2、View的惟一标识(ID)

2.1 调研

  用于区分界面上每一个View的ID? Android系统是否提供给了咱们这个ID?

确实,Android系统提供了一个ID,view.getId()便可得到一个int型的id用于区分View,可是这个ID由于如下两个缘由却并不能知足咱们的须要.

  1. 有至关一部分view是NO_ID,好比在布局文件中未指定id,或者直接在代码里面new出来view,view.getId()返回的所有都是NO_ID
  2. 这个ID是不稳定的,因为这个ID其实就是每次编译产生的R文件中的int常量,所以同一个按钮,两个版本编译出来的ID极可能时不同的.

所以,咱们只能本身动手构建咱们的ID喽,怎么构建?答案是利用所属Page+ViewTree构建ViewID.

2.2 利用ViewTree构建ViewID

  在Android的概念里,每一个Window(ActivityWindow/DialogWindow/PopupWindow等)上面都生长着一棵ViewTree.而屏幕中看到的各类控件(ImageView/Button等)都是这棵ViewTree上的节点.
  有Android开发环境的同窗只须要打开AndroidDeviceMonitor-dump view hierarchy 就能够看到ViewTree的模样,以下图:

图2-1 ViewTree概念图

所以,咱们萌生出一个想法:

利用Page+ViewTree中的位置构建ViewID.

View在ViewTree中的位置主要用两点来肯定:

  • 纵向的深度
  • 横向的index

考虑这两个因素后,咱们定义一个ViewPath:

ViewPath:当前view到ViewTree根节点的一条路径,用于在ViewTree中惟必定位当前view。路径中的每一个节点包含两部分信息,即节点View类型信息,以及节点View在兄弟中的index。

以下图,是一个简单的ViewTree模型(简单到深度只有两层,每层只有两三个控件)

图2-2 ViewTree模型图

按照以前给的定义,上图中控件1,2,3,4的ViewPath以下

控件1ViewPath: RootView/LinearLayout[0]   index为1表示此节点是兄弟节点中第一个控件
控件4ViewPath: RootView/LinearLayout[0]/ChildView1[0]
控件2ViewPath: RootView/RelativeLayout[1]
控件3ViewPath: RootView/LinearLayout[2]复制代码

上述给出的ViewPath中,每一个节点(除了首节点)有两部份内容:

  • LinearLayout,RelativeLayout,ChildView1等ViewType信息(节点View的类型
  • "[]"内的index信息,此index指示此节点是兄弟节点的第几个

这是最初的ViewPath,用ViewPath定位view,有两点特别重要:

  • 一致性: 同一个view的ViewPath在ViewTree的动态变化中应保持不变
  • 区分度: 不一样view的ViewPath应该不一样

按照这个最初的ViewPath定义在实践中还不能在一致性和区分度上知足咱们的需求,后面会对ViewPath进行优化。

2.3 ViewPath的生成

  上面咱们由构建ViewID的需求引出了ViewPath的定义,那么当交互事件(例如:按钮点击)发生时,咱们如何生成此控件的ViewPath?
  如上一篇文章< > 所述,当用户点击某个按钮时,咱们插入OnClickListener.OnClick方法中的以下代码将会被调用:

Monitor.onViewClick(view);复制代码

上面,入参view即为当前被点击的view,获取此view的ViewPath伪代码以下:

public static ViewPath getPath(View view) {
    do {
      //1. 构造ViewPath中于view对应的节点:ViewType[index]
      ViewType=view.getClass().getSimpleName();
      index=view在兄弟节点中的index;
      ViewPath节点=ViewType[index];
    }while ((view=view.getParent())instanceof View);//2. 将view指向上一级的节点
  }复制代码

构造出来的ViewPath以下面例子所示:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]复制代码

2.4 ViewPath的优化

a. 一致性优化1
情景:

在图2-2 ViewTree模型图中,若是像下面图中所示,在控件2和3中动态插入一个FrameLayout呢?

图2-3 Android界面动态性变化情景1

此时按照原始ViewPath的定义,咱们来看看控件3的ViewPath发生了哪些变化?

ViewTree动态变化前: RootView/LinearLayout[2]
ViewTree动态变化后: RootView/LinearLayout[3]复制代码

优化:

ViewPath节点中index的含义从“兄弟节点的第几个”优化为:“相同类型兄弟节点的第几个”

优化后,发生图2-3所示界面布局动态变化时,控件3的ViewPath变化为:

ViewTree动态变化前: RootView/LinearLayout[1]   index为1表示此节点是兄弟节点中第二个LinearLayout
ViewTree动态变化后: RootView/LinearLayout[1]复制代码

能够看出,此处优化使控件3的ViewPath在ViewTree动态插入除了LinearLayout以外其它任何类型时都保持先后一致。

b. 一致性优化2
情景:

在图2-2 ViewTree模型图中,若是像下面图中所示,在控件2和3中动态插入一个LinearLayout时,控件3的ViewPath可否继续保持先后一致?

按照上述情景,控件3ViewPath的变化以下:

ViewTree动态变化前: RootView/LinearLayout[1]   index为1表示此节点是兄弟节点中第二个LinearLayout
ViewTree动态变化后: RootView/LinearLayout[2]   前面插入一个LinearLayout致使此节点变为兄弟节点中第三个LinearLayout了复制代码

问题
上述情景指的实际上是一个问题:ViewTree中同类型兄弟节点动态变化(插入/移除/移位)影响ViewPath的一致性

  • ViewPath节点中的index,在同类型(ViewType相同,例如都是LinearLayout)兄弟节点动态加入/删除时,当前节点的index没法在变化先后保持一致。
  • “一致性优化1”中的优化能够抵御不一样类型兄弟节点的影响,却对同类型兄弟节点的影响迫不得已

从ViewPath的定义上难以找到在同类型兄弟节点动态变化先后保持一致的方法,但咱们能够分析发生此种界面动态变化的情景:

  1. 使用Fragment的动态布局
      Android界面的动态布局发生情景中,使用Fragment实现界面动态变化的频率和影响控件数量仍是比较大的(相对于直接addView())
  2. ListView(等可复用View)中同类型的itemViews。
      此种状况虽然没有发生在一个itemView前动态插入一个itemView,可是因为itemView的复用,致使itemView展现的内容和在父节点listView内的index的对应关系动态变化,所以也归于此类。

2中所说“ListView等可复用View”形成的问题后面会有优化,此处针对1中的情景讨论。1中情景发生时以下图:

图2-4 使用Fragment形成界面动态性的情景

  上图中FragmentA,FragmentB,FragmentC的顶层视图控件所有是LinearLayout同类型),此时这三个Fragment加入的顺序将形成ViewPath在此处各类不一致,从而致使ViewPath在动态变化先后不能保持一致(如前面:ViewTree动态变化先后控件3ViewPath的变化所示)。
优化:

在ViewPath节点中,使用Fragment的名字替换ViewType

  优化后,发生图2-4所示界面布局动态变化时,控件3的ViewPath变化为:

ViewTree动态变化前: RootView/FragmentB[0]   index为0表示此节点是兄弟节点中第一个FragmentB
ViewTree动态变化后: RootView/FragmentB[0]复制代码

  如上,这次优化使得,在顶层视图ViewType相同的Fragment动态添加/删除到ViewTree时,ViewPath在变化先后保持一致。

c. 针对可复用View的优化
情景
  以最常使用的ListView为例,假设有一ListView满屏只显示3个条目,那么此ListView可能只有3个子控件(ItemView),而此ListView上滑以后能够显示100项内容
  这3个ItemView与100项内容是一对多的对应关系,并且映射并没有可靠规律。
  此时,咱们但愿ViewPath能够区分这100项显示的内容条目,而非仅仅区分3个ItemView

上面情景中的问题可用下图表达:

图2-5 可复用View的ViewPath区分性优化

  如上图中,内容条目1和4都是用itemView1来呈现的,按照以前的ViewPath定义,图2-5中各个内容条目的ViewPath以下:

内容条目1: ListView/ItemView[0]   index为0表示此节点是兄弟节点中第一个ItemView
内容条目4: ListView/ItemView[0]   
内容条目2: ListView/ItemView[1]  
内容条目3: ListView/ItemView[2]复制代码

  能够看出内容条目1和4的ViewPath区分不开。此种问题能够总结为:

显示内容与ViewTree中的控件不是一一对应的状况形成基于ViewTree的ViewPath区分度不够

  • 可复用View,好比:ListView,RecyclerView,Spinner等,呈现出来子View的数目和实际子View的数目未必一致
  • ViewPager设置缓存页面数为1,第二页显示时,第二个页面顶级View实际上是ViewPager的第一个ChildView。此种状况也会形成显示内容(第二页)与ViewTree中的控件(第一个ChildView)不对应的状况。

所以咱们对于ViewPath做以下优化:

ViewPath节点的index取内容的第几项,而非第几个ItemView。

优化:
优化后图2-5中各个内容条目的ViewPath以下:

内容条目1: ListView/ItemView[0]   index为0表示此节点是ListView显示的第一个内容条目
内容条目4: ListView/ItemView[3]   
内容条目2: ListView/ItemView[1]  
内容条目3: ListView/ItemView[2]复制代码

可见,以前ViewPath没法区分的内容条目1和4如今能够区分开了。各类可复用View取内容的第几项的代码方法以下:

ListView,Spinner等AdapterView------------ListView.getPositionForView(itemView)
RecyclerView------------------------------------RecyclerView.getChildAdapterPosition(itemView)
ViewPager----------------------------------------ViewPager.getCurrentItem()复制代码

d. ViewPath起点优化
  ViewPath从ContentView为起点,而非DecorView

  • DecorView : Window上的根视图,ViewTree中的根,最顶层视图
  • ContentView: 客户端程序员定义的全部视图的父节点,如Actvity中常见的setContentView(view)

一个实际中的ViewPath以下:

DecorView/LinearLayout[0]/FrameLayout[0]/ActionBarOverlayLayout[0]/ContentFrameLayout[0]/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]复制代码

  上面的“ContentFrameLayout[0]”这个节点表明的就是ContentView,程序员在xml或者代码里面构建的View都在ContentView中。

  从DecorView到“ContentFrameLayout[0]”的这一段Path是Android系统Framework层决定的,理论上应该是一致的,可是因为碎片化等缘由可能ViewPath的这一段发生变化.在实践中,咱们也发现确实有一些Rom发生了此类状况,可是比率很小.
  为了屏蔽这种可能形成同一个View在不一样设备上产生ViewPath不一样的状况,ViewPath的起点定义在ContentView比较好.如上面的ViewPath可优化为:

ContentView/FrameLayout[0]/LinearLayout[0]/ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]#mybutton复制代码

作法:
  构造每个ViewPath节点时能够取view.getId(),看看id的packageId部分是否是系统的(系统资源id以16进制的0x01,0x00开头),若是是,生成ViewPath时屏蔽这段便可.


3、页面的划分

3.1 合理划分页面的重要性

  页面在Android中对应于Activity和部分Fragment(好比不少app首页多tab的设计,若每一个tab是使用Fragment实现的,那么这种tab通常看做一个页面).页面的划分很重要,由于两点:

  1. 对于页面,须要获取Show/Hide两个时机,在此时机上报页面Show/Hide事件,非页面则不须要
  2. 页面的划分关系着用户交互事件的所属,例如,按钮点击事件上报格式以下:
事件名称 所属页面 ViewPath 其余属性
ButtonClicked MainActivity XXX 省略

  表格中的"所属页面"表示这次按钮点击事件发生在MainActivity中.将交互事件归属于页面这样对后面不管是进行路径分析仍是统计控件点击量分布都有很大的好处.

3.2 Android中的页面

  Android中一般须要看做页面的有Activity和Fragment(对于像全屏Dialog或者全屏的View暂不考虑).对于Activity,上节中提到的两点都很容易办到.

a. Activity页面

  1. 从Application.ActivityLifecycleCallbacks的onActivityResumed/onActivityPaused这两个回调方法就能够分别获得Activity页面Show/Hide的时机,并在此时机上报相应页面事件
  2. 交互归属的Activity页面能够经过Context轻松得到,例如上篇文章< > 提到,当按钮点击时,会触发咱们插桩的代码:
    Monitor.onViewClick(view)复制代码
    入参view即为咱们点击的view,经过view.getContext()咱们通常就能够获得此View所属的Activity,伪代码以下:
    //从View中利用context获取所属Activity的名字
    public static String getActivityName(View view) {
     Context context = view.getContext();
     if (context instanceof Activity) {
       //context自己是Activity的实例
       return context.getClass().getSimpleName().;
     } else if (context instanceof ContextWrapper) {
       //Activity有可能被系统"装饰",看看context.base是否是Activity
       Activity activity = getActivityFromContextWrapper((ContextWrapper) context);
       if (activity != null) {
         return activity.getClass().getSimpleName();
       } else {
         //若是从view.getContext()拿不到Activity的信息(好比view的context是Application),则返回当前栈顶Activity的名字
         return currentActivityName;
       }
     }
     return "";
    }复制代码

b. fragment页面
  相对于Activity,将某些Fragment看做页面的逻辑就要稍微复杂一些了.这里面涉及下面几个问题:

  • 哪些Fragment能够须要看做页面?
      这是须要人工决策的,机器作不了这个决定.
      目前咱们这我的工干预是交给用户研究团队,全部Fragment截图等信息均展现在平台上,由用研同事选择须要看做页面的那些,用研选择的结果将自动化配置到SDK中
  • 如何获得Fragment页面的Show/Hide页面事件?
      因为fragment使用场景比较多样,单单依靠OnResume/OnPause两个回调表示fragment Show/Hide是不许确的,好比:
    场景一
      首页一个Activity承载多个Fragment Tab的状况,此时tab间切换并不会触发Fragment的OnResume/OnPause.触发的回调函数是onHiddenChanged(boolean hidden)
    场景二:
      一个ViewPager承载多个页面的Fragment时
        a.当第一个Fragment1显示时,虽然第二个Fragment2此时还没有显示,可是Fragment2的OnResume却以及执行,处于resumed的状态.
        b.ViewPager页面切换OnResume/OnPause/onHiddenChanged均未触发,触发的回调是setUserVisibleHint
      此时判断Fragment Show/Hide应该用setUserVisibleHint,而非OnResume/OnPause
      如前一篇文章XXX,所述,咱们经过插桩的方式Hook到了fragment的以下生命周期函数用于包装成为Show/Hide事件:
    onResume()
    onPause()
    onHiddenChanged(boolean hidden)
    setUserVisibleHint(boolean isVisibleToUser)复制代码
    使用这几个回调包装成适用于各类情景的FragmentShow/Hide事件的伪代码以下:
    //此回调发生,则证实是场景一中使用情景,
    onHiddenChanged(boolean hidden) {
      hidden == true ------FragmentShow
      hidden == false------FragmentHide
    }
    //场景二中ViewPager页面切换时触发Fragment的此回调,
    setUserVisibleHint(boolean isVisibleToUser) {
      if (fragment.isResumed()) {//只有resumed状态的fragment适用此情景
        isVisibleToUser == true ------FragmentShow
        isVisibleToUser == false------FragmentHide
      }
    }
    //上述使用情景以外的通常场景
    OnResume/OnPause{
     //fragment没有被hide,而且UserVisibleHint为可见的情景
      if (!fragment.isHidden() && fragment.getUserVisibleHint()) {
        OnResume ------ FragmentShow
        OnPause  ------ FragmentHide
      }
    }复制代码
  • 如何将Fragment内部的交互归属到Fragment页面,也就是说如何在交互发生时从view实例拿到Fragment页面的名字(像以前拿到Activity页面名字同样)?
      view能够经过context拿到Activity的信息,可是却没有途径拿到fragment的引用。那么,当某个View交互发生,咱们又须要获取Fragment页面名字的状况下,咱们只能事先将Fragment页面名写入此View的属性中。
      作法大体以下:
        a. 按照前一篇文章xxx里面的方法,在Fragment.OnCreateView方法的结尾插桩,拿到return的view(即为此Fragment的顶层视图)
        b. 判断此Fragment是否被指定为Fragment页面,若是是,下一步
        c.遍历以Fragment的顶层视图为根节点的ViewTree, 将Fragment名设置到此ViewTree的每个view上。设置方法以下所示:
    view.setTag(0xff000001, fragmentName);复制代码
    注意:View类有两个名为setTag的方法
    public void setTag(final Object tag)复制代码
      此方法,类内部用一Object对象存储tag,protected Object mTag = null;。listAdapter中经常使用于设置holder。咱们此处用的不是这个,不会于此用法冲突
    public void setTag(int key, final Object tag)复制代码
      此方法,类内部有一稀疏数组存储tag,private SparseArray mKeyedTags;
      tag的key官方推荐资源id,所以咱们能够选用相似0xff000001之类的app用不到的资源id进行tag存储以免冲突
        d. 当须要使用Fragment名时,以下调用便可得到:
    view.getTag(0xff000001)复制代码

3.3 页面名组成

前面讲了将交互事件(好比点击事件)归属到某一个页面的方法是:

在交互事件中设置一个字段,值为页面名称。

页面能够是Activity或者Activity承载的Fragment,咱们的页面名称组成以下:

Activity类名[Activity别名][Fragment类名][Fragment别名]复制代码

说明以下:

  1. “[]”内的组成部分是可选的,可能有可能没有。另外,各个组成部分之间有分隔符分割。
  2. 页面名组成中,Activity的描述(类名/别名)是第一层,Fragment的描述(类名/别名)是第二层
  3. 别名的出现是为了解决单纯依赖类名没法精确区分页面的某些状况,好比:
    在某个电商应用中,“商品详情页”(同一个Activity)用于展现各类商品(iphone,电视等),若是须要把“不一样商品的商品详情页“区分红不一样页面来统计pv等指标的话,须要设置别名,如:
    商品详情页#iphone
    商品详情页#电视复制代码
    对于别名的设置,须要程序员在业务代码里面(如Activity.OnCreate,Fragment.onCreate等)显式设置.

4、无需埋点轻松收集定制的业务数据

4.1 配置示例

  以前提到过,数据收集SDK能够经过配置下发即时收集定制的数据,那么在Android端这个是怎么作到的呢?
首先,看一下下发的配置样例:

//第一部分:描述
PageName:MainActivity
ViewPath:DecorView/.../ViewPager[0]/ButtonFragment[0]/AppCompatButton[0]
EventType:ViewClick
//第二部分:数据路径(当描述符合时,按照此路径取数据)
DataPath:this.context.demoList[5]复制代码

上面例子翻译成数据需求就是:

1. 当页面(MainActivity)
2. 中的控件(DecorView/.../ViewPager[0]/ButtonFragment[0]/AppCompatButton[0])
3. 发生点击事件(ViewClick)时
4. 按照路径(this.context.demoList[5])取出数据
5. 并附加到点击事件上面一块儿上报复制代码

按照这个描述,咱们还能够描述以下等等各类数据需求:

当(某页面)发生事件(Show)时,按照路径(xxx)取出数据,并附加到页面Show事件上面一块儿上报复制代码

总结下描述的组成部分,以下:

第一层 第二层 含义
描述部分 页面 限定页面
ViewPath 限定按钮
EventType 限定时机(点击/前台/PageShow)
数据路径 一种DSL,指示目标数据在内存中的位置(可理解为“引用路径”)

4.2 无埋点收集流程

  上节展现了用于无埋点定制业务数据收集的配置,那么SDK收到这样的一份配置如何最终把想要的数据收集上来呢?

  • 步骤一:产生原始事件。好比点击时收集,当点击时会触发咱们插桩的代码,并生成原始的点击事件
    Monitor.onViewClick(view)复制代码
  • 步骤二:匹配配置
    在onViewClick方法中匹配下发的配置信息,看看Page,ViewPath是否与当前view匹配,EventType是否与当前事件类型匹配,若匹配则进行下一步
    注:ViewPath的匹配能够有精确匹配和模糊匹配,精确匹配时一个ViewPath精确匹配惟一一个控件.模糊匹配时一个ViewPath可匹配多个控件,例如能够用用一个ViewPath模糊匹配一个列表中的全部条目.
  • 步骤三:按照数据路径(DataPath)逐级反射拿到目标数据,并将找到的数据附在原始的点击事件上进行上报。

4.3 数据路径(DataPath)

  上述步骤三进行数据收集主要是按照DataPath的描述进行(例如示例中提到的"this.context.demoList[5]"),DataPath是一种咱们用于收集定制数据而定义的一种DSL.含义以下:

a. 含义

DataPath: 指向要收集的目标数据的一条引用路径,解析此路径并逐级反射最终拿到目标数据.

  DataPath写法中的一些关键字(符):

关键字(符) 含义
. 表示对象所属关系,如:a.b 表示实例a中的字段b
.() 表示公有方法调用,如:a.b() 表示调用实例a中的方法b.注意:方法入参能够是DataPath指向的Object
[] 数组/线性表的index. 注意:此index能够是常量数字,也能够是一个DataPath指向的数字
this DataPath字符串的起点,表示起点为当前实例(当前View)
item DataPath字符串的起点,表示起点为当前View父节点中AdapterView adapter中当前条目. 经常使用于列表中的数据获取
parent DataPath节点中的关键字,用于表示当前view的parentView.效果同view.getParent(),使用此关键字可减小视图引用中的反射
childAt(x) DataPath节点中的关键字,用于表示当前view的第x个childView.效果同view.getChildAt(x),使用此关键字可减小视图引用中的反射

b. 应用示例
  下面用两个例子说明如何从DataPath找到目标数据.

图4-1 DataPath示例

示例1:列表数据获取
  上图中显示是一个列表,红框中是列表的第一个条目.那么,若是咱们想要在列表中条目点击时,将列表展现的交易品ID(或者合做方ID)等不在界面上显示而又存在于内存中的数据跟随点击事件上报.此处DataPath该怎么写?

item.productId复制代码

  DataPath解释:

  1. 起点定为"item",则表示今后ListView(或者RecylerView)绑定的Adapter中当前数据item为起点取数据.
    假设此ListView绑定的Adapter以下:
    public class DemoAdapter extends BaseAdapter {
    private ArrayList<DataItem> mDataItems;
    ......
    }复制代码
    则此处"item"表明的就是mDataItems[x] (x表示当前被点击条目的itemId)

2."productId"是model类DataItem中表示"交易品ID"的字段名称.

  经过DataPath获取数据:

  1. 当第x条目被点击时,若是发现有匹配的配置,对于起点为"item"的DataPath,先经过view.getParent找到上层ListView实例,而后经过listView.getAdapter()得到绑定的Adapter实例,最后经过Adapter.getItem(ListView.getPositionForView(itemView))获得数据中第x个item,即mDataItems[x]
  2. 反射获取mDataItems[x]中的productId字段,便可获得第x个条目的"交易品ID",将此ID跟随第x条目的点击事件进行上报便可.

实例2:界面数据获取
  一样时图4-1所示,加入咱们想在列表中条目点击时,将条目中展现的"最新价"跟随点击事件上报.此处DataPath该怎么写?
  红框所示ViewTree子树以下:

图4-2 列表Item ViewTree子树结构

  如上图,选中部分是列表的ItemView(RelativeLayout),可见"最新价"是由index为2的TextView所展现,由此可得,列表中条目点击获取"最新价"数据的DataPath以下:

this.childAt(2).mText复制代码

  DataPath解释:

  1. 起点为"this",表示当前被点击的view实例(图4-2中被选中的RelativeLayout)
  2. "childAt(2)"表示RelativeLayout.getChildAt(2),获得图4-2中index为2的TextView
  3. "mText" 表示取出步骤2中获得TextView实例的mText字段(TextView控件显示的文字内容存储在mText字段内)
  4. 将取出的界面上显示的"最新价"数据添加到原始点击事件中,一块儿上报.

c. DataPath注意事项:
1.混淆.
  因为DataPath本质上描述的时内存中的"引用路径",而且按照DataPath取数据时用了反射的方法,所以DataPath应该描述的是混淆以后的"引用路径".
  虽然DataPath可能受到混淆的影响,可是

* 用于存储数据的model类一般是不被混淆的.如咱们以前的item关键字直接将起点设置为列表条目的model类对象,不受混淆影响.
* 经过关键字parent/childAt(x)能够在视图的引用中不受混淆影响
* 接口的方法一般不受混淆影响.所以在DataPath中多用接口方法调用复制代码

  所以开发在配置DataPath时应尽可能用上述不被混淆影响的字段及方法.可是,若是真的用到了混淆过的字段怎么办.咱们的方案是:

数据报警

  好比版本1上配置的DataPath "a.b",在升级新版本2后再也不适用,则新版本2按照"a.b"收集时将收集不到,产生报警信息到后台.后台收到大量此种信息会提醒开发为新版本配置适用新版本的DataPath.

2.代码变化致使引用路径变化,从而导致以前配置的DataPath失效.
  与代码中埋点相比,线上配置进行收集数据与代码的变化是并行的,无关的.这就有可能形成原有代码修改致使DataPath失效.其实若是客户端架构设计合理,功能迭代更可能是在进行代码的扩展,而非修改,这种致使DataPath失效的状况应该会大大下降的.
  可是不管如何:

配置的DataPath摆脱不了与版本的相关性

  对于此种问题咱们依然是经过前面提到的"数据报警"进行监控及避免的.


5、结语

  综上,本文介绍了数据收集逻辑中3个比较关键的点(ViewID/Page/DataPath),结合上一篇文章的(AOP原理),Android端无埋点数据收集技术上比较关键的点皆以总结完毕.  固然实现SDK过程当中遭遇过不少比较有意思的技术问题,后续也会陆续进行整理.

相关文章
相关标签/搜索