前言

在起初的手动埋点的时候,每次版本大更新,很多埋点都要进行修改,删除。这个时候之前嵌在源码里面的一行行埋点代码要进行修改,删除。删了又找,找了又改,很麻烦。如果遇到有代码洁癖的,“产品你竟然要在我代码里加这么多埋点,很影响我代码美观,晓得不!”心里敢想不敢说。于是我就想能不能利用字节码插桩把埋点信息插进去呢...

在去年突发奇想,想利用Gradle插件,Transform+ASM实现字节码插桩,将需要手动埋点的地方通过操作字节码进行埋点。

先是在网上搜索相关无痕埋点框架,搜到不少,都并不符合我的预期,我预期是对源代码不要进行任何的手动操作。而很多框架是利用了注解,需要在埋点的地方标记注解,emmm.....似乎都是这么解决了。

还有一种无痕埋点呢,像didi的无痕埋点,是记录所有View的id。框架太复杂了。。。。望而却步。

就像这样一个埋点,记录新手用户点击领取新用户礼包这样一个事件信息,"new_user_receive_gift" 就这么一个信息。我怎么才能在不修改源代码的情况下,把这个加入到对应的领取按钮触发的方法里面呢。

我起初想到的是用AspectJ,但是对于使用者来说,还是比较麻烦的,有一定的学习成本,如果不使用注解进行aop的话,那操作起来是相当麻烦的。

于是我就放弃了AspectJ,转而了解了一下Transform和ASM,我发现这个可行。

我设想的实际操作流程:

  1. 我通过一个配置文件将埋点信息记录。(实际使用)
  2. 编写一个接收埋点信息事件的接收类,将接收到的埋点信息通过埋点统计框架上传。(实际使用)
  3. 通过Transform执行的时候读取埋点信息。(框架封装)
  4. 利用ASM将埋点字节码写入原文件。(框架封装)

使用文件配置进行无痕埋点,在添加埋点的时候,我就可以不手动修改源代码了,只需要在配置文件中增加一个埋点信息就行了。

我的Slotting无痕埋点完成了

实在是不知道起什么名字好了。随便搜了个 “开槽(Slotting)”。给代码开个槽吧。

经过一周的学习和一周的代码编写大约两周的时间。从对Transform,ASM,Gradle Plugin一窍不通到一壳要秃。

基于我的无痕埋点设想首先我定义了一个埋点信息接收接口:

interface Slotting {
       /**
        * 接收一个数组消息
        * - send("msg") ; send("abc",19,this.name)
        */
       fun send(vararg msg: Any?)
       /**
        * 接收一个Map消息
        * - val map = mutableMap<String,Any?>()
        * - map .put(key1,"value1")
        * - map.put(key2 ,this.value2 )
        * - send(map)
        */
       fun send(map: Map<String, Any?>)
}

说是接收器,其实就是直接调用这个类方法。

1.实现Slotting.kt接口

kotlin:
 object SimpleSlotting : Slotting{
     override fun send(vararg msg: Any?) {
     }


     override fun send(map: Map<String, Any?>) {
     }
 }
java
public class JavaSlotting implements Slotting {
 
    public static JavaSlotting INSTANCE = new JavaSlotting();


    @Override
    public void send(@NonNull Object... objects) {
 
    }


    @Override
    public void send(@NonNull Map<String, ?> map) {


    }
}

注意:

Kotlin中使用object实现接口。

Java中实现接口之后需要再创建一个静态对象INSTANCE便于调用。这个调用不需要手动调用。是插桩框架生成字节码调用。

2.创建埋点配置文件

在app目录下创建slotting.json文件.

app/
 |-libs/
 |-src/
 |-slotting.json <----

可以放在其他目录中:

app/
 |-libs/
 |-src/
 |-simpleDir/
     |-slotting.json <----

3.添加脚本配置

在app的build.gradle中引入插件,并修改插件配置信息。

plugins {
     id 'com.dboy.slotting'
}


