手动埋点转无痕埋点,如何做到代码“零”入侵
前言
在起初的手动埋点的时候,每次版本大更新,很多埋点都要进行修改,删除。这个时候之前嵌在源码里面的一行行埋点代码要进行修改,删除。删了又找,找了又改,很麻烦。如果遇到有代码洁癖的,“产品你竟然要在我代码里加这么多埋点,很影响我代码美观,晓得不!”心里敢想不敢说。于是我就想能不能利用字节码插桩把埋点信息插进去呢...
在去年突发奇想,想利用Gradle插件,Transform+ASM实现字节码插桩,将需要手动埋点的地方通过操作字节码进行埋点。
先是在网上搜索相关无痕埋点框架,搜到不少,都并不符合我的预期,我预期是对源代码不要进行任何的手动操作。而很多框架是利用了注解,需要在埋点的地方标记注解,emmm.....似乎都是这么解决了。
还有一种无痕埋点呢,像didi的无痕埋点,是记录所有View的id。框架太复杂了。。。。望而却步。
就像这样一个埋点,记录新手用户点击领取新用户礼包这样一个事件信息,"new_user_receive_gift" 就这么一个信息。我怎么才能在不修改源代码的情况下,把这个加入到对应的领取按钮触发的方法里面呢。
我起初想到的是用AspectJ,但是对于使用者来说,还是比较麻烦的,有一定的学习成本,如果不使用注解进行aop的话,那操作起来是相当麻烦的。
于是我就放弃了AspectJ,转而了解了一下Transform和ASM,我发现这个可行。
我设想的实际操作流程:
我通过一个配置文件将埋点信息记录。(实际使用)
编写一个接收埋点信息事件的接收类,将接收到的埋点信息通过埋点统计框架上传。(实际使用)
通过Transform执行的时候读取埋点信息。(框架封装)
利用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
第一行,还是method
的return
时的位置。默认是false
event
事件和eventMap
事件的Value值可以使用占位符来获取全局变量和局部变量。
使用
${...}
来进行占位标识,${this.xxx}
表示获取全局变量xxx。${xxx}
表示获取方法的局部变量xxx
例如:
event :"全局变量:,${this.globalName},局部变量:,${localName}" eventMap :{"全局变量":"${this.globalName}","方法局部变量":"${localName}"}
event
和eventMap
两种类型的事件只取其一,如果两者都有数据,优先使用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位置都会被插入同样的埋点信息。
如果你携带了局部变量。当局部变量不在可索引范围内的时候,埋点事件框架不会将无法索引的局部变量添加到事件中。
例如埋点:上传检查后的a
和b
的值
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
手动埋点转无痕埋点,如何做到代码“零”入侵相关推荐
- ios无痕埋点_iOS无痕埋点方案分享探究
原标题:iOS无痕埋点方案分享探究 作者丨SandyLoo https://www.jianshu.com/p/b8a67c4acfb3 前言 当前互联网行业的竞争已经是非常激烈了, "功能 ...
- iOS 最优无痕埋点方案
iOS 最优无痕埋点方案 在移动互联网时代,对于每个公司.企业来说,用户的行为数据非常重要.重要到什么程度,用户在这个页面停留多久.点击了什么按钮.浏览了什么内容.什么手机.什么网络环境.App什么版 ...
- 无痕埋点的设计与实现
在移动互联网时代,对于每个公司.企业来说,用户的行为数据非常重要.重要到什么程度,用户在这个页面停留多久.点击了什么按钮.浏览了什么内容.什么手机.什么网络环境.App什么版本等都需要清清楚楚.一些大 ...
- iOS-史上最强、最详细无痕埋点方案
在移动互联网时代,对于每个公司.企业来说,用户的行为数据非常重要.重要到什么程度,用户在这个页面停留多久.点击了什么按钮.浏览了什么内容.什么手机.什么网络环境.App什么版本等都需要清清楚楚.一些大 ...
- 美团点评前端无痕埋点实践
构建一个数据平台,大体上包括数据采集.数据上报.数据存储.数据计算以及数据可视化展示等几个重要的环节.其中,数据采集与上报是整个流程中重要的一环,只有确保前端数据生产的全面.准确.及时,最终产生的数据 ...
- ios无痕埋点_移动端无痕埋点实践详解(二)
0x01 前言 在移动端无痕埋点实践详解(一)这篇文章大致总结了移动端无痕埋点的基本原理.主要介绍了什么是无痕埋点,无痕埋点的基础数据流程以及在Android系统上总体思路.这篇文章着重总结下无痕埋点 ...
- ios无痕埋点_iOS可视化埋点方案
前言 随着公司业务的发展,数据的重要性日益体现出来. 数据埋点的准确和全面性显得尤为重要. 通过精准和详细的数据,后面的分析才有意义.随着业务的不断变化,动态化埋点也越来越重要. 三大埋点方式 为了解 ...
- iOS 打点上报、无痕埋点
最近研习了美团等大厂的一些埋点方案. 还要感谢大神<xuhaoranLeo>的指点.(既然大神没空写博客.但我可以代劳哈). 本文的宗旨是尽量全面.精简.满足我能想到尽量多的埋点需求. 主 ...
- 无痕埋点在Android中的实现
无痕埋点在Android中的实现 目标 解决手动打点效率低下问题 自动化埋点 本篇技术实现主要是运行是代理,不涉及到插桩技术,不引入插件,对业务影响点最小 技术难点 1. 如何拦截到所有的view的点 ...
最新文章
- 面试题目集锦--二叉树
- 【设计模式】前端必懂EventEmitter
- 历届试题 打印十字图(模拟)
- 蓝桥杯-算法提高-种树
- weblogic多次连接后tcp服务堵塞_网络编程——服务器篇
- python 两个变量同时循环_python基础篇(子非鱼)
- java 自己的 pid_Java获取自身PID方法搜集
- INFORMATION_SESSION_VARIABLES feature is disabled问题
- 【python博客爬虫】
- python构造icmp数据包_Python原始套接字未接收ICMP数据包
- 新版win10卸载Microsoft Edge
- 看书必备:40个全球免费开放电子图书馆
- Java 拾遗补阙 ----- Switch case语句
- 使用navicat创建mysql全文索引
- 用Regedit命令控制注册表
- 【网络安全】文件上传漏洞 详解
- RTSP 和 RTMP原理 通过ffmpeg实现将本地摄像头推流到RTSP服务器
- 1.3T计算机学科视频教程
- 漂亮的后台界面PSD下载
- Flask最强攻略 - 跟DragonFire学Flask - 第十五篇 Flask-Script