您的位置:首页 > 移动开发 > Android开发

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配置文件中依赖这个框架

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 alibaba