无埋点——检瓜子的探索与实践(一)

业务背景

由于业务发展,埋点统计的需求越来越多,所以无埋点一事在组内提上日程,经过一番探索与实践,有了一点初步成果。

埋点方式

  • 手动埋点
  • 无痕埋点
  • 配置埋点

插件设计

对于埋点方式在此不做过多叙述,主要讲下无埋点方案探索。
先看下整个无埋点方案的流程:
无埋点——检瓜子的探索与实践(一)

从这个流程中可以看出,我们的插入点就在于build之后,生成apk之前,再此期间通过一些方式方法进行代码注入。这里我们想了2个方案:

  • hook dx.jar
  • Transform API

这里我们选择了Transform API,它是gradle提供的API,用起来也比较方便。gradle版本要在1.5.0以上。
选定了方案,在考虑如何设计gradle插件,我们来看下,这个插件的主要作用:
无埋点——检瓜子的探索与实践(一)

这里的Slark Plugin就是我们的插件名字,如图可以得知,我们的主要功能就是寻找OnClick方法,在编译后,我们的Onclick时间都会变成一个内部类,不管你用的是lamda表达式,还是databinding,还是普通的点击事件,编译后的class文件是一样的,所以这里可以不用考虑点击事件的实现方式,通过这个流程,我们可以得知,现在的任务就是选定hook方式,也就是AOP编程的框架选择问题。同样有几种方案,最终我们选定了ASM。

ASM

asm与其他类库相比较,更小更快,有一个简单的对比:

Name First Time Later times
Javassist 257 5.2
ASM 62.4 1.1

但是有一点值得一提的是,ASM学习成本比较高,最好多查一查资料,看一下官方文档,官方文档

技术点

ViewId

对于埋点,我们首要解决的就是ViewId的问题,首先这个id应该具备以下特征:

  1. 唯一
  2. 同一个view,不同设备生成的ID应该一致
  3. 同一个view,不同编译不同时间,生成的ID应该不变

于是很容易得到一个结论,系统设置的id在编译后是变化的,不可用的。。。
所以我们寻找了一种id的解决方案,就是用页面和viewtree来标记id,保证其唯一性:viewId = pageName+viewtreePath
无埋点——检瓜子的探索与实践(一)
比如上图的ChildView1,如果这个页面是MainActivity,那么它的表示方式应该就是MainActivity#RootView/LinearLayout/ChildView1,但是这个并不能解决动态添加view的问题,比如:
无埋点——检瓜子的探索与实践(一)

如果我在RelativeLayout后面加了一个FrameLayout或者LinearLayout,那么这个方式就不行了,网上有一种方案是增加index,比如插入FrameLayout前后,增加view的index,这样可以保持统一,例:增加前,ChildView1=MainActivity#RootView0/LinearLayout0/ChildView10,增加后,ChildView1
的路径其实是没有任何变化的,但是如果ChildView在第三个LinearLayout上,那么在第三个LinearLayout之前插入一个LinearLayout,就会出现不统一的问题,那么这种情况又该怎么解决?

代系概念的引入

假设有这样一种情况,有一个界面,上面有两个LinearLayout,然后有一个按钮,点击的时候会在两个LinearLayout中间插入一个新的LinearLayout,那么如何让viewId在插入前后能够对应上?
我们提出了这样一个概念,代系,描述如下:
开始的两个LinearLayout路径分别为:
Page#RootView/LinearLayout[1]
Page#RootView/LinearLayout[2]
在点击按钮插入后,路径变为:
Page#RootView/LinearLayout[1-1]
Page#RootView/LinearLayout[2-0]
Page#RootView/LinearLayout[3-2]
那么观察下,插入后路径变为了2位,其中第一位表示当前的顺序,第二位如果是0表示这个在之前是不存在的,如果是非0则表示它上一代时候的位置信息。这样每次发现有新的未标记的view需要埋点的时候,我们就可以增加一代,用来给新生view增加唯一id,同时可以让其他view与原来之前的path对应。

页面

关于页面调用的跟踪,考虑以下几个方面:

  • 页面的种类
  • 页面切换调用的方法
  • 页面调用方法未复写的处理

    对于第一点,首先页面种类我们仔细想下其实就两类:Activity和Fragment。
    对于第二点,其实就是监听它在跳转中可能调用的方法。
    对于第三点,这里不得不提用ASM的一个好处,就是可以随意hook,而且可以hook未复写的方法。

    Activity

    对于Activity来说,监控其生命周期方法就可以解决大部分问题,所以这个可以用ActivityLifecycleCallbacks,注册在Application中,这样基本上就可以了。

    Fragment

    对于Fragment,相对麻烦一点,因为没有这样一个方法能让我们省时省力的去监听它的跳转,所以我们对于各种情况下的Fragment跳转进行了一个总结,一共有四个方法需要我们监听:

  • onResume

  • onPause
  • onHiddenChanged
  • setUserVisibleHint

实现了对这四个方法的监听,基本上就覆盖了Fragment所有的进入与退出。

整个页面监控的流程大致如下:
无埋点——检瓜子的探索与实践(一)

目前是对第一版探索与实践的总结,如果有人想了解ASM相关使用,可以留言。