前言

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

在去年突发奇想,想利用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();@Overridepublic void send(@NonNull Object... objects) {}@Overridepublic 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.jsonfileName "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进行处理发送例如:Umengval 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进行处理发送例如:Umengval 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"] = userNameSimpleSlotting.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 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 {//引入Apiimplementation 'io.github.dboy233:slotting-api:1.0.1'
}

结尾

代码已经开源了。在Dboy233/Slotting (github.com)上。我注释写的很全了。有兴趣的话,可以看看,多多指点。

如果你也有这样的无痕埋点方案,一起讨论。

如果你有更好的无痕埋点思路,一起卷。

关注我获取更多知识或者投稿

项目地址:https://github.com/Dboy233/Slotting

作者:年小个大
链接:https://juejin.cn/post/7028405590984491022

手动埋点转无痕埋点,如何做到代码“零”入侵相关推荐

  1. ios无痕埋点_iOS无痕埋点方案分享探究

    原标题:iOS无痕埋点方案分享探究 作者丨SandyLoo https://www.jianshu.com/p/b8a67c4acfb3 前言 当前互联网行业的竞争已经是非常激烈了, "功能 ...

  2. iOS 最优无痕埋点方案

    iOS 最优无痕埋点方案 在移动互联网时代,对于每个公司.企业来说,用户的行为数据非常重要.重要到什么程度,用户在这个页面停留多久.点击了什么按钮.浏览了什么内容.什么手机.什么网络环境.App什么版 ...

  3. 无痕埋点的设计与实现

    在移动互联网时代,对于每个公司.企业来说,用户的行为数据非常重要.重要到什么程度,用户在这个页面停留多久.点击了什么按钮.浏览了什么内容.什么手机.什么网络环境.App什么版本等都需要清清楚楚.一些大 ...

  4. iOS-史上最强、最详细无痕埋点方案

    在移动互联网时代,对于每个公司.企业来说,用户的行为数据非常重要.重要到什么程度,用户在这个页面停留多久.点击了什么按钮.浏览了什么内容.什么手机.什么网络环境.App什么版本等都需要清清楚楚.一些大 ...

  5. 美团点评前端无痕埋点实践

    构建一个数据平台,大体上包括数据采集.数据上报.数据存储.数据计算以及数据可视化展示等几个重要的环节.其中,数据采集与上报是整个流程中重要的一环,只有确保前端数据生产的全面.准确.及时,最终产生的数据 ...

  6. ios无痕埋点_移动端无痕埋点实践详解(二)

    0x01 前言 在移动端无痕埋点实践详解(一)这篇文章大致总结了移动端无痕埋点的基本原理.主要介绍了什么是无痕埋点,无痕埋点的基础数据流程以及在Android系统上总体思路.这篇文章着重总结下无痕埋点 ...

  7. ios无痕埋点_iOS可视化埋点方案

    前言 随着公司业务的发展,数据的重要性日益体现出来. 数据埋点的准确和全面性显得尤为重要. 通过精准和详细的数据,后面的分析才有意义.随着业务的不断变化,动态化埋点也越来越重要. 三大埋点方式 为了解 ...

  8. iOS 打点上报、无痕埋点

    最近研习了美团等大厂的一些埋点方案. 还要感谢大神<xuhaoranLeo>的指点.(既然大神没空写博客.但我可以代劳哈). 本文的宗旨是尽量全面.精简.满足我能想到尽量多的埋点需求. 主 ...

  9. 无痕埋点在Android中的实现

    无痕埋点在Android中的实现 目标 解决手动打点效率低下问题 自动化埋点 本篇技术实现主要是运行是代理,不涉及到插桩技术,不引入插件,对业务影响点最小 技术难点 1. 如何拦截到所有的view的点 ...

最新文章

  1. 面试题目集锦--二叉树
  2. 【设计模式】前端必懂EventEmitter
  3. 历届试题 打印十字图(模拟)
  4. 蓝桥杯-算法提高-种树
  5. weblogic多次连接后tcp服务堵塞_网络编程——服务器篇
  6. python 两个变量同时循环_python基础篇(子非鱼)
  7. java 自己的 pid_Java获取自身PID方法搜集
  8. INFORMATION_SESSION_VARIABLES feature is disabled问题
  9. 【python博客爬虫】
  10. python构造icmp数据包_Python原始套接字未接收ICMP数据包
  11. 新版win10卸载Microsoft Edge
  12. 看书必备:40个全球免费开放电子图书馆
  13. Java 拾遗补阙 ----- Switch case语句
  14. 使用navicat创建mysql全文索引
  15. 用Regedit命令控制注册表
  16. 【网络安全】文件上传漏洞 详解
  17. RTSP 和 RTMP原理 通过ffmpeg实现将本地摄像头推流到RTSP服务器
  18. 1.3T计算机学科视频教程
  19. 漂亮的后台界面PSD下载
  20. Flask最强攻略 - 跟DragonFire学Flask - 第十五篇 Flask-Script

热门文章

  1. php怎么弄三角形,css中怎么设置三角形
  2. 办公自动化系统OA学习要点
  3. 阿里云IoTStudio中的“移动可视化开发”不见了
  4. SAP 采购发票校验
  5. 向量代数与空间解析几何
  6. ESP32设备驱动-MMA7455L加速计驱动
  7. LC振荡电路L和C 参数越小 频率越高
  8. PGM学习之七 MRF,马尔科夫随机场
  9. 【转载】8B/10B Encode/Decode详解
  10. 封装、继承、多态 通俗理解