字节码插桩属于编译插桩范畴之内,编译插桩是指在代码编译期间修改已有的代码或者生成新代码。Android 在编译过程中:
Java 源文件经过 javac 编译成 Java 字节码的 class 文件,再经过 dx/d8 工具处理成 Android 虚拟机字节码的 dex 文件。那么编译插桩的时机可以大致分为两种:
字节码插桩的使用场景有很多:
由于我们将要使用的 ASM 框架操作的是 class 字节码,而不是 dex 字节码,所以后续我们主要讨论的都是 class 字节码文件。但是你最起码需要知道,JVM 是基于栈的虚拟机,而 Dalvik/ART 是基于寄存器的虚拟机,后者的指令数以及数据的移动次数要比前者少。
在 Android Studio 中看到的 class 文件是这样的:
public class MainActivity extends AppCompatActivity {
public MainActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131427356);
this.test();
}
private void test() {
}
}
这其实是 Android Studio 按照 class 文件的格式进行反编译后的产物,用 010 Editor 查看 class 文件的二进制数据看到的才是庐山真面目:
其中前 4 个字节的 CAFEBABE 表示文件格式,所有 class 文件的开头都是 CAFEBABE。
那字节码插桩就是直接去操作这些二进制数据吗?当然不是,如果直接修改二进制文件需要弄清楚每一位二进制都表示的含义,成本太高,会改到吐血的……虽然不用去研究二进制,但是虚拟机的指令集多少还是要了解下的。
我们写的 Java 源代码都会转换成虚拟机指令之后才交给虚拟机执行,比如说:
图片左侧是 Java 源代码,右侧是使用 Android Studio 的 ASM Bytecode Viewer 插件转换出的对应的指令。比如说 ICONST_N 表示将 int 型常量压入【操作数栈】的位置 N,ISTORE K 表示将栈顶 int 型数值存入【局部变量表】的位置 K,IADD 表示执行 int 型加法。指令执行过程如下:
上面的 test() 还有一处需要注意,就是红框标记的 test()V,它是 Java 的方法签名,分成三个部分:
换一个方法看的能更清楚些:
当然你也可以用 javap 命令反编译 class 文件,与 ASM 插件显示的结果大致相同:
javap -c xxx.class
ASM 框架可以帮助我们进行字节码插桩,即便我们不熟悉 class 文件格式也可以操作字节码文件,修改已经存在的 class 文件(jar 包中的 class 也可以)中的属性、方法等,也可创建一个全新的 class。
ASM 的官方网站:ASM,目前最新版本是 ASM 9.3。
使用前需要在项目中引入 ASM 依赖:
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
ASM 主要有以下三个类:
两种基本用法,生成一个全新的类、修改已经存在的类。
假设要生成的字节码反编译后是这样的:
package com.example.old;
public class Demo1 {
private static final String TAG = "Demo1";
private String name;
public Demo1() {
}
public void print() {
System.out.println("name:" + this.name);
}
public void setName(String var1) {
this.name = var1;
}
}
ASM 代码可以这样写:
// 生成一个类文件,最后用 classWriter.toByteArray() 转换成 byte[] 返回
public byte[] createClass() {
// ClassWriter 的参数还可以传 COMPUTE_MAXS 和 COMPUTE_FRAMES,
// 分别表示自动更新操作数栈和方法调用帧计算
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
// 1.生成一个 public 的类,全类名为 com.example.asm.Demo1
classWriter.visit(V1_7, ACC_PUBLIC | ACC_SUPER, "com/example/asm/Demo1", null, "java/lang/Object", null);
// 2.生成字符串常量 TAG,并赋值为 Demo1
fieldVisitor = classWriter.visitField(ACC_PRIVATE | ACC_FINAL | ACC_STATIC, "TAG", "Ljava/lang/String;", null, "Demo1");
fieldVisitor.visitEnd();
// 3.生成私有成员变量 name
fieldVisitor = classWriter.visitField(ACC_PRIVATE, "name", "Ljava/lang/String;", null, null);
fieldVisitor.visitEnd();
// 4.生成默认构造方法
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "" , "()V", null, null);
// 开始方法访问
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
// 执行 INVOKESPECIAL 指令调用 java/lang/Object 的构造方法 ,参数为空没有返回值,不是接口方法
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "" , "()V", false);
methodVisitor.visitInsn(RETURN);
// 更新操作数栈
methodVisitor.visitMaxs(1, 1);
// 结束方法访问
methodVisitor.visitEnd();
// 5.生成 print 方法
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "print", "()V", null, null);
methodVisitor.visitCode();
// 执行 GETSTATIC 指令获取 java/lang/System 的成员 out,该成员类型为 java/io/PrintStream
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder");
methodVisitor.visitInsn(DUP);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "" , "()V", false);
methodVisitor.visitLdcInsn("name:");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitFieldInsn(GETFIELD, "com/example/asm/Demo1", "name", "Ljava/lang/String;");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(3, 1);
methodVisitor.visitEnd();
// 6.生成 setName 方法
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "setName", "(Ljava/lang/String;)V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(13, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitVarInsn(ALOAD, 1);
methodVisitor.visitFieldInsn(PUTFIELD, "com/example/asm/Demo1", "name", "Ljava/lang/String;");
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(14, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("this", "Lcom/example/asm/Demo1;", null, label0, label2, 0);
methodVisitor.visitLocalVariable("name", "Ljava/lang/String;", null, label0, label2, 1);
methodVisitor.visitMaxs(2, 2);
methodVisitor.visitEnd();
// 结束访问
classWriter.visitEnd();
// 转换成 byte[]
return classWriter.toByteArray();
}
然后用 IO 输出到指定路径的 Demo1.class 文件中即可:
public void run() throws Exception {
FileOutputStream fos = new FileOutputStream("...\\com\\example\\asm\\Demo1.class");
fos.write(createClass());
fos.close();
}
需要先用 IO 读取需要修改的 class 文件,并将数据(输入流/byte[])传入 ClassReader 再进行操作:
public void modify() throws Exception {
// 从 Demo1.class 读取,修改后输出到 Demo2.class 中
FileOutputStream fos = new FileOutputStream("...\\com\\example\\asm\\Demo2.class");
FileInputStream fis = new FileInputStream("...\\com\\example\\asm\\Demo1.class");
fos.write(modifyClass(fis));
fis.close();
fos.close();
}
private byte[] modifyClass(FileInputStream fis) throws IOException {
ClassReader classReader = new ClassReader(fis);
ClassWriter classWriter = new ClassWriter(0);
// 在 ClassVisitor 中修改
ClassVisitor classVisitor = new ClassVisitor(ASM7, classWriter) {
// 要修改方法,就在 visitMethod() 中进行
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
// 删除 print 方法
if ("print".equals(name)) {
return null;
}
// 将 setName 方法的访问负设为 private
if ("setName".equals(name)) {
access = ACC_PRIVATE;
}
// super 内调用的其实是 ClassVisitor 构造方法中 classWriter 的 visitMethod()
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
// 要增加方法或字段,最好在 visitEnd() 中进行,避免破坏之前排好的类结构
@Override
public void visitEnd() {
// 增加一个字段 private String address
FieldVisitor fieldVisitor = cv.visitField(ACC_PRIVATE, "address", "Ljava/lang/String;", null, null);
fieldVisitor.visitEnd();
// 增加一个方法 public String getAddress()
MethodVisitor methodVisitor = cv.visitMethod(ACC_PUBLIC, "getAddress", "()Ljava/lang/String;", null, null);
methodVisitor.visitCode();
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitFieldInsn(GETFIELD, "com/example/asm/Demo1", "address", "Ljava/lang/String;");
methodVisitor.visitInsn(IRETURN);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
super.visitEnd();
}
};
classReader.accept(classVisitor, 0);
return classWriter.toByteArray();
}
我们做的修改是将 Demo1.class 中的 print() 删除,将 setName() 修改为 private 的,同时增加 address 字段和 getAddress(),效果如下:
通过以上代码可能你已经发现了,使用 ASM 框架实现字节码插桩其实需要对虚拟机指令有一定的了解才可以,否则很难将简单的 Java 源代码转换成指令。在确实对指令没有熟练掌握的前提下,可以通过 ASM Bytecode 插件自动生成 ASM 框架代码:
字节码插桩最常用的例子就是统计方法的执行时间:
方法跟前面的例子大致相同,主要在于如何找准方法开始和结束的时机,示例代码如下:
private boolean useAdviceAdapter = false;
public void test() throws Exception {
// 1.创建 ClassReader、ClassWriter 对象
FileInputStream fis = new FileInputStream("...\\com\\example\\old\\Test.class");
ClassReader classReader = new ClassReader(fis);
// COMPUTE_FRAMES 会自动计算局部变量和操作数栈等
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
// 2.创建 ClassVisitor 对象,构造方法参数为当前所使用的 ASM 库的版本号以及 ClassWriter
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM7, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
// 使用自定义的 MethodVisitor 以达到修改方法内容的目的
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
if (!useAdviceAdapter) {
return new CalTimeVisitor1(Opcodes.ASM7, methodVisitor, name);
} else {
return new CalTimeVisitor2(Opcodes.ASM7, methodVisitor, access, name, descriptor);
}
}
};
// 3.开始解析
classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
// 4.输出到字节码文件中
byte[] bytes = classWriter.toByteArray();
FileOutputStream fos = new FileOutputStream("...\\com\\example\\old\\Test.class");
fos.write(bytes);
fos.close();
}
MethodVisitor 的子类定义了两个,效果是相同的,实现方式有差别,CalTimeVisitor1 是传统的实现方式,继承 MethodVisitor:
static class CalTimeVisitor1 extends MethodVisitor {
private String name;
public CalTimeVisitor1(int api, MethodVisitor methodVisitor, String name) {
super(api, methodVisitor);
this.name = name;
}
// 在开始访问方法时回调
@Override
public void visitCode() {
super.visitCode();
// 构造方法不插桩
if (!"" .equals(name)) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 1);
}
}
@Override
public void visitInsn(int opcode) {
// 在 return 之前添加代码
if (!"" .equals(name) && (opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mv.visitVarInsn(LSTORE, 3);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "" , "()V", false);
mv.visitLdcInsn("execute:");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
mv.visitVarInsn(LLOAD, 3);
mv.visitVarInsn(LLOAD, 1);
mv.visitInsn(LSUB);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
}
CalTimeVisitor2 则继承 AdviceAdapter,AdviceAdapter 是 MethodVisitor 的子类,它可以直接提供方法入口和出口的回调方法 onMethodEnter() 和 onMethodExit():
static class CalTimeVisitor2 extends AdviceAdapter {
private int start;
protected CalTimeVisitor2(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
// 方法入口回调
@Override
protected void onMethodEnter() {
super.onMethodEnter();
if ("" .equals(getName())) return;
// 调用静态方法 java/lang/System.currentTimeMillis()
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J"));
// 创建一个 Long 型的局部变量用来接收静态方法的返回值
start = newLocal(Type.LONG_TYPE);
// 将 invokeStatic() 运行的结果存入 start 中
storeLocal(start);
}
// 方法出口回调
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
if ("" .equals(getName())) return;
// 1.获取方法执行结束的时间并赋值给 end
invokeStatic(Type.getType("Ljava/lang/System;"),
new Method("currentTimeMillis", "()J")); // ()J 表示空参数,返回值为 Long 型
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);
// 2.获取 System.out 静态成员
getStatic(Type.getType("Ljava/lang/System;"), "out",
Type.getType("Ljava/io/PrintStream;"));
// 3.创建 StringBuilder 实例,用来保存拼接的字符串
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
dup();
// 调用 StringBuilder 的构造方法
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("" , "()V"));
// 将字符串 execute: 压入操作数栈
visitLdcInsn("execute:");
// 执行 StringBuilder 的 append()
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder")); // 方法签名最后的分号一定别忘了
// end - start
loadLocal(end);
loadLocal(start);
math(SUB, Type.LONG_TYPE);
// 将结果通过 append() 拼接到后面
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(J)Ljava/lang/StringBuilder;"));
// StringBuilder.toString()
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("toString", "()Ljava/lang/String;"));
// 执行 PrintStream 的 println()
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),
new Method("println", "(Ljava/lang/String;)V"));
}
}
实现效果:
有个细节需要注意下,填写方法签名时,如果是引用类型,不要忘记结尾的分号,否则插桩的代码可能会有问题,比如像下面这样:
字节码插桩可以帮我们实现 AOP,面向切面思想,将需要字节码插桩的方法视为一个切面,不需要的视为另一个切面,借助注解来完成切面划分,即打了注解的插桩,没打的不插:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ASMAnnotation {
}
重写 MethodVisitor 的 visitAnnotation(),这里仅以 CalTimeVisitor2 为例:
static class CalTimeVisitor2 extends AdviceAdapter {
// 是否对当前方法进行插桩
private boolean inject = false;
// 方法入口回调
@Override
protected void onMethodEnter() {
super.onMethodEnter();
// 构造方法与 inject = false 的方法不插桩
if ("" .equals(getName()) || !inject) return;
...
}
// 方法出口回调
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
// 构造方法与 inject = false 的方法不插桩
if ("" .equals(getName()) || !inject) return;
...
}
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
// 只有方法上打了 ASMAnnotation 注解时才插桩
if ("Lcom/example/test/ASMAnnotation;".equals(descriptor)) {
inject = true;
}
return super.visitAnnotation(descriptor, visible);
}
}
效果是在源码中标记了 @ASMAnnotation 注解的方法会被插桩:
此外,ASM 可以与 Gradle 插件结合使用实现自动化补丁,具体可以参考 Android 热修复 的【三、自定义 Gradle 插件打补丁包】这一节。