Android ASM插桩探索及实战

本文深入探讨了Android中ASM插桩技术,包括ASM的作用、使用方法以及如何将其应用于实际项目。ASM允许在编译期间修改Class文件,以实现如添加日志、方法节流等AOP功能。通过创建自定义的ClassVisitor和MethodVisitor,可以解析并修改字节码,实现对Android SDK和第三方库代码的增强。文章还介绍了如何构建Android Gradle插件,使用Transform遍历并修改项目中的Class文件,以及如何调试和优化编译过程。

前言

我们都知道,在Android编译过程中,Java代码会被编译成Class文件,Class文件再被打进Dex文件,虚拟机最终会加载Dex文件去执行。

编辑

添加图片注释,不超过 140 字(可选)

插桩,就是干涉代码的编译过程,在编译期间生成新的代码或者修改已有的代码。

常用的EventBus、ARouter,内部就是使用了APT(Annotation Process Tool),在编译的最开始解析Java文件中的注解,并生成新的Java文件。

但如果有以下两个需求:

  • 给Activity的attach()方法加日志

  • 将第三方库中所有调用getDeviceId()的地方替换为我们自己的方法,使其符合隐私规范

这两个需求一个是需要修改Android SDK的Activity文件,一个是需要修改三方库中的某个方法。而我们集成它们的方式是通过Jar包/AAR,本质上也就是Class文件。这时候就需要我们能够在编译阶段去修改Class文件,这也就是ASM发挥作用的地方。

通过本文,你可以解决如下问题:

  1. ASM的作用是什么?

  2. 如何使用ASM?

  3. 如何将ASM运用到我们的实际项目中来?

ASM的作用是什么?

在介绍ASM插桩之前,首先来回顾一下Java Class文件。在AS中,我们可以看到打开一个Class文件是这样的:

编辑

添加图片注释,不超过 140 字(可选)

但其实这是IDE为了方便开发者查阅,特意解析渲染了CLASS文件。如果直接拖进编辑器查看这个文件的话,我们可以看到它其实是这样的:

编辑

添加图片注释,不超过 140 字(可选)

上图是CLASS文件的16进制代码。一般人都看不懂这些代码的含义...但既然AS可以将这些代码解析成开发者可以看懂的样子,说明CLASS文件肯定是遵循某个格式规范的。所以,一个熟悉CLASS文件格式规范的开发者,是完全有能力解析所有的CLASS文件,甚至修改CLASS文件的。

ASM的开发者就是这么做的,并且提供一套完整的API帮助我们 在不需要了解CLASS文件格式规范的情况下,可以解析并修改CLASS文件 。

如何使用ASM?

基本使用方式

下面我们就来使用一下ASM,看一下它能达到的效果。假设现在我们需要统计MainActivity所有方法的耗时,原先的MainActivity.Class文件是这样的:

编辑

添加图片注释,不超过 140 字(可选)

用ASM修改过后的MainActivity.Class文件:

编辑

添加图片注释,不超过 140 字(可选)

具体的实现代码:

 
 

// 读取Class文件 String clazzFilePath = "/Users/xiaozhi/AndroidStudioProjects/ASMTest/app/build/intermediates/javac/debug/classes/com/xiaozhi/asmtest/MainActivity.class"; ClassReader classReader = new ClassReader(new FileInputStream(clazzFilePath)); ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter); classReader.accept(methodTimeConsumeClassVisitor, ClassReader.SKIP_FRAMES); // 写入Class文件 byte[] bytes = classWriter.toByteArray(); FileOutputStream fos = new FileOutputStream(clazzFilePath); fos.write(bytes); fos.flush(); fos.close(); 复制代码

首先在第6行,通过ClassReader.accept(classVisitor, parsingOptions)读取Class文件。然后将修改完的字节码用FileOutputStream写回原文件,原先的Class代码也就被修改了。但这里我们看不到是怎么修改的,因为 修改其实就发生在读取阶段,ClassReader负责读取解析Class文件,遇到相应节点后,调用ClassVisitor中的方法去修改相应的节点代码 (4、5行)。

这里涉及到两个类,ClassWriter与MethodTimeConsumeClassVisitor,这两个类都继承于ClassVisitor。结合第9行我们可以猜测,ClassWriter肯定可以记录我们修改后的字节码。既然ClassWriter是用来记录的,而第6行ClassReader.accept(classVisitor, parsingOptions)读取Class文件又只能接收一个classVisitor,那我们怎么用另一个ClassVisitor去修改Class文件呢?

我们可以看到ClassVisitor有这么一个构造函数:

 
 

public ClassVisitor(final int api, final ClassVisitor classVisitor) 复制代码

所以我们第5行的代码,实际上是用自定义的ClassVisitor-MethodTimeConsumeClassVisitor,代理了ClassWriter,在需要修改的Class节点复写方法进行修改就可以了。

另外我们额外了解一下构造函数中的几个参数。

 
 

// 接收Flag参数,用于设置方法的操作数栈的深度。COMPUTE_MAXS可以自动帮我们计算stackSize。 ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS); // 接收api与ClassVisitor。 Opcodes.ASM4~Opcodes.ASM9标识了ASM的版本信息 MethodTimeConsumeClassVisitor methodTimeConsumeClassVisitor = new MethodTimeConsumeClassVisitor(Opcodes.ASM5, classWriter); // 接收ClassVisitor与parsingOptions参数。 parsingOptions用来决定解析Class的方式,SKIP_FRAMES代表跳过MethodVisitor.visitFram

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值