Android端可视化埋点的实现
***导语 ***客户端埋点是数据网络的最基本本领,对于一款APP来说,代码埋点(就是在业务代码中,在必要埋点的view的点击变乱回调处做点击上报的处置惩罚,当此view被点击时,举行相应变乱的上报)是最为常见的埋点方式,但由于业务迭代速率很快,手动埋点方案固然机动多变,但是极大的增加了客户端开辟职员的工作量。开辟完成业务功能必要花费很大的精神处置惩罚埋点变乱,而且随着迭代版本,新的需求增加,埋点的数量会越来越多,这些老旧埋点的维护工作也必要付出不小的积极。而且,代码埋点必要跟版本迭代,假如在开辟过程中忘记埋点,就只能等待下个版本再埋点。以是,假如可以或许实现不必要大概很少必要开辟职员参与就能实现根据差别业务场景埋点的功能对于进步版本迭代速率和开辟职员的幸福感绝对是一件非常有代价的变乱。 以是,为了镌汰代码埋点的未便,不必要开辟职员参与,使运营大概用研的同砚就可以随时动态调解必要上报的点,我们必要改变下当前的埋点方式。
一、需求配景
1、什么是埋点
预先在目的应用收罗数据,对特定用户举动或变乱举行捕获、处置惩罚以肯定方式上报至服务器的干系技能及其实行过程。
在大数据话的今天,一个好的产物就应该符适用户的需求,我们做统计就是网络用户的举动,分析用户的喜好,不停的根据用户举动改进我们的产物,当然专业的必要数据分析来完成,我们必要的就是为分析提供用户数据。
原来我们的方式是代码埋点,在每一个要上报的view的点击变乱都都要由开辟职员参加上报的代码。
public void onClick(View v) { .. .. //在点击变乱回调中走同一的上报 ClickReport.collect(ClickReport.MineAction.MINE_MORE_PRIVILEGE); .. ..}复制代码2、存在的标题
但由于业务迭代速率很快,手动埋点方案固然机动多变,但是也存在很多标题:
1、开辟完成业务功能必要花费很大的精神处置惩罚埋点变乱,而且多余的代码显得很冗余,浪费开辟时间
2、随着迭代版本,新的需求增加,埋点的数量会越来越多,这些老旧埋点的维护工作也必要付出不小的积极。
3、代码埋点必要跟版本迭代,无法实现动态化的设置,假如在开辟过程中忘记埋点,就只能等待下个版本再埋点。
以是,为了镌汰代码埋点的未便,镌汰开辟职员参与,使运营大概用研的同砚就可以随时动态化的调解必要上报的点,我们必要改变下当前的埋点方式,开辟一个符合客户端埋点的上报模式。
二、可视化埋点
1、埋点的三种方式:
由于差别的app也会有自己的异化处置惩罚,埋点的方式也是根据特殊情况有这浩繁方案的,但是大要上现在紧张盛行的就是三种方案:
1、代码埋点:将网络数据的代码直接写在必要的地方,当用户点击某个控件大概打开某个页面时调用到该部门代码完成数据的网络。
- 优点:准确性高,网络数据和发送数据都能准确控制,同时能方便的设置自界说属性,自界说控件,自界说View等。
- 缺点:埋点工作量大,更新代价大,必要跟版本,不机动。
2、可视化埋点:根据可视化界面举行设置然后上报背景,背景向终端下发设置文件,终端点击时获取当前点击的控件根据设置文件举行选择上报。
- 优点:数据量相对准确,覆盖范围大。
- 缺点:可视化平台搭建困难,控件树的元素的准确辨认较为贫苦。
3、无埋点:与可视化埋点基本划一。差别点在于可视化埋点是根据设置文件网络数据,无埋点是预先网络全部的用户举动,然后根据设置文件来提取数据。
- 优点:数据覆盖全,开辟职员工作量小。
- 缺点:数据量大,后端筛选分析工作量大,每个点都统计对性能有隐患。
2、为什么选择可视化的埋点
对于无埋点,和可视化埋点的方案很相似,区别在于无埋点必要将以是的点击都网络,后续在根据设置文件在分析想要的数据,这种方式对于客户端来说很省事,客户端也不消分析是哪个点,只要用户点击了就上报,但是对于后端的筛选和分析肯定的影响,而且大概我们必要统计的点并不是很多时大概大多数上报的点都不是我们想要的,这时用无埋点的方式感觉有些得不偿失。
MTA也有一个可视化埋点的功能,但是咱们我们要上报的平台有很多,好比要上报到罗盘大概我们自己服务器的统计平台,仅仅利用MTA的sdk无法满意各种平台的上报,以是我们选择可视化埋点的方式统计用户的举动,而且在同一的模块举行各个平台的上报。
三、可视化埋点方案探究与详细实现
1、方案先容
紧张的流程可以分为设置埋点模式,和用户模式。
上报的信息:
{ String ViewId; //当前View的唯一ID 由路径得到 String EventId; //View的变乱ID 由产物干系职员设置}复制代码在设置模式中,由产物同砚大概运营同砚设置必要上报的点击view,将点击view的唯一标识viewID和事先定好的用于分析的eventID绑定形成一个设置map表,设置竣过后,将已经设置完成的map表上传至背景,完成设置模式。
[图片上传失败...(image-4aaf40-1649852259824)]
<figcaption style="display: block;">图1:可视化埋点流程</figcaption>
在设置模式中,由产物同砚大概运营同砚设置必要上报的点击view,将点击view的唯一标识viewID和事先定好的用于分析的eventID绑定形成一个设置map表,设置竣过后,将已经设置完成的map表上传至背景,完成设置模式。
在用户模式下,就是当用户开启app时,会拉取到在产物大概运营同砚在设置模式下设置的map表,然后当用户点击相应点时,可以通过某种方式与设置表中的点做对照,假如在设置表中,就走上报逻辑。
通过上述流程,可以总结出以下三个标题:
- 怎样获取当前点击的view;
- 怎样确定view的唯一标识viewID;
- 用户点击view的上报流程;
下面针对这三个标题举行分析和方案简直定。
2、获取当前点击的view
由于我门必要在一个同一的地方获取当前的点击变乱,来做同一的设置大概筛选上报工作,这些工作不大概在每一个view的点击变乱回调中完成,比力普遍的做法就是遍历当前activity下的viewtree,根据UI结构的特性和Android点击变乱通报机制来找到当前的view。
主流的方案:通过位置遍历盘算
让项目中主框架的BaseFragmentActivity基类重写Activity的dispatchTouchEvent方法,当touch button时,可以获取到按下(DOWN)和抬起(UP)时产生的MotionEvent对象。这个MotionEvent对象有两个方法,getRawX()和getRawY(),通过这两个方法我们可以获取到“点击位置”在界面中的坐标。通过rootview可以层层遍历其下的子view以及全部子View上的控件,这些View和控件在屏幕中的坐标和宽高我们是可以获取到的。然后搜索全部的子View大概控件的结构地区是否包罗“点击位置”,从而来判定哪个View或控件被点击。
/** * 通过遍历的方式获取当前view */private View searchClickView(View view, MotionEvent event,StringBuffer stringBuffer) { .. .. if (isInView(view, event) && view.getVisibility() == View.VISIBLE) { //这里肯定要判定View是可见的 if (view instanceof ViewGroup) { //遇到一些Layout之类的ViewGroup,继承遍历它下面的子View ViewGroup group = (ViewGroup) view; for (int i = group.getChildCount() - 1; i >= 0; i--) { View chilView = group.getChildAt(i); clickView = searchClickView(chilView, event,stringBuffer); if (clickView != null) return clickView; } } } .. ..}复制代码但是这种获取view的方式究竟是靠遍向来得到的,难免对性能有一些影响,其实通过viewGroup的TouchTarget的范例可以有一种更好的方式去得到当前的view。
我们的方案:通过TouchTarget范例获取
ViewGroup中有一个TouchTarget 范例的变量 mFirstTouchTarget,表现消耗当前触摸变乱的控件列表。比方,点击屏幕上一个按钮,那么按钮地点ViewGroup的mFirstTouchTarget 变量就指向这个按钮。当ViewGroup派发触摸变乱时,他会起首判定变量mFirstTouchTarget是否存在,假如变量存在,会循环遍历TouchTarget链表元素,找到能处置惩罚该变乱的View并将MotionEvent 派发给该View。假如不存在TouchTarget,ViewGroup 会循环遍历全部child view,直到找到一个能处置惩罚该变乱的View,并将该View作为first touch target 赋值给mFirstTouchTarget。 当用户触发Down变乱时,会实行如下逻辑,探求消耗当前变乱的TouchTarget。
if (actionMasked == MotionEvent.ACTION_DOWN){ //假如是down变乱,遍历child,找到TouchTarget .. .. final View[] children = mChildren; .. .. if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // child 消耗了触摸变乱 .. .. // 根据消耗了触摸变乱的View创建TouchTarget newTouchTarget = addTouchTarget(child, idBitsToAssign); .. .. break; }}复制代码addTouchTarget就是获取子view的touchTarget,并将其参加到TouchTarget链的最顶部,由于是从获取到DOWN变乱的view层层向上递归,以是TouchTarget链的尾端就是目的view。
private TouchTarget addTouchTarget(View child, int pointerIdBits) { TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target;}复制代码当触发Down变乱而且找到TouchTarget,大概触发非Down变乱时,实行如下处置惩罚逻辑。
if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS);} else { //Down变乱发生时找到TouchTarget,大概非Down变乱直接实行如下逻辑 // 将变乱派发给TouchTarget表现的View TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { final boolean cancelChild = resetCancelNextUpFlag(target.child)|| intercepted; if (dispatchTransformedTouchEvent(ev, cancelChild,target.child,target.pointerIdBits)) { //指定TouchTarget对应的View准确消耗了变乱 handled = true; } .. .. } .. .. }}复制代码由于根据view的点击变乱可知,view点击时变乱是从根节点开始向下举行通报,假如viewgroup存在TouchTarget,会从TouchTarget的成员变量中获取当前的处置惩罚变乱的view大概viewgroup,假如该viewgroup存在TouchTarget,就继承向下查找,直到当前的viewgroup的TouchTarget为空时,就分析是此viewgroup消耗了这个变乱;大概直到通报到一个view而不是viewgroup时,此view就是当前用户点击的view。
由于mFirstTouchTarget是一个TouchTarget范例的私有成员变量,我们必要通过反射的方式去获取:
/** * 获取当前点击的view */public final View getTouchTarget(final ViewGroup vg) { .. .. while (currentTarget != null) { Field fieldTouchTarget = ReflectHelper.getDeclaredField(ViewGroup.class, "mFirstTouchTarget"); Object touchTarget = ReflectHelper.getFieldValue(fieldTouchTarget, currentTarget); if (touchTarget == null) { break; } Field fieldChild = ReflectHelper.getDeclaredField(touchTarget.getClass(), "child"); View child = (View) ReflectHelper.getFieldValue(fieldChild, touchTarget); if (child instanceof ViewGroup) { preTarget = currentTarget; currentTarget = child; continue; } else if (child instanceof View) { currentTarget = child; break; } } .. ..return currentTarget;}复制代码利用ViewGroup的这种变乱处置惩罚机制,可以在activity的dispatchTouchEvent中添加处置惩罚逻辑,假如汲取到down变乱,就让其通报下去使mFirstTouchTarget被赋值,当汲取到up变乱时,做相应的获取当前view逻辑,通过rootview的mFirstTouchTarget获取其子view,假如子view的mFirstTouchTarget不为空,就通过这种TouchTarget的链式关系得到这次点击举动的终极view。
3、唯一的标识一个view
根据上述流程,用户模式下,当用户点击某个view时,必要一个唯一的viewID用来在设置表中举行查找,而且必要与其他的view做区分;在设置模式下也必要通过唯一标示的viewID作为key与作为value的eventID共同上报到设置表中,那么应该怎样选取这个唯一的标示呢?
主流的方案:遍历viewTree天生路径
对于view来说,Android体系提供了一个ID,view.getId()即可得到一个int型的id用于区分View,但是这个ID由于以下两个缘故原由却并不能满意我们的必要。
- 有相当一部门view是NO_ID,好比在结构文件中未指定id,大概直接在代码内里new出来view,view.getId()返回的全部都是NO_ID
- 这个ID是不稳固的,由于这个ID其实就是每次编译产生的R文件中的int常量,因此同一个按钮,两个版本编译出来的ID很大概是不一样的。
以是为了使viewID不受版本和手机的影响,通常的方式就是利用所属activity+viewtree的路径情势来构建viewID。
[图片上传失败...(image-23560b-1649852259824)]
<figcaption style="display: block;">图2:view层级表现图</figcaption>
通过遍历当前activity的viewTree的方式,得到当前view的父类、祖父类一直到根view,然后根据view的路径层级关系再来确定viewID。但是这又必要遍历,我们不应该每得到一个view都要举行遍历,如许也是比力繁琐的。
我们的方案:通过TouchTarget链获取
在第一个标题假如获取当前view时已经说过,可以通过viewGroup的TouchTarget链来获取当前的点击view,而且链上的层级关系就是从根view到父view再到子view的一个层级,以是在获取view的同时就可以趁便纪录层级关系,来作为viewID。
可以在获取当前view的过程中,纪录每一个节点TouchTarget的mFirstTouchTarget,如许就可以在得到点击view的同时,也得到相应的view的路径好比:DecorView-LinearLayout-FrameLayout-RelativeLayout-Button,再设置上view的class类名等信息,我们就可以同时确定目的控件的唯一标识,将viewID通过setTag举行设置,在必要的地方取出。以是在方案上我们还是用利用获取viewgroup的TouchTarget来得到当前点击的view,同时也可以根据这种链式关系来纪录点击view的从父类到子view的路径,从而将此路径作为当前view的唯一标示viewID。
4、用户端的埋点上报流程
对于可视化埋点操纵来说,在用户模式下,当用户开启app时举行设置表拉取的操纵,然后当用户举行点击时,通过遍历当前activity下的viewtree得到点击的view,大概通过上述分析的查找viewtree根视图的TouchTarget链表的方式来获取当前的view,然后从设置表中查找出与点击view的viewID划一的数据,举行上报。
主流的方案:view署理监听的方式
尚有一种比力常用可视化埋点方式就是view的署理监听的方式,ActivityLifecycleCallbacks,用来监听Activity生命周期,当activity被开启时,遍历当前activity下的全部view,假如view在设置表中,就设置当前view的setAccessibilityDelegate。
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)public class VisualAnalysisManager extends View.AccessibilityDelegate{ .. .. view.setAccessibilityDelegate(this); .. ..}复制代码当view产生了click大概long_click等点击变乱时,分析View的源码在处置惩罚点击变乱的回调时调用了 View.performClick 方法,内部调用了sendAccessibilityEvent,会在相应原有的Listener方法后,发送消息给AccessibilityDelegate,然后在继承AccessibilityDelegate的类中重写sendAccessibilityEvent方法来上报自动埋点变乱。
public boolean performClick() { .. sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); ..}public void sendAccessibilityEvent(int eventType) { if (mAccessibilityDelegate != null) { mAccessibilityDelegate.sendAccessibilityEvent(this, eventType); } else { sendAccessibilityEventInternal(eventType); }}复制代码署理监听的方式就是在ActivityLifecycleCallbacks监听到activity开启时对viewtree举行遍历,假如遍历view的viewID在背景下发的设置表中,就设置setAccessibilityDelegate,这种方式比力常用,但也要求sdk版本大于14。
我们的方案:获取viewID与设置表对比
用户模式与设置模式都必要获取当前点击的view,以是除类用上述view的署理监听的方式举行上报,还可以与设置模式类似,用TouchTarget链表的方式来获取当前的view,再判定当前的点击view是否必要上报。本需求在用户模式下也是接纳TouchTarget链表的方式获取当前view举行上报,与设置模式同一,方便开辟也制止署理监听的方式在在sdk小于14无法利用的困扰。
在获取view中,必要获取当前的activity,以及我们大概必要在当前activity生命周期中去做一些设置以及初始化等操纵,最简单的方式是通过ActivityLifecycleCallbacks来监听activity的生命周期。然后在回调中做相应的操纵,可以实今世码解藕。
public class ActivityTrackUtils implements ActivityTracer.ActivityLifecycleCallbacks { .. .. private ActivityTracer activityTracer;public void start(Application app) {activityTracer.install(app); activityTracer.registerActivityLifecycleCallbacks(this); }@Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) {if (callback != null) {callback.onActivityCreated(activity, savedInstanceState); } .. ..复制代码四、实现中遇到的标题以及办理方式
1、兼容性标题上的优化
上面第四章也分析了,获取当前view是通过反射的方式获取viewGroup中的私有成员,如许就大概由于差别手机api版本差别,源码会有一些差别,大概在某个版本的api中就无法得到,固然如许的概率很小,但是为了包管代码的稳固,进步鲁棒性,还是可以提供一个降级方案:
public final View getTouchTarget(final ViewGroup vg,final MotionEvent event) { .. .. //方案A targetView = mVisualAnalysisHelper.getTouchTarget(vg); //方案B //假如mFirstTouchTarget的方式查询不出来,走降级方案 if(targetView == null){ targetView = mVisualAnalysisHelper.getTouchTargetForPlanB(vg,event); } ..复制代码可以将一个标题中的获取view的主流方法作为降级方案,通过遍历当前viewTree的情势,判定当前的坐标是否在view的范围之中,并循环遍历viewGroup中的子view,终极可以获取到当前的点击view。
2、特殊控件的处置惩罚
有些控件大概会提前截取点击变乱单独做处置惩罚,如许变乱无法下发,导致无法获取当前的view,TouchTarget为null,好比微云项目中有个大标题中的控件,在我查找了浩繁缘故原由后发现其实就是它在onInterceptTouchEvent 方法中自己做了处置惩罚,制止了变乱的通报,对于这种特殊的标题,只能做一下单独的处置惩罚,在它处置惩罚之前,判定它的子view中是否有我们必要的view,假如必要就获取view做相应的处置惩罚。
尚有一个标题就是dialog中的view是无法处置惩罚的,由于在Android中,dialog其实与activity都有自己的wondow,独立与activity的view层级,它与activity有各自的点击变乱通报,以是要针对dialog的变乱通报做单独的处置惩罚,在baseDialog的dispatchTouchEvent中做与activity类似的判定,根据模式的差别做差别的处置惩罚。
@Override public boolean dispatchTouchEvent(MotionEvent ev) { .. .. /** * 可视化埋点 在设置模式下就返回false,非设置模式下判定点是否在上报map中,有就上报 */ VisualAnalysisManager visualAnalysisManager = VisualAnalysisManager.getInstance(); if(ev.getAction() == MotionEvent.ACTION_UP)if (visualAnalysisManager.getIsInConfigVisual()) { //设置模式 visualAnalysisManager.handleTouchEventInConfigMode(ev); return false; }else { //用户模式//截取up变乱,让down变乱通报下去,来获取当前点击的view visualAnalysisManager.handleTouchEventInNormalMode(ev); } .. ..复制代码五、待优化的viewID标题
viewID是根据view的层级来确定的,假如项目举行重构大概变动层级,类似view的viewID就会变革,导致很多view要重新设置,有一种束缚ID的方案,就是单独天生一个view与viewID的对应表,但是如许在添加新view时又要做相应的对应,也会带来开辟上的未便利,以是目前还是维持现有的天生viewID的方案,当遇到重构大概层级变动的标题时就只能将上报的点迁徙并重新天生viewID再上报,这是一个待优化的方向,后续想到符合的方案时会将其优化。 |