想像一下,你的app有没有这种的埋点,同时有activity A、B、C、D,需要分别统计A->D(A跳转到D),B->D、C->D 的点,这种情况下,如果用普通实现可能就是在D 页面通过intent取出来源是A或B或C 来做埋点。这种两个页面以上组合起来称为一条路径 track. 还有更长的,如A->B,然后页面B的某按钮点击,再进页面C …… 等等,如果用intent传值方式需要把来源一路往下传,非常麻烦,而且让其他人在看代码的时候,往往不懂这个值是干嘛的,代码不清晰,所以 tracker 框架就是解决这类问题的。
如页面跳转表示为
Track.from(A.class).to(B.class).subscribe(new OnSubscribe<Intent>() {
@Override
public void call(Intent intent) {
Log.d(TAG, "A->B " + intent + " t=" + Thread.currentThread());
}
});
当发生startActivity…(A.this,B.class) 时,执行call 回调。这样是不是很清晰。详细操作符见api 文档。
一些术语:
- 操作点:是一个基础操作,如 activity跳转,view 点击,activity 生命周期等
- 操作符:操作符是操作点在tracker 框架对应的方法,如 操作点:view.setOnclickliener 对应操作符 viewClick(R.id.button) , startActivity 页面跳转操作 对应操作符为 to(),完整操作符见 api 文档
- 起始事件:一条路径的第一个事件。
- 终点事件:一条路径的最后一个事件 (subscribe 前的操作)
如:Track.from(A.class).to(B.class).to(C.class).viewClick(R.id.button2).subscribe(xx)
起始事件为:Track.from(A.class).to(B.class)
终点事件为:.viewClick(R.id.button2)
- 路径:一条路径由多个操作点组成,并且操作点是有序的,只有当第一个操作触发时,第二个操作才有可能被触发,以此类推。
- 点亮路径:当路径的第一个点被触发时,称为点亮的路径。当路径的点全部点亮时,回调给 订阅者。
- 熄灭路径:把该路径恢复当默认状态,也就是该路径已点亮的点都恢复成默认。
- 取消订阅:不再订阅此路径。
一些规定:
- 当路径的所有操作都点亮时,才会触发回调给订阅者。
- 如果该路径已全部点亮,此时再触发终点事件,回调会再次的执行。如:
Track.from(A.class).to(B.class).to(C.class).viewClick(R.id.button2).subscribe(xx);
当A->B、B->C 时,点击 button2,触发回调,再次点击,会再次触发。因为此时的点击依然是有效的。 - 上下文相关性:所有事件都是有上下文相关性的。每一个操作都依赖于前一个操作。
其中to() 会把后面的操作改为to() 里的上下文,而activityFinish()、activityOnDestoryed() 会把后面的操作切回到上一个上下文。
to 如:Track.from(A.class).to(B.class).to(C.class).viewClick(R.id.button2).subscribe(xx);
表示当A->B, 然后B->C,然后C中的R.id.button2 被点击,回调触发。并且在B->C 的操作前,不关心在B 页面做的任何操作,如点击、B跳D,D返回B。都不影响,只要最终是B->C,则会点亮。
activityOnDestoryed 如:
Track.from(A.class).to(B.class)
.viewClick(R.id.button2).activityOnDestroyed()
.viewClick(R.id.button2).subscribe(xx);
表示A->B,然后B页面的R.id.button2 被点击,然后B 页面关闭,然后A 页面的 R.id.button2 被点击,则回调触发。
- 所有回调与操作都是在子线程。回调中的参数 为 事件发生时的参数,如viewClick 的参数是被点击时的view 。to 的参数是跳转时的intent,你可以通过此参数获取一些额外数据。
实现原理:
想要实现无侵入式监听某些操作,必须要用到AOP,AOP 原理是指在编译时,对所有class、jar 进行处理,对需要侵入的代码切入点插入代码。这里AOP 用到的是Aspectjx。
- 首先编译期,对所有需要插入的点插入track 的处理代码。见 AopAspect.java ,如点击事件,当setOnClickListener 时,代理OnClickLiener 对象,当点击事件当生时,切到 tracker 线程处理逻辑。
- 应用启动时,首先先订阅需要的事件。每一个操作符,都转换为一个Node 对象,在数据结构的选择上,如果是普通使用,如:
Track.from(AActivity.class).to(BActivity.class).to(CActivity.class).viewClick(R.id.button).subscribe(xx)
这种一条链式的调用,数据结构自然会想到用到链表或者list的方式,但是我们希望能像rxjava 一样,对象可以重复的用,如这种用法:
Track<?> track=Track.from(AActivity.class).to(BActivity.class);
track.to(CActivity.class).subscribe(new OnSubscribe<Intent>() {
@Override
public void call(Intent intent) {
//表示A->B,然后B—>C
}
});
track.viewClick(R.id.button).subscribe(new OnSubscribe<View>() {
@Override
public void call(View view) {
//表示:A->B,然后B中R.id.button被点击。
}
});
这两条路径之间无关系,但是前提都是需要A->B 这个起点,为了实现这种效果,倒过来,其实就是一颗树,所以用了树的结构来的保存所有注册结点。
然后向TrackManager 注册,TM 中管理所有路径。
- 运行时,当点击事件当生时,切到 tracker 线程处理逻辑。首先对数据进行过滤,过滤无ID 的view,或者未向track 注册的的viewId,这些事件都直接忽略。所的操作符都类似,每个操作符都有自己比较的逻辑,核心查找点亮见 findTrack。
结尾
性能与兼容
由于AOP是在编译时插入代码,而运行到插入代码时,会切到内部Track-Thread进行计算,所以不会对主线程有任何影响。没有保存对象,所以也不会造成内存泄露问题。由于只你的apk代码有关,和android 版本无关,所以也不存在版本不兼容的问题,可放心使用。
Trakcer框架已在天天拍车内部几款app 使用,想着大家可能也会有这种复杂的埋点需求,现开源出来。
Tracker 虽然是在埋点需求的背景下产生的,但还可以用来做其它的事,只要是想监听用户的行为,动作,而不想侵入业务代码。都可以用此框架提供一种思路。