前言
字节码插桩,看起来挺牛皮,实际上是真的很牛皮。
但是牛皮不代表难学,只需要一点前置知识就能轻松掌握。
Gradle Transform
Google在Android Gradle的1.5.0 版本以后提供了 Transfrom API,允许开发者在项目的编译过程中操作 .class 文件。Transfrom需要介绍的地方不多,唯一的难点就是要熟悉API,我会在文尾推荐相关文章,这里就不过多介绍,影响大家的阅读体验。
ASM
ASM是一种通用Java字节码操作和分析框架。它可以用于修改现有的class文件或动态生成class文件。
刚去了解ASM的时候,我是真的差点被字节码吓退,字节码这东西根本就不是给人读的,在我认知里能去读字节码的都是大神。就在我准备放弃时,ASM Bytecode Viewer从天而降拯救了我。
ASM Bytecode Viewer
ASM Bytecode Viewer是一款能 查看字节码 和 生成ASM代码 的插件,帮助我们打败了ASM学习路上最大的拦路虎,剩下就是对ASM的熟悉和使用可以说是so easy。
1.在Android Studio中搜索 ASM Bytecode Viewer Support Kotlin 找到并安装
2.代码右键 ASM Bytecode Viewer 便能自动生成ASM插桩代码,效果如下:
实战:
前面介绍了 Gradle Transform 、 ASM 及 ASM Bytecode Viewer,现在就正式进入实战,先看下目录结构:
1、StatisticPlugin
顾名思义就是我们本次编写的插件,在apply
方法的注册 BuryPointTransform,读取 build.gradle 里面配置的需要埋点的方法和注解。(Gradle Transform属实没啥好介绍,后面我就不过多哔哔,直接上代码和注释。熟悉并觉得无聊可直接跳到 BuryPointMethodVisitor)
class StatisticPlugin implements Plugin<Project> {
public static Map<String, BuryPointEntity> BURY_POINT_MAP
@Override
void apply(Project project) {
def android = project.extensions.findByType(AppExtension)
// 注册BuryPointTransform
android.registerTransform(new BuryPointTransform())
// 获取gradle里面配置的埋点信息
def statisticExtension = project.extensions.create('statistic', StatisticExtension)
project.afterEvaluate {
// 遍历配置的埋点信息,将其保存在BURY_POINT_MAP方便调用
BURY_POINT_MAP = new HashMap<>()
def buryPoint = statisticExtension.getBuryPoint()
if (buryPoint != null) {
buryPoint.each { Map<String, Object> map ->
BuryPointEntity entity = new BuryPointEntity()
...省略中间非关键代码,详细请到github中查看...
if (entity.isAnnotation) {
if (map.containsKey("annotationDesc")) {
entity.annotationDesc = map.get("annotationDesc")
}
if (map.containsKey("annotationParams")) {
entity.annotationParams = map.get("annotationParams")
}
BURY_POINT_MAP.put(entity.annotationDesc, entity)
} else {
if (map.containsKey("methodOwner")) {
entity.methodOwner = map.get("methodOwner")
}
if (map.containsKey("methodName")) {
entity.methodName = map.get("methodName")
}
if (map.containsKey("methodDesc")) {
entity.methodDesc = map.get("methodDesc")
}
BURY_POINT_MAP.put(entity.methodName + entity.methodDesc, entity)
}
}
}
}
}
}
2、BuryPointTransform
通过transform
方法的 Collection<TransformInput> inputs
对 .class文件遍历拿到所有方法
class BuryPointTransform extends Transform {
...省略中间非关键代码,详细请到github中查看...
/**
*
* @param context
* @param inputs 有两种类型,一种是目录,一种是 jar 包,要分开遍历
* @param outputProvider 输出路径
*/
@Override
void transform(
@NonNull Context context,
@NonNull Collection<TransformInput> inputs,
@NonNull Collection<TransformInput> referencedInputs,
@Nullable TransformOutputProvider outputProvider,
boolean isIncremental
) throws IOException, TransformException, InterruptedException {
if (!incremental) {
//不是增量更新删除所有的outputProvider
outputProvider.deleteAll()
}
inputs.each { TransformInput input ->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
handleDirectoryInput(directoryInput, outputProvider)
}
// 遍历jar 第三方引入的 class
input.jarInputs.each { JarInput jarInput ->
handleJarInput(jarInput, outputProvider)
}
}
}
}
3、BuryPointClassVisitor
通过visitMethod
拿到方法进行修改
class BuryPointVisitor extends ClassVisitor {
...省略中间非关键代码,详细请到github中查看...
/**
* 扫描类的方法进行调用
* @param access 修饰符
* @param name 方法名字
* @param descriptor 方法签名
* @param signature 泛型信息
* @param exceptions 抛出的异常
* @return
*/
@Override
MethodVisitor visitMethod(int methodAccess, String methodName, String methodDescriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(methodAccess, methodName, methodDescriptor, signature, exceptions)
if ((methodAccess & Opcodes.ACC_INTERFACE) == 0 && "<init>" != methodName && "<clinit>" != methodName) {
methodVisitor = new BuryPointAdviceAdapter(api, methodVisitor, methodAccess, methodName, methodDescriptor)
}
return methodVisitor
}
}
4、BuryPointAdviceAdapter
终于到了本次文章的核心代码了。
-
visitAnnotation
在扫描到注解时调用。我们通过 descriptor 来判断是否是需要埋点的注解,如果是则保存注解参数和对应的方法名称,等到onMethodEnter
时进行代码插入。 -
visitInvokeDynamicInsn
在描到lambda表达式时调用,bootstrapMethodArguments[0] 得到方法描述,通过 name + desc 判断当前lambda表达式是否是需要的埋点的方法,如果是则保存lambda方法名称,等到onMethodEnter
时进行代码插入。 -
onMethodEnter
在进入方法时调用,这里就是我们插入代码的地方了。通过 methodName + methodDescriptor 判断当前方法是否是需要的埋点的方法,如果是则插入埋点方法。
——重点,要考,画起来——
mv.visitVarInsn(type.getOpcode(ISTORE), slotIndex)
的 slotIndex 是怎么的来的呢?
答:因为我们要通过visitVarInsn
把注解参数压入到局部变量表中,而局部变量表(Local Variable Table)是一组变量值存储空间,用于存放 方法参数和方法内定义的局部变量。具体的顺序是 this-方法接收的参数-方法内定义的局部变量。因此我们要通过newLocal(type)
来获取 slotIndex 按顺序把注解参数压入到局部变量表中。isStatic(methodAccess) ? 0 : 1
为什么 static 方法是0开始计算?
答:对于非静态方法(non-static method)来说,索引位置为0
的位置存放的是this
变量,所以要加1;对于静态方法(static method)来说,索引位置为0
的位置则不需要存储this
变量。
class BuryPointAdviceAdapter extends AdviceAdapter {
...省略中间非关键代码,详细请到github中查看...
/**
* 扫描类的注解时调用
* @param descriptor 注解名称
* @param visible
* @return
*/
@Override
AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
AnnotationVisitor annotationVisitor = super.visitAnnotation(descriptor, visible)
// 通过descriptor判断是否是需要扫描的注解
BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(descriptor)
if (entity != null) {
BuryPointEntity newEntity = entity.clone()
return new BuryPointAnnotationVisitor(api, annotationVisitor) {
@Override
void visit(String name, Object value) {
super.visit(name, value)
// 保存注解的参数值
newEntity.annotationData.put(name, value)
}
@Override
void visitEnd() {
super.visitEnd()
newEntity.methodName = methodName
newEntity.methodDesc = methodDesc
StatisticPlugin.BURY_POINT_MAP.put(newEntity.methodName + newEntity.methodDesc, newEntity)
}
}
}
return annotationVisitor
}
/**
* lambda表达式时调用
* @param name
* @param descriptor
* @param bootstrapMethodHandle
* @param bootstrapMethodArguments
*/
@Override
void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments)
String desc = (String) bootstrapMethodArguments[0]
BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(name + desc)
if (entity != null) {
String parent = Type.getReturnType(descriptor).getDescriptor()
if (parent == entity.methodOwner) {
Handle handle = (Handle) bootstrapMethodArguments[1]
BuryPointEntity newEntity = entity.clone()
newEntity.isLambda = true
newEntity.methodName = handle.getName()
newEntity.methodDesc = handle.getDesc()
StatisticPlugin.BURY_POINT_MAP.put(newEntity.methodName + newEntity.methodDesc, newEntity)
}
}
}
/**
* 方法进入时调用
*/
@Override
protected void onMethodEnter() {
super.onMethodEnter()
BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(methodName + methodDesc)
if (entity != null && !entity.isMethodExit) {
onMethod(entity)
}
}
/**
* 方法退出前调用
*/
@Override
protected void onMethodExit(int opcode) {
BuryPointEntity entity = StatisticPlugin.BURY_POINT_MAP.get(methodName + methodDesc)
if (entity != null && entity.isMethodExit) {
onMethod(entity)
}
super.onMethodExit(opcode)
}
private void onMethod(BuryPointEntity entity) {
if (entity.isAnnotation) {
// 遍历注解参数并赋值给采集方法
for (Map.Entry<String, String> entry : entity.annotationParams.entrySet()) {
String key = entry.getKey()
if (key == "this") {
//所在方法的当前对象的引用
mv.visitVarInsn(ALOAD, 0)
} else {
mv.visitLdcInsn(entity.annotationData.get(key))
Type type = Type.getType(entry.getValue())
int slotIndex = newLocal(type)
mv.visitVarInsn(type.getOpcode(ISTORE), slotIndex)
mv.visitVarInsn(type.getOpcode(ILOAD), slotIndex)
}
}
mv.visitMethodInsn(INVOKESTATIC, entity.agentOwner, entity.agentName, entity.agentDesc, false)
// 防止其他类重名方法被插入
StatisticPlugin.BURY_POINT_MAP.remove(methodName + methodDesc, entity)
} else {
// 获取方法参数
Type methodType = Type.getMethodType(methodDesc)
Type[] methodArguments = methodType.getArgumentTypes()
// 采集数据的方法参数起始索引( 0:this,1+:普通参数 ),如果是static,则从0开始计算
int slotIndex = (methodAccess & ACC_STATIC) != 0 ? 0 : 1
// 获取采集方法参数
Type agentMethodType = Type.getMethodType(entity.agentDesc)
Type[] agentArguments = agentMethodType.getArgumentTypes()
List<Type> agentArgumentList = new ArrayList<Type>(Arrays.asList(agentArguments))
// 将扫描方法参数赋值给采集方法
for (Type methodArgument : methodArguments) {
int size = methodArgument.getSize()
int opcode = methodArgument.getOpcode(ILOAD)
String descriptor = methodArgument.getDescriptor()
Iterator<Type> agentIterator = agentArgumentList.iterator()
// 遍历采集方法参数
while (agentIterator.hasNext()) {
Type agentArgument = agentIterator.next()
String agentDescriptor = agentArgument.getDescriptor()
if (agentDescriptor == descriptor) {
mv.visitVarInsn(opcode, slotIndex)
agentIterator.remove()
break
}
}
slotIndex += size
}
if (agentArgumentList.size() > 0) { // 无法满足采集方法参数则return
return
}
mv.visitMethodInsn(INVOKESTATIC, entity.agentOwner, entity.agentName, entity.agentDesc, false)
if (entity.isLambda) {
StatisticPlugin.BURY_POINT_MAP.remove(methodName + methodDesc, entity)
}
}
}
}
5、 如何使用?
5.1、 先打包插件到本地仓库进行引用
5.2、 在项目的根build.gradle加入插件的依赖
repositories {
google()
mavenCentral()
jcenter()
maven{
url uri('repos')
}
}
dependencies {
classpath "com.android.tools.build:gradle:$gradle_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.meituan.android.walle:plugin:1.1.7'
// 使用自定义插件
classpath 'com.example.plugin:statistic:1.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
5.3、 在app的build.gradle中使用并配置参数
plugins {
id 'com.android.application'
id 'statistic'
}
statistic {
buryPoint = [
[
//注解标识
'isAnnotation' : true,
//方式插入时机,true方法退出前,false方法进入时
'isMethodExit' : true,
//采集数据的方法的路径
'agentOwner' : 'com/example/fragment/library/common/utils/StatisticHelper',
//采集数据的方法名
'agentName' : 'testAnnotation',
//采集数据的方法描述(对照annotationParams,注意参数顺序)
'agentDesc' : '(Ljava/lang/Object;ILjava/lang/String;)V',
//扫描的注解名称
'annotationDesc' : 'Lcom/example/fragment/library/common/utils/TestAnnotation;',
//扫描的注解的参数
'annotationParams': [
//参数名 : 参数类型(对应的ASM指令,加载不同类型的参数需要不同的指令)
//this : 所在方法的当前对象的引用(默认关键字,按需可选配置)
'this' : 'Ljava/lang/Object;',
'code' : 'I',
'message': 'Ljava/lang/String;',
]
],
]
}
6、 运行项目查看输出日志
2021-06-28 20:04:49.544 25211-25211/com.example.fragment.project.debug I/----------自动埋点:注解: MainActivity.onCreate:false
2021-06-28 20:05:06.085 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:coin ViewText:我的积分
2021-06-28 20:05:11.616 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:username ViewText:去登录
2021-06-28 20:05:16.816 25211-25211/com.example.fragment.project.debug I/----------自动埋点: ViewId:login ViewText:登录
参考
在AndroidStudio中自定义Gradle插件
史上最通俗易懂的ASM教程
Android函数插桩(Gradle + ASM)
Thanks
以上就是本篇文章的全部内容,如有问题欢迎指出,我们一起进步。
如果喜欢的话希望点个赞吧,您的鼓励是我前进的动力。
谢谢~~
项目地址
- github: https://github.com/miaowmiaow