Android ASM字节码插桩(上)
一、ASM简介
ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码、添加成员变量、修改父类、添加接口等等。
插桩就是将一段代码插入或者替换原本的代码。字节码插桩就是在编写的java源码编译成class字节码后,在Android下生成dex之前修改class文件,修改或者增强原有代码逻辑的操作。
二、引入ASM库
可以访问ASM官网,https://asm.ow2.io/index.html,更新asm的版本,目前最新版本9.3
在app/build.gradle下引入asm库
dependencies {
/**
* 使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,
* 对我们Android中的依赖关系没有任何影响
*/
testImplementation 'org.ow2.asm:asm:9.3'
testImplementation 'org.ow2.asm:asm-commons:9.3'
}
引入asm库之后,就在src\test目录下编写测试代码
cfab9ccb35f2360f5bebe6c71a9ba0f.png
三、使用ASM进行字节码插桩
应用场景:通过字节码插桩计算方法的执行时间。
3.1 首先编写测试类InjectTest.java
package com.xyaty.asmdemo;
/**
* DESC : 测试类
*/
public class InjectTest {
public static void main(String[] args) throws InterruptedException {
//模拟方法执行的时间
Thread.sleep(1000);
}
public void methodA() {
System.out.println("methodA");
}
}
3.2 接下来对InjectTest.java文件通过javac命令进行编译成InjectTest.class
由于我们操作的是字节码插桩,也就是class文件,所以需要进入 test/java下面使用 javac对这个java类进行编译生成对应的class文件,具体操作是:在Android studio底部Terminal窗口,通过cd进入到test/java目录下,然后执行以下命令:
D:\work\plugin\ASMDemo\app\src\test\java>javac com\xyaty\asmdemo\InjectTest.java
执行上面的命令编译后,就会在test/java下面生成对应的InjectTest.class文件,这个class文件就是待插桩的文件。
生成的InjectTest.class文件如下:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.xyaty.asmdemo;
public class InjectTest {
public InjectTest() {
}
public static void main(String[] var0) throws InterruptedException {
Thread.sleep(1000L);
}
public void methodA() {
System.out.println("methodA");
}
}
3.3 期望实现的效果就是利用ASM完成对InjectTest.class字节码的插桩
public static void main(String[] args) throws InterruptedException {
//方法开始的时间
long start = System.currentTimeMillis();
Thread.sleep(1000);
//方法结束的时间
long end = System.currentTimeMillis();
//输出方法执行花费的时间
System.out.println("execute: "+(end - start)+"ms");
}
3.4 编写测试类InjectUnitTest.java执行插桩
package com.xyaty.asmdemo;
import org.junit.Test;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
/**
* DESC :
*/
public class InjectUnitTest {
/**
* 单元测试方法,右击test()方法,选择run test()方法即可查看结果
*/
@Test
public void test() {
try {
//读取待插桩的class
FileInputStream fis = new FileInputStream(
new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));
/**
* 执行分析与插桩
* ClassReader是class字节码的读取与分析引擎
*/
ClassReader classReader = new ClassReader(fis);
// ClassWriter写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
/**
* 执行分析,处理结果写入classWriter, EXPAND_FRAMES表示栈图以扩展格式进行访问
* 执行插桩的代码就在MyClassVisitor中实现
*/
classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);
//获得执行了插桩之后的字节码数据
byte[] bytes = classWriter.toByteArray();
// 重新写入InjectTest.class中(也可以写入到其他class中,InjectTest1.class),完成插桩
FileOutputStream fos = new FileOutputStream(
new File("src/test/java/com/xyaty/asmdemo/InjectTest.class"));
fos.write(bytes);
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
}
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
System.out.println("visitMethod==>name="+name);
/**
* 会输出以下方法:
* visitMethod==>name=<init>
* visitMethod==>name=main
*/
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(api, methodVisitor, access, name,descriptor);
}
}
/**
* 之所以继承自AdviceAdapter,是因为AdviceAdapter是MethodVisitor的子类,
* AdviceAdapter封装了指令插入方法,更为直观与简单,
* 要使用其中的onMethodEnter和 onMethodExit方法进行字节码插桩,
*
* 继承关系如下:
* AdviceAdapter extends GeneratorAdapter
* GeneratorAdapter extends LocalVariablesSorter
* LocalVariablesSorter extends MethodVisitor
*/
public class MyMethodVisitor extends AdviceAdapter {
long start;
private int startIdentifier;
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println("visitAnnotation===>methodName="+getName()+", descriptor="+descriptor);
return super.visitAnnotation(descriptor, visible);
}
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
/**
* 进入方法插入内容
*/
@Override
protected void onMethodEnter() {
super.onMethodEnter();
// start = System.currentTimeMillis();
/**
* @Type owner 调用哪个类
* @Method method 调用某个类的静态方法(参数name: 方法名字,descriptor:方法中参数和方法返回值类型)
*/
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
//调用newLocal创建一个long类型的变量,返回一个int类型索引identifier
startIdentifier = newLocal(Type.LONG_TYPE);
//保存到本地变量索引中,用一个本地变量接收上一步执行的结果
storeLocal(startIdentifier);
}
/**
* 在方法结尾插入内容
* @param opcode
*/
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
// long end = System.currentTimeMillis();
// System.out.println("execute: "+(end - start)+"ms");
invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));
//调用newLocal创建一个long类型的变量,返回一个int类型索引identifier
int endIdentifier = newLocal(Type.LONG_TYPE);
//保存到本地变量索引中,用一个本地变量接收上一步执行的结果
storeLocal(endIdentifier);
//获取System的静态字段out,类型为PrintStream
getStatic(Type.getType("Ljava/lang/System;"),
"out", Type.getType("Ljava/io/PrintStream;"));
/**
* "execute: "+(end - start)+"ms"实际是内部创建StringBuilder来拼接
* 源码:NEW java/lang/StringBuilder
* 创建一个对象StringBuilder
*/
newInstance(Type.getType("Ljava/lang/StringBuilder;"));
// dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
dup();
/**
* 源码:INVOKESPECIAL java/lang/StringBuilder.<init> ()V
* 创建StringBuilder的构造方法,用init来代替
*/
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),
new Method("<init>", "()V"));
visitLdcInsn("execute: ");
/**
* 源码:INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
* 调用append方法
*/
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
/**
* 对结束时间和开始时间进行减法操作
* LLOAD 3 先加载结束时间
* LLOAD 1 后加载开始时间
* LSUB 执行减法操作
*/
loadLocal(endIdentifier);
loadLocal(startIdentifier);
//执行减法操作,返回long类型
math(SUB, Type.LONG_TYPE);
/**
* 源码:INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
* LDC "ms"
*/
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(J)Ljava/lang/StringBuilder;"));
//拼接毫秒
visitLdcInsn("ms");
/**
* 源码:
* INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
* INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
* INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
*/
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),
new Method("toString", "()Ljava/lang/String;"));
invokeVirtual(Type.getType("Ljava/io/PrintStream;"),
new Method("println", "(Ljava/lang/String;)V"));
}
}
}
在上述代码中可以看到,
其实就是在onMethodEnter()方法中插入了:
long start = System.currentTimeMillis();
在onMethodExit()方法中插入了:
long end = System.currentTimeMillis();
System.out.println("execute: "+(end - start)+"ms");
但是使用字节码指令却写了一大堆代码。
3.5 可以通过android studio的插件ASM(以下三选一),查看指令代码
(1)ASM Bytecode Viewer
(2)ASM Bytecode Viewer Support Kotlin
(3)ASM Bytecode Outline
在AS4.1以上版本使用ASM Bytecode Viewer和ASM Bytecode Outline都报错,可能是AS升级后没有做兼容,使用ASM Bytecode Viewer Support Kotlin即可搞定(亲测)。
安装好ASM之后,重启AS,在InjectTest.java中右键点击选项“ASM Bytecode Viewer”查看指令代码
InjectTest.java文件
package com.xyaty.asmdemo;
/**
* Author :
* Date : 2022/7/21
* DESC :
*/
public class InjectTest {
@ASMTest
public static void main(String[] args) throws InterruptedException {
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println("execute: "+(end - start)+"ms");
}
public void methodA() {
System.out.println("methodA");
}
}
ASM指令代码截图:
778c04f87130c625967e140f11bf6dc.png
ASM指令代码如下:
// class version 51.0 (51)
// access flags 0x21
public class com/xyaty/asmdemo/InjectTest {
// compiled from: InjectTest.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 9 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V throws java/lang/InterruptedException
@Lcom/xyaty/asmdemo/ASMTest;() // invisible
L0
LINENUMBER 13 L0
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 1
L1
LINENUMBER 14 L1
LDC 1000
INVOKESTATIC java/lang/Thread.sleep (J)V
L2
LINENUMBER 15 L2
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 3
L3
LINENUMBER 16 L3
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "execute: "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
LLOAD 3
LLOAD 1
LSUB
INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
LDC "ms"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L4
LINENUMBER 17 L4
RETURN
L5
LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
LOCALVARIABLE start J L1 L5 1
LOCALVARIABLE end J L3 L5 3
MAXSTACK = 6
MAXLOCALS = 5
// access flags 0x1
public methodA()V
L0
LINENUMBER 20 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "methodA"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 21 L1
RETURN
L2
LOCALVARIABLE this Lcom/xyaty/asmdemo/InjectTest; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
这些指令涉及到了java类型描述符和方法描述,
f0d7a05a07f4257db0a53660bebb9bd.png 59a8e46f68d9904b087b3e0c49fcafc.png
①类型描述符
Java代码中的类型,在字节码中有相应的表示协议:
(1)Java基本类型的描述符是单个字符,例如Z表示boolean、C表示char
(2)类的类型的描述符是这个类的全限定名,前面加上字符L , 后面跟上一个「;」,例如String的类型描述符为Ljava/lang/String;
(3)数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符,多维数组则使用多个方括号。
借助上面的协议分析,想要看到字节码中参数的类型,就比较简单了。
②方法描述符
方法描述符(方法签名)是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。
方法描述符以左括号开头,然后是每个形参的类型描述符,然后是是右括号,接下来是返回类型的类型描述符,例如,该方法返回void,则是V,要注意的是,方法描述符中不包含方法的名字或参数名。
比如:
void m(int i, float f)对应的方法描述符是(IF)V ,表明该方法会接收一个int和float型参数,且无返回值。
int m(Object o)对应的方法描述符是(Ljava/lang/Object;)I 表示接收Object型参数,返回int。
int[] m(int i, String s)对应的方法描述符是(ILjava/lang/String;)[I 表示接受int和String,返回一个int[]。
Object m(int[] i)对应的方法描述符是 ([I)Ljava/lang/Object; 表示接受一个int[],返回Object。