slotting{
     //配置文件名,要包含扩展名.json , 默认名称:slotting.json
     fileName "slotting.json"
     //配置文件路径, 默认位置是app模块根目录
     //simple: filePath = "simpleDir/"
     filePath ""
     //消息接收实现类,实现接口 [com.dboy.slotting.api.Slotting]
     implementedClass "com.example.SimpleSlotting"
}

4.编写slotting.json文件

这个文件是json格式的。

[
    {
        "classPath": "com.xxx.MainActivity",
        "entryPoints": [
            {
                "methodName": "simpleEventMethod",
                "event": "event,from,${name}"
            },
            {
                "methodName": "simpleEventMapMethod",
                "eventMap": {
                    "msg": "is msg 1",
                    "msg2": "${this.msg2}"
                }
            }
        ]
    },
    {
        "classPath": "com.xxx.MainActivity"
    }
]

Json字段说明:

此json根节点是一个List表,实体对象内容为:

  • classPath : 指明需要埋点的class文件全量名称,排除.class后缀。
  • entryPonts: 切入点/埋点位置。这是个list列表,内部包含了当前classPath所有需要触发埋点的方法信息。
  • methodName: 需要埋点的方法名字。
  • event: 埋点触发事件,可以是单个字符串,也可以多个埋点事件,通过英文 “,” 逗号进行分割。接收此事件方法fun send(vararg msg: Any?).
  • eventMap: 具有Key->value映射的事件。接收此事件方法fun send(map: Map<String, Any?>)
  • isFirstLine : 这个是一个boolean数据表明这个埋点事件是插入method第一行,还是methodreturn时的位置。默认是false

event事件和eventMap事件的Value值可以使用占位符来获取全局变量和局部变量。

使用${...}来进行占位标识,${this.xxx}表示获取全局变量xxx。${xxx}表示获取方法的局部变量xxx

例如:

event :"全局变量:,${this.globalName},局部变量:,${localName}" 


 eventMap :{
     "全局变量":"${this.globalName}",
     "方法局部变量":"${localName}"
 }

eventeventMap两种类型的事件只取其一,如果两者都有数据,优先使用event数据

配置完成之后即可进行项目构建(Build)。

注意:修改class文件不需要clean项目,如果修改了slotting{}脚本配置,或者修改了slotting.json文件,需要clean整个项目重新build or rebuild。由于Transform的增量编译,不会通过slotting.json文件的变化而修改对应class,所以当配置修改后,检测字节码的时候原class没有变更是不会二次插桩修改的。

字节码插桩生成演示

编写自己的事件接收文件SimpleSlotting.kt

object SimpleSlotting : Slotting {


     override fun send(vararg msg: Any?) {
         //使用统计平台对msg进行处理发送例如:Umeng
         val str = StringBuilder()
         msg.forEach {
             if (it == null) {
                 str.append("null")
             } else {
                 str.append(it.toString())
             }
         }
         MobclickAgent.onEventObject(Utils.getApp(), str)
     }


     override fun send(map: Map<String, Any?>) {
         //使用统计平台对map进行处理发送例如:Umeng
         val event = map["event"]
         MobclickAgent.onEventObject(Utils.getApp(), event, map)
     }
}

原始SimpleClass.kt

class SimpleClass {
 
     fun testEvent1(){
 
     }
 
     fun testEvent2(){
 
     }


     fun testEvent3(userName: String) {
 
     }
 
}

slotting.json配置文件

[
    {
        "classPath": "com.simple.SimpleClass",
        "entryPonts": [
            {
                "methodName": "testEvent1",
                "event": "testEvent1"
            },
            {
                "methodName": "testEvent2",
                "event": "testEvent2,two"
            },
            {
                "methodName": "testEvent3",
                "eventMap": {
                    "event": "testEvet3",
                    "name": "${userName}"
                }
            }
        ]
    }
]

字节码插桩后的SimpleClass.kt

class SimpleClass {
     fun testEvent1(){
         SimpleSlotting.send("testEvent1")
     }


     fun testEvent2(){
         SimpleSlotting.send("testEvent2","two")
     }


