alibaba/android_viewtracker源码详解
2017-12-26 13:27
423 查看
最近逛逛了阿里的github发现有这么两个小框架android_viewtracker和Virtualview-Android,android_viewtracker是用来监测点击事件,比如我想统计app每个界面的控件被点击次数,从而统计出那个界面被浏览的次数多,这里只是一个借鉴,统计app最常统计的是崩溃率、一天的启动次数、异常、活跃度、等等,当然统计这些东西,我们可以利用面向切面AOP技术比如Aspectj,或者在编译期修改字节码比如javassist从而达到我们想统计任何内容的目的。
而Virtualview-Android框架是用来写动态配置客户端的,什么是动态配置?动态配置就是假如我写一个app界面,过了没几天产品让你改成另外的展示,既然领导说改,咱就改白,但是每次改完之后是不是每次都需要重新上架app,用户每次都需要重新更新有木有,当然了你说可以用增量更新、热修复或插件化,虽然这些方案都可以,但是都是存在缺陷的,它们虽然减小了下载的app安装包的大小,但是每一种技术都需要重新合并新旧apk或者重新加载新的apk,不管你合并也好加载也罢,都会面临hook源码,既然hook源码,反射是少不了的,大家都知道反射是耗性能的,当然任何东西有利也有弊,还有就是既然要hook源码了,你必须对系统源码有深入的认识,这里可不止一个版本奥,谷歌对android的系统更新那么快,同样的hook源码放在不同版本系统上可能就会不起作用甚至崩溃,如果谷歌没有改那部分源码,还有可能手机生产商把源码给你改了,这种情况更糟糕,所以说以上三种方案开发成本稍微大许多,所以才有这种动态配置的方案,打个比方我把android的原生控件弄成xml或者json格式的一种,只要出现这个标签就认为它是什么控件,然后自定义自己的解析协议,将xml或json映射成不同的控件,然后显示在界面上,也就相当于渲染,从而给用户带来这种不需要更新app而界面改变的效果,当然你也可以直接用h5,最后都是在服务端替换文件达到效果,只不过h5速度上会慢一些。
好了,扯了这么多,进入今天的主题,先来探讨一下android_viewtracker源码的实现,毕竟是大公司生产的框架,看完肯定会给我们带来一定的收获吧,俗话说的好,借着巨人的肩膀让我们飞的更远。
官方例子中只需要在Applation中配置这么几句就ok了,当然你得在gradle配置文件中依赖这个框架
这里把GlobalsContext一些变量给初始化logOpen
(是否打印log),trackerOpen (是否监听click),trackerExposureOpen (是否监听控件的fling和scroll)
,然后注册activity生命周期的回调,注意这个方法是在api14后出的,所以在写一些自己不熟悉的api的时候最好看一下文档,最好系统兼容性。这个监听如下
很明显,这个方法是额外的为你的布局添加一个父布局TrackerFrameLayout ,添加这个是不是要干一些不为人知的事情
,好接着跟入
看到了什么,dispatchTouchEvent干嘛的?这个不是事件分发的方法,触摸事件在到达你的布局之前,都会先经过TrackerFrameLayout事件分发,首先这里弄了一个手势识别器GestureDetector,手势识别器帮助你判断是点击、长按还是scroll或fling的工具了。ok,继续这个方法首先调用了ClickManager.getInstance().eventAspec,ClickManager看人家的命名也知道接下里是处理点击的事件了,
最后判断触摸是不是ACTION_DOWN方法,然后通过 handleViewClick(activity,
event, commonInfo)获得当前的触摸事件是在哪个view身上触摸的,
这个方法最值得借鉴的就是获得在哪一个view触摸的时候,为它设置setAccessibilityDelegate,我们可以通过为控件设置AccessibilityDelegate监听view的状态变化,比如点击事件、长按事件、焦点变化事件、滑动事件(这里首先你的控件得弄滑动)等等,实现如下
那么这个方法什么时候被通知呢,如果想判断你的触摸是不是点击事件,必须得有两个动作,第一个是手指按下、第二个是手指抬起,只有在手指抬起的时候,你手指抬起时的位置还停留在原来的位置允许的范围误差的话,则系统认为是点击事件然后调用view的performClick方法,如下:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
public void sendAccessibilityEvent(int eventType) {
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}
是不是最终回调了sendAccessibilityEvent方法,也就是布局中的每个控件发生点击事件的时候,那么就会调用DataProcess.processClickParams进行统计,也就是调用如果你重写数据操作类的话,例如将那个控件属于哪一个activity的被点击的信息上传到服务器
手势识别器的回调方法如下
当发生上边的事件的时候,会调用ExposureManager.getInstance().triggerViewCalculate打印哪一个view发生了滑动或fling,具体细节和点击差不多,这里不再探讨。
而Virtualview-Android框架是用来写动态配置客户端的,什么是动态配置?动态配置就是假如我写一个app界面,过了没几天产品让你改成另外的展示,既然领导说改,咱就改白,但是每次改完之后是不是每次都需要重新上架app,用户每次都需要重新更新有木有,当然了你说可以用增量更新、热修复或插件化,虽然这些方案都可以,但是都是存在缺陷的,它们虽然减小了下载的app安装包的大小,但是每一种技术都需要重新合并新旧apk或者重新加载新的apk,不管你合并也好加载也罢,都会面临hook源码,既然hook源码,反射是少不了的,大家都知道反射是耗性能的,当然任何东西有利也有弊,还有就是既然要hook源码了,你必须对系统源码有深入的认识,这里可不止一个版本奥,谷歌对android的系统更新那么快,同样的hook源码放在不同版本系统上可能就会不起作用甚至崩溃,如果谷歌没有改那部分源码,还有可能手机生产商把源码给你改了,这种情况更糟糕,所以说以上三种方案开发成本稍微大许多,所以才有这种动态配置的方案,打个比方我把android的原生控件弄成xml或者json格式的一种,只要出现这个标签就认为它是什么控件,然后自定义自己的解析协议,将xml或json映射成不同的控件,然后显示在界面上,也就相当于渲染,从而给用户带来这种不需要更新app而界面改变的效果,当然你也可以直接用h5,最后都是在服务端替换文件达到效果,只不过h5速度上会慢一些。
好了,扯了这么多,进入今天的主题,先来探讨一下android_viewtracker源码的实现,毕竟是大公司生产的框架,看完肯定会给我们带来一定的收获吧,俗话说的好,借着巨人的肩膀让我们飞的更远。
官方例子中只需要在Applation中配置这么几句就ok了,当然你得在gradle配置文件中依赖这个框架
TrackerManager.getInstance().setCommit(new DemoDataCommitImpl()); GlobalsContext.logOpen = true; TrackerManager.getInstance().init(this, true, true, true);这里的DemoDataCommitImpl是实现了框架的数据处理接口,当需要数据处理时,你是上传到服务端呢还是保存在本地呢还是仅仅只是打印呢,数据的处理完全交给你,本框架概不处理。接下来就是进行一些初始化操作了,如下
public void init(Application application, boolean trackerOpen, boolean trackerExposureOpen, boolean logOpen) { GlobalsContext.mApplication = application; GlobalsContext.trackerOpen = trackerOpen; GlobalsContext.trackerExposureOpen = trackerExposureOpen; GlobalsContext.logOpen = logOpen; if (GlobalsContext.trackerOpen || GlobalsContext.trackerExposureOpen) { mActivityLifecycle = new ActivityLifecycleForTracker(); application.registerActivityLifecycleCallbacks(mActivityLifecycle); } }
这里把GlobalsContext一些变量给初始化logOpen
(是否打印log),trackerOpen (是否监听click),trackerExposureOpen (是否监听控件的fling和scroll)
,然后注册activity生命周期的回调,注意这个方法是在api14后出的,所以在写一些自己不熟悉的api的时候最好看一下文档,最好系统兼容性。这个监听如下
private class ActivityLifecycleForTracker implements Application.ActivityLifecycleCallbacks { @Override public void onActivityCreated(Activity activity, Bundle bundle) { } @Override public void onActivityStarted(Activity activity) { } @Override public void onActivityResumed(Activity activity) { TrackerLog.d("onActivityResumed activity " + activity.toString()); attachTrackerFrameLayout(activity); } @Override public void onActivityPaused(Activity activity) { if (GlobalsContext.trackerExposureOpen) { TrackerLog.d("onActivityPaused activity " + activity.toString()); if (GlobalsContext.batchOpen) { batchReport(); } } } @Override public void onActivityStopped(Activity activity) { } @Override public void onActivityDestroyed(Activity activity) { TrackerLog.d("onActivityDestroyed activity " + activity.toString()); detachTrackerFrameLayout(activity); } @Override public void onActivitySaveInstanceState(Activity activity, Bundle bundle) { } }这里每次我们启动一个Activity的话,那么我们都可以监听到这个activity的生命周期,还记得以前完全退出怎么做的么,每次一个activity启动后将它加到集合中,点完全退出后,遍历集合结束activity,那么这个添加集合的操作完全可以在这个接口里实现,虽然把它写在基类activity中也一样但是显得臃肿还是扩展出去比较好,单一职责吗。在这个实现接口中可以看出在每一个acitity的onActivityResumed回调方法中做了attachTrackerFrameLayout这个操作,ok进入
public void attachTrackerFrameLayout(Activity activity) { // this is a problem: several activity exist in the TabActivity if (activity == null || activity instanceof TabActivity) { return; } // exist android.R.id.content not found crash try { ViewGroup container = (ViewGroup) activity.findViewById(android.R.id.content); if (container == null) { return; } if (container.getChildCount() > 0) { View root = container.getChildAt(0); if (root instanceof TrackerFrameLayout) { TrackerLog.d("no attachTrackerFrameLayout " + activity.toString()); } else { TrackerFrameLayout trackerFrameLayout = new TrackerFrameLayout(activity); while (container.getChildCount() > 0) { View view = container.getChildAt(0); container.removeViewAt(0); trackerFrameLayout.addView(view, view.getLayoutParams()); } container.addView(trackerFrameLayout, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); } } } catch (Exception e) { TrackerLog.e(e.toString()); }
很明显,这个方法是额外的为你的布局添加一个父布局TrackerFrameLayout ,添加这个是不是要干一些不为人知的事情
,好接着跟入
public boolean dispatchTouchEvent(MotionEvent ev) { mGestureDetector.onTouchEvent(ev); if (getContext() != null && getContext() instanceof Activity) { // trigger the click event ClickManager.getInstance().eventAspect((Activity) getContext(), ev, commonInfo); } switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: mOriX = ev.getX(); mOriY = ev.getY(); break; case MotionEvent.ACTION_MOVE: if ((Math.abs(ev.getX() - mOriX) > CLICK_LIMIT) || (Math.abs(ev.getY() - mOriY) > CLICK_LIMIT)) { // Scene 1: Scroll beginning long time = System.currentTimeMillis(); TrackerLog.v("dispatchTouchEvent triggerViewCalculate begin "); ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap); TrackerLog.v("dispatchTouchEvent triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time)); } else { TrackerLog.d("dispatchTouchEvent ACTION_MOVE but not in click limit"); } break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(ev); }
看到了什么,dispatchTouchEvent干嘛的?这个不是事件分发的方法,触摸事件在到达你的布局之前,都会先经过TrackerFrameLayout事件分发,首先这里弄了一个手势识别器GestureDetector,手势识别器帮助你判断是点击、长按还是scroll或fling的工具了。ok,继续这个方法首先调用了ClickManager.getInstance().eventAspec,ClickManager看人家的命名也知道接下里是处理点击的事件了,
public void eventAspect(Activity activity, MotionEvent event, HashMap<String, Object> commonInfo) { GlobalsContext.start = System.currentTimeMillis(); if (!GlobalsContext.trackerOpen) { return; } if (activity == null) { return; } // sample not hit if (isSampleHit == null) { //产生一个随机数做比较 isSampleHit = CommonHelper.isSamplingHit(GlobalsContext.sampling); } //如果产生的随机数小于100则直接返回,此次点击不予处理 if (!isSampleHit) { TrackerLog.d("click isSampleHit is false"); return; } try { if (event.getAction() == MotionEvent.ACTION_DOWN) { handleViewClick(activity, event, commonInfo); } } catch (Throwable th) { TrackerLog.e(th.getMessage()); }这个方法比较有意思的一点是,随机数产生比较,如果当前产生的随机数小于你设置的数的话,点击方法继续,否则不予处理,这个方法的好处就是我们可以动态增加它需要方法实现的概率,随机数实现如下
ublic static boolean isSamplingHit(int sample) { Random rand = new Random(); int samplingSeed = rand.nextInt(100); if (samplingSeed >= sample) { return false; } else { return true; } }
最后判断触摸是不是ACTION_DOWN方法,然后通过 handleViewClick(activity,
event, commonInfo)获得当前的触摸事件是在哪个view身上触摸的,
private void handleViewClick(Activity activity, MotionEvent event, HashMap<String, Object> commonInfo) { View view = activity.getWindow().getDecorView(); View tagView = null; //得到被点击的view的范围 View clickView = getClickView(view, event, tagView); //给当前点击的view设置状态监听器 if (clickView != null) { if (mDelegate != null) { mDelegate.setCommonInfo(commonInfo); } clickView.setAccessibilityDelegate(mDelegate); } }
这个方法最值得借鉴的就是获得在哪一个view触摸的时候,为它设置setAccessibilityDelegate,我们可以通过为控件设置AccessibilityDelegate监听view的状态变化,比如点击事件、长按事件、焦点变化事件、滑动事件(这里首先你的控件得弄滑动)等等,实现如下
public void sendAccessibilityEvent(View clickView, int eventType) { TrackerLog.d("eventType: " + eventType); if (eventType == AccessibilityEvent.TYPE_VIEW_CLICKED) { TrackerLog.d("click: " + clickView); DataProcess.processClickParams(commonInfo, clickView); } super.sendAccessibilityEvent(clickView, eventType); }
那么这个方法什么时候被通知呢,如果想判断你的触摸是不是点击事件,必须得有两个动作,第一个是手指按下、第二个是手指抬起,只有在手指抬起的时候,你手指抬起时的位置还停留在原来的位置允许的范围误差的话,则系统认为是点击事件然后调用view的performClick方法,如下:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
public void sendAccessibilityEvent(int eventType) {
if (mAccessibilityDelegate != null) {
mAccessibilityDelegate.sendAccessibilityEvent(this, eventType);
} else {
sendAccessibilityEventInternal(eventType);
}
}
是不是最终回调了sendAccessibilityEvent方法,也就是布局中的每个控件发生点击事件的时候,那么就会调用DataProcess.processClickParams进行统计,也就是调用如果你重写数据操作类的话,例如将那个控件属于哪一个activity的被点击的信息上传到服务器
public void commitClickEvent(HashMap<String, Object> commonInfo, String viewName, HashMap<String, Object> viewData) { if (TextUtils.isEmpty(viewName)) { TrackerLog.d("commitClickEvent viewName is null"); return; } TrackerLog.d("viewName=" + viewName); HashMap<String, String> argsInfo = new HashMap<String, String>(); // add the common info if (commonInfo != null && !commonInfo.isEmpty()) { argsInfo.putAll(TrackerUtil.getHashMap(commonInfo)); } if (argsInfo.containsKey(TrackerConstants.PAGE_NAME)) { argsInfo.remove(TrackerConstants.PAGE_NAME); } // add the special info if (viewData != null && !viewData.isEmpty()) { argsInfo.putAll(TrackerUtil.getHashMap(viewData)); } if (GlobalsContext.trackerOpen) { if (!argsInfo.isEmpty()) { TrackerUtil.commitCtrlEvent(viewName, argsInfo); } else { TrackerUtil.commitCtrlEvent(viewName, null); } } }这里只是对数据进行打印,并没有提供保存操作,看到这我还是比较喜欢用AspectJ进行切面化统计,接下里是滑动和fling统计
case MotionEvent.ACTION_MOVE: if ((Math.abs(ev.getX() - mOriX) > CLICK_LIMIT) || (Math.abs(ev.getY() - mOriY) > CLICK_LIMIT)) { // Scene 1: Scroll beginning long time = System.currentTimeMillis(); TrackerLog.v("dispatchTouchEvent triggerViewCalculate begin "); ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, this, commonInfo, lastVisibleViewMap); TrackerLog.v("dispatchTouchEvent triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time)); } else { TrackerLog.d("dispatchTouchEvent ACTION_MOVE but not in click limit"); }
手势识别器的回调方法如下
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) { long time = System.currentTimeMillis(); TrackerLog.v("onFling triggerViewCalculate begin"); this.postDelayed(new Runnable() { @Override public void run() { ExposureManager.getInstance().triggerViewCalculate(TrackerInternalConstants.TRIGGER_VIEW_CHANGED, TrackerFrameLayout.this, commonInfo, lastVisibleViewMap); } }, 1000); TrackerLog.v("onFling triggerViewCalculate end costTime=" + (System.currentTimeMillis() - time)); return false; }
当发生上边的事件的时候,会调用ExposureManager.getInstance().triggerViewCalculate打印哪一个view发生了滑动或fling,具体细节和点击差不多,这里不再探讨。
相关文章推荐
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏ViewGroup事件派发机制详解与源码分析
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
- Android 读书笔记:View的事件分发机制 源码详解 ------《Android开发艺术探索》
- Android View 事件分发机制源码详解(ViewGroup篇)
- Android事件处理(二)——View的dispatchTouchEvent 函数源码详解
- Android触摸屏事件派发机制详解与源码分析一(View篇)onTouch,onClick,ontouchevent
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- Android View 事件分发机制源码详解(ViewGroup篇)
- Android事件处理(三)——View的onTouchEvent 函数源码详解
- Android触摸屏事件派发机制详解与源码分析一(View篇)
- android WebView详解,常见漏洞详解和安全源码(上)
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
- Android事件分发详解(三)——ViewGroup的dispatchTouchEvent()源码学习
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)
- Android触摸事件分发处理机制详解与源码分析一(View篇)
- Android事件分发详解(三)——ViewGroup的dispatchTouchEvent()源码学习
- Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)