想像一下,你的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 文档。

一些术语:

  1. 操作点:是一个基础操作,如 activity跳转,view 点击,activity 生命周期等
  2. 操作符:操作符是操作点在tracker 框架对应的方法,如 操作点:view.setOnclickliener 对应操作符 viewClick(R.id.button) , startActivity 页面跳转操作 对应操作符为 to(),完整操作符见 api 文档
  3. 起始事件:一条路径的第一个事件。
  4. 终点事件:一条路径的最后一个事件 (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)
  5. 路径:一条路径由多个操作点组成,并且操作点是有序的,只有当第一个操作触发时,第二个操作才有可能被触发,以此类推。
  6. 点亮路径:当路径的第一个点被触发时,称为点亮的路径。当路径的点全部点亮时,回调给 订阅者。
  7. 熄灭路径:把该路径恢复当默认状态,也就是该路径已点亮的点都恢复成默认。
  8. 取消订阅:不再订阅此路径。

一些规定:

  1. 当路径的所有操作都点亮时,才会触发回调给订阅者。
  2. 如果该路径已全部点亮,此时再触发终点事件,回调会再次的执行。如:
    Track.from(A.class).to(B.class).to(C.class).viewClick(R.id.button2).subscribe(xx); 当A->B、B->C 时,点击 button2,触发回调,再次点击,会再次触发。因为此时的点击依然是有效的。
  3. 上下文相关性:所有事件都是有上下文相关性的。每一个操作都依赖于前一个操作。
    其中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 被点击,则回调触发。

  1. 所有回调与操作都是在子线程。回调中的参数 为 事件发生时的参数,如viewClick 的参数是被点击时的view 。to 的参数是跳转时的intent,你可以通过此参数获取一些额外数据。

实现原理:

想要实现无侵入式监听某些操作,必须要用到AOP,AOP 原理是指在编译时,对所有class、jar 进行处理,对需要侵入的代码切入点插入代码。这里AOP 用到的是Aspectjx

  1. 首先编译期,对所有需要插入的点插入track 的处理代码。见 AopAspect.java ,如点击事件,当setOnClickListener 时,代理OnClickLiener 对象,当点击事件当生时,切到 tracker 线程处理逻辑。
  2. 应用启动时,首先先订阅需要的事件。每一个操作符,都转换为一个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 中管理所有路径。

  1. 运行时,当点击事件当生时,切到 tracker 线程处理逻辑。首先对数据进行过滤,过滤无ID 的view,或者未向track 注册的的viewId,这些事件都直接忽略。所的操作符都类似,每个操作符都有自己比较的逻辑,核心查找点亮见 findTrack。

结尾

性能与兼容

由于AOP是在编译时插入代码,而运行到插入代码时,会切到内部Track-Thread进行计算,所以不会对主线程有任何影响。没有保存对象,所以也不会造成内存泄露问题。由于只你的apk代码有关,和android 版本无关,所以也不存在版本不兼容的问题,可放心使用。
Trakcer框架已在天天拍车内部几款app 使用,想着大家可能也会有这种复杂的埋点需求,现开源出来。

Tracker 虽然是在埋点需求的背景下产生的,但还可以用来做其它的事,只要是想监听用户的行为,动作,而不想侵入业务代码。都可以用此框架提供一种思路。

github