     fun testEvent3(userName: String) {
         val map = HashMap<String,Any>()
         map["event"] = "testEvent3"
         map["name"] = userName
         SimpleSlotting.send(map)
     }
}

当你需要在最后一行插入代码的时候需要注意:

当埋点需要在方法最后一行插入的时候,所有return的位置都有可能是方法结束时的最后一行。所以所有return位置都会被插入同样的埋点信息。

如果你携带了局部变量。当局部变量不在可索引范围内的时候,埋点事件框架不会将无法索引的局部变量添加到事件中。

例如埋点:上传检查后的ab的值

fun check(){
           var a = ""
           //...
           if(a==null){
          //...在这里只能访问到变量a,变量b无法访问 , 最后会插入Slotting.send(a)
               return
           }
           var b = ...
           if(b==null){
             //....在这里,a和b变量都可以被访问到,最后会插入Slotting.send(a,b)
             return
           }
         //...Slotting.send(a,b)
       }

上面的做法显然有点问题,数据检查和数据的使用应该分开,这样就更有利于代码插装,和业务上的明细。

不如模拟一个正经的场景:用户登录。

埋点描述:用户登录失败,上传失败原因user_login_error_xxx(xxx是哪一步错了),成功上传user_login_success

//不对这个方法插码
    fun checkUserInfo(){
        //检查用户名是否输入正确
        var name = ...
        if(name==null){
            showLoginErrorToast("userName")
            return
        }
        //检查密码格式是否输入正确
        var password = ...
        if(password == null){
            showLoginErrorToast("userPassword")
            return
        }
        //提交信息
        commit(name,password)
    }
    //对这个方法插码
    fun showLoginErrorToast(errorMsg:String){
        toast(errorMsg)
        //...json配置这个方法发送错误 event:"user_login_error_,${errorMsg}"
        //这里将会插入Slotting.send("user_login_error_",errorMsg)
        //在接收处做拼接,上传事件
    }
   //对这个方法插码
    fun commit(name:Any,paddword:Any){
         //做点什么...
         //...
        //...在json配置这个方法发送登录成功event:"user_login_success"
    }

向这样的,在编写代码的时候,尽量做到,方法的职责单一。

当然如果埋点比较简单。你可以直接配置埋点在方法的第一行插入。

之后的改进和计划

虽然告别了手动修改源码进行埋点,但是编辑这个slotting.json文件也是让人很棘手的事。

我计划着在之后学习一下编写IDEA插件。实现一个图形化修改slotting.json的工具。这样埋点就更方便了。

当找到需要埋点的位置,在对应方法上鼠标右键,在菜单中增加一个slotting code选项。点击之后读取slotting.json配置

如果配置了信息,将会显示配置信息内容,并且代码左侧栏会有一个小tag标记这个方法记录了埋点。点击小tag跳转到对应slotting.json配置信息的位置。

android recyclerview item 埋点曝光_android

android recyclerview item 埋点曝光_java_02

对于现阶段实现的功能,有一些埋点业务场景还不匹配。缺少对于逻辑埋点的配置。之后会想一下如何针对逻辑埋点进行设计。

还有就是对通用埋点,所有类似的方法或者父类方法,android sdk和第三方sdk中进行埋点的优化适配。

总的来说,我是比较喜欢使用我这种,通过一个文件进行无痕埋点。主要是对源代码0入侵,后期如果不需要这个统计平台了,也方便移除埋点。不用再一个一个class文件中去删除了。

添加依赖:

在第一版1.0.0完成之后,对插件的依赖做了调整。重新封装了Transform。注释说明和Log也做了调整,发布了1.0.1的优化。

project 的 build.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        //添加插件
        classpath 'io.github.dboy233:slotting-plugin:1.0.1'
    }
}
有的项目可能是在setting.gradle中设置
dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}

以前版本还是在allprojects

allprojects {
    repositories {
        mavenCentral()
    }
}
app模块下的build.gradle
plugins {
  id 'com.dboy.slotting'
}


dependencies {
   //引入Api
   implementation 'io.github.dboy233:slotting-api:1.0.1'
}


作者:年小个大