学向勤中得,萤窗万卷书。这篇文章主要讲述Android无埋点数据收集SDK关键技术解析相关的知识,希望能为你提供帮助。
前言 本文基于网易乐得无埋点数据收集SDK,
无埋点数据收集SDK用于向大数据平台提供全量,
完整,
准确的客户端数据.
android端无埋点数据收集SDK实现中涉及到比较关键的技术点有:
- 用字节码插桩的方式实现Android端的AOP(“Hook”)
- 唯一定位界面上任何控件的ViewID
- Fragment页面的合理划分
- 自定义数据收集DSL, 用于线上配置, 即时收集定制的业务数据
一、概述 本部分首先简要介绍一下我们的收集方案目前可以收集到哪些数据, 然后对于本文重点介绍的三个技术点进行概述.
1.1 SDK数据收集能力现状 目前我们的SDK进行数据收集时基本有两个能力:
通用数据全量收集
通用数据指的是与业务无关的用户行为数据, 无论是电商应用还是社区应用, 接入SDK后通用数据的收集上都是无差的, 这些通用数据大致有:
事件 | 描述 |
---|---|
冷启动事件 | App第一次启动时的, 版本号、设备ID、渠道、内存使用情况, 磁盘使用情况等信息 |
前后台事件 | App进入前台或者后台 |
页面事件 | 页面( Activity或Fragment) 显示( Show) / 隐藏( Hide) |
控件点击事件 | 某个控件( 包括页面上控件和弹窗中控件) 被用户点击 |
列表浏览事件[可选] | 某个列表的哪些条目被用户浏览了 |
位置事件[可选] | 上报用户地理位置信息 |
其它事件 | 省略描述 |
除了上述通用数据, 与具体业务相关的数据收集。拿网易贵金属的首页举个例子:
文章图片
假使需要在用户点击上图红框区域时, 把“粤贵银”这个交易品的ID( 或者下方显示的指数等, 只要在内存中存在的数据都可以) 一起报上来。
对于此种需求, 数据收集SDK做到了无需埋点, 不依赖开发周期, 通过线上下发一些配置信息, 即可即时进行数据收集。具体原理第四节叙述。
1.2关键技术点概述 View的唯一标识( ID) ,( 详见本文第二节)
当我们收集控件数据时碰到的第一个问题就是: 如何把界面上的任何一个View与其他View区分开来.
比如: 某个Button被点击了这就需要为界面上的每一个控件生成一个唯一的ID. 此ID除了具有区分性, 还需要用于一致性. 一致性是同一个View无论界面布局如何动态变化, 或者说多次进入同一页面, 此ID需要保持不变.
我们在上报数据的时候需要把这个Button和其他所有控件( 比如另一个Button, 另一个ImageView等) 区分开来, 这样这条上报的数据才能表示" 就是那个Button被点击了一下" .
页面的划分, ( 详见本文第三节)
除了Activity有些Fragment也需要看作页面, 这就要求:
- 在Fragment show/hide时上报相关页面事件.
- 页面Fragment中发生的用户交互事件也需要归于此Fragment页面, 即点击某个View需要上报页面Fragment的信息( 从View中怎么获取Fragment信息? )
如前面所述, 默认情况下数据收集SDK会收集全量的用户交互数据, 对于定制的业务收集需求, 数据收集SDK也做到了无需代码埋点, 通过线上下发一些配置进行即时收集.
二、View的唯一标识( ID) 2.1 调研 用于区分界面上每个View的ID? Android系统是否提供给了我们这个ID?
确实,Android系统提供了一个ID,view.getId()即可获得一个int型的id用于区分View,但是这个ID因为以下两个原因却并不能满足我们的需要.
- 有相当一部分view是NO_ID, 比如在布局文件中未指定id,或者直接在代码里面new出来view, view.getId()返回的全部都是NO_ID
- 这个ID是不稳定的, 由于这个ID其实就是每次编译产生的R文件中的int常量, 因此同一个按钮, 两个版本编译出来的ID很可能时不一样的.
2.2 利用ViewTree构建ViewID 在Android的概念里, 每个Window(ActivityWindow/DialogWindow/PopupWindow等)上面都生长着一棵ViewTree.而屏幕中看到的各种控件(ImageView/Button等)都是这棵ViewTree上的节点.
有Android开发环境的同学只需要打开AndroidDeviceMonitor-dump view hierarchy 就可以看到ViewTree的模样, 如下图:
文章图片
因此, 我们萌生出一个想法:
利用Page+ ViewTree中的位置构建ViewID.View在ViewTree中的位置主要用两点来确定:
- 纵向的深度
- 横向的index
ViewPath: 当前view到ViewTree根节点的一条路径, 用于在ViewTree中唯一定位当前view。路径中的每个节点包含两部分信息,即节点View类型信息, 以及节点View在兄弟中的index。如下图, 是一个简单的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指示此节点是兄弟节点的第几个
- 一致性: 同一个view的ViewPath在ViewTree的动态变化中应保持不变
- 区分度: 不同view的ViewPath应该不同
2.3 ViewPath的生成 上面我们由构建ViewID的需求引出了ViewPath的定义, 那么当交互事件( 例如: 按钮点击) 发生时, 我们如何生成此控件的ViewPath?
如上一篇文章Android AOP之字节码插桩所述, 当用户点击某个按钮时, 我们插入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的优化 2.4.1一致性优化1
情景:
在图2-2 ViewTree模型图中, 如果像下面图中所示, 在控件2和3中动态插入一个FrameLayout呢?
文章图片
此时按照原始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之外其它任何类型时都保持前后一致。
2.4.2一致性优化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的定义上难以找到在同类型兄弟节点动态变化前后保持一致的方法, 但我们可以分析发生此种界面动态变化的情景:
- ViewPath节点中的index, 在同类型( ViewType相同, 例如都是LinearLayout) 兄弟节点动态加入/删除时, 当前节点的index无法在变化前后保持一致。
- “一致性优化1”中的优化可以抵御不同类型兄弟节点的影响, 却对同类型兄弟节点的影响无可奈何。
- 使用Fragment的动态布局
Android界面的动态布局发生情景中, 使用Fragment实现界面动态变化的频率和影响控件数量还是比较大的( 相对于直接addView()) - ListView(
等可复用View)
中同类型的itemViews。
此种情况虽然没有发生在一个itemView前动态插入一个itemView,但是由于itemView的复用, 导致itemView**展示的内容和在父节点listView内的index的对应关系动态变化**, 因此也归于此类。
文章图片
上图中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在变化前后保持一致。
2.4.3针对可复用View的优化
情景
以最常使用的ListView为例, 假设有一ListView满屏只显示3个条目, 那么此ListView可能只有3个子控件( ItemView) ,而此ListView上滑之后可以显示100项内容。
这3个ItemView与100项内容是一对多的对应关系, 而且映射并无可靠规律。
此时, 我们希望ViewPath可以区分这100项显示的内容条目, 而非仅仅区分3个ItemView。
上面情景中的问题可用下图表达:
文章图片
如上图中, 内容条目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区分度不够因此我们对于ViewPath作如下优化:
- 可复用View,比如: ListView,RecyclerView,Spinner等, 呈现出来子View的数目和实际子View的数目未必一致
- ViewPager设置缓存页面数为1, 第二页显示时, 第二个页面顶级View其实是ViewPager的第一个ChildView。此种情况也会造成显示内容( 第二页) 与ViewTree中的控件( 第一个ChildView) 不对应的情况。
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()
2.4.4 ViewPath起点优化
ViewPath从ContentView为起点, 而非DecorView
一个实际中的ViewPath如下:
- DecorView : Window上的根视图, ViewTree中的根, 最顶层视图
- ContentView: 客户端程序员定义的所有视图的父节点, 如Actvity中常见的setContentView(view)
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.1 合理划分页面的重要性 页面在Android中对应于Activity和部分Fragment(比如很多app首页多tab的设计, 若每个tab是使用Fragment实现的,那么这种tab一般看作一个页面). 页面的划分很重要, 因为两点:
1. 对于页面, 需要获取Show/Hide两个时机, 在此时机上报页面Show/Hide事件,非页面则不需要
2. 页面的划分关系着用户交互事件的所属, 例如, 按钮点击事件上报格式如下:
事件名称 | 所属页面 | ViewPath | 其他属性 |
---|---|---|---|
ButtonClicked | MainActivity | XXX | 省略 |
3.2 Android中的页面 Android中通常需要看作页面的有Activity和Fragment(对于像全屏Dialog或者全屏的View暂不考虑). 对于Activity, 上节中提到的两点都很容易办到.
Activity页面
- 从Application.ActivityLifecycleCallbacks的onActivityResumed/ onActivityPaused这两个回调方法就可以分别得到Activity页面Show/Hide的时机, 并在此时机上报相应页面事件
- 交互归属的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 "
"
;
}
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的属性中。
做法大致如下:
1. 按照前一篇文章xxx里面的方法, 在Fragment.OnCreateView方法的结尾插桩, 拿到return的view( 即为此Fragment的顶层视图)
2. 判断此Fragment是否被指定为Fragment页面,如果是, 下一步
3. 遍历以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存储以避免冲突。
4. 当需要使用Fragment名时, 如下调用即可获得:
view.getTag(0xff000001)
3.3 页面名组成 前面讲了将交互事件( 比如点击事件) 归属到某一个页面的方法是:
在交互事件中设置一个字段, 值为页面名称。页面可以是Activity或者Activity承载的Fragment, 我们的页面名称组成如下:
Activity类名[Activity别名][Fragment类名][Fragment别名]
说明如下:
- “[]”内的组成部分是可选的, 可能有可能没有。另外, 各个组成部分之间有分隔符分割。
- 页面名组成中, Activity的描述( 类名/别名) 是第一层, Fragment的描述( 类名/别名) 是第二层
- 别名的出现是为了解决单纯依赖类名无法精确区分页面的某些情况,
比如:
在某个电商应用中, “商品详情页”( 同一个Activity) 用于展示各种商品( iphone,电视等) , 如果需要把“不同商品的商品详情页“区分成不同页面来统计pv等指标的话, 需要设置别名, 如:
商品详情页#
iphone
商品详情页#
电视
对于别名的设置, 需要程序员在业务代码里面( 如Activity.OnCreate,Fragment.onCreate等) 显式设置.
四、无需埋点轻松收集定制的业务数据 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, 指示目标数据在内存中的位置( 可理解为“引用路径”) |
* 步骤一: 产生原始事件。比如点击时收集, 当点击时会触发我们插桩的代码, 并生成原始的点击事件
Monitor.onViewClick(view)
- 步骤二:
匹配配置
在onViewClick方法中匹配下发的配置信息, 看看Page,ViewPath是否与当前view匹配, EventType是否与当前事件类型匹配, 若匹配则进行下一步
注: ViewPath的匹配可以有精确匹配和模糊匹配, 精确匹配时一个ViewPath精确匹配唯一一个控件. 模糊匹配时一个ViewPath可匹配多个控件, 例如可以用用一个ViewPath模糊匹配一个列表中的所有条目. - 步骤三: 按照数据路径( DataPath) 逐级反射拿到目标数据, 并将找到的数据附在原始的点击事件上进行上报。
含义
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),使用此关键字可减少视图引用中的反射 |
下面用两个例子说明如何从DataPath找到目标数据.
文章图片
示例1 : 列表数据获取
上图中显示是一个列表, 红框中是列表的第一个条目. 那么, 如果我们想要在列表中条目点击时, 将列表展示的交易品ID(或者合作方ID)等不在界面上显示而又存在于内存中的数据跟随点击事件上报. 此处DataPath该怎么写?
item.productId
DataPath解释:
- 起点定为" 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获取数据:
- 当第x条目被点击时, 如果发现有匹配的配置, 对于起点为”item”的DataPath, 先通过view.getParent找到上层ListView实例, 然后通过listView.getAdapter()获得绑定的Adapter实例, 最后通过Adapter.getItem(ListView.getPositionForView(itemView))得到数据中第x个item,即mDataItems[x]
- 反射获取mDataItems[x]中的productId字段, 即可得到第x个条目的" 交易品ID" ,将此ID跟随第x条目的点击事件进行上报即可.
同样时图4-1所示, 加入我们想在列表中条目点击时, 将条目中展示的”最新价”跟随点击事件上报. 此处DataPath该怎么写?
红框所示ViewTree子树如下:
文章图片
如上图, 选中部分是列表的ItemView( RelativeLayout) ,可见”最新价”是由index为2的TextView所展示, 由此可得, 列表中条目点击获取”最新价”数据的DataPath如下:
this.childAt(2).mText
DataPath解释:
- 起点为" this" ,表示当前被点击的view实例( 图4-2中被选中的RelativeLayout)
- “childAt(2)”表示RelativeLayout.getChildAt(2),得到图4-2中index为2 的TextView
- “mText” 表示取出步骤2 中得到TextView实例的mText字段( TextView控件显示的文字内容存储在mText字段内)
- 将取出的界面上显示的”最新价”数据添加到原始点击事件中, 一起上报.
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摆脱不了与版本的相关性对于此种问题我们依然是通过前面提到的" 数据报警" 进行监控及避免的.
五、结语 【Android无埋点数据收集SDK关键技术解析】综上, 本文介绍了数据收集逻辑中3 个比较关键的点( ViewID/Page/DataPath) , 结合上一篇文章的(AOP原理), Android端无埋点数据收集技术上比较关键的点皆以总结完毕.
当然实现SDK过程中遭遇过很多比较有意思的技术问题, 后续也会陆续进行整理.
推荐阅读
- android矩阵具体解释
- Android Warning not all local changes may be shown due to an error
- android binder 机制二(client和普通server)
- 本地如何搭建IPv6环境测试你的APP(转)
- android的download manager
- Android四大组件-Broadcast Receiver
- Android自定义View探索—生命周期
- Windows cordova build Error: Could not find gradle wrapper within Android SDK.(转)
- android 自定义控件之ViewGroup生命周期执行步骤