> 文档中心 > Android编译时技术(三)ASM 基础使用之代码修改

Android编译时技术(三)ASM 基础使用之代码修改


一、前言

        在上一篇已经介绍了ASM如何生成代码,接下来我们将介绍如何用ASM工具对代码进行插入和修改。先说下我们这章节的任务,如下:

  • 对已有的Class文件的方法里插入方法执行时间的统计

二、加载Class文件

        想要修改一个Class文件的内容,那肯定要先把Class文件加载进内存,不然你修改个&…(%¥。正好,在ASM工具里,有一个ClassReader类就可以帮助我们读取Class文件。接下来让我们看看如何使用

2.1 准备一个Class文件

准备一个Class文件,你可以先写一个Java文件,通过编译器编译或者自己用javac命令都可以。

 

 2.2 读取Class文件

 直接上代码(具体路径看自己Class文件所在)

public class ASMLoadDemo {    public static void main(String[] args) throws Exception {     insertCodeByLoadClass("D:\\20210426\\code\\JavaProjec\\JMMDemo\\out\\production\\JMMDemo\\asm\\InjectTest.class");    }    /**     * 加载Class插入代码     *     * @param path     */    public static void insertCodeByLoadClass(String path) { try {     //1.获取Class文件     FileInputStream fis = new FileInputStream(path);     //2.读取Class文件     ClassReader classReader = new ClassReader(fis);     //3.准备     ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);     //4.重点!! 开始插桩 对代码进行扫描,进行代码插入。     classReader.accept(new MyClassVisitor(Opcodes.ASM9, classWriter), ClassReader.EXPAND_FRAMES);     //5.插桩结束 输出字节码     byte[] bytes = classWriter.toByteArray();     //6.保存文件     FileOutputStream fos = new FileOutputStream("src/asm/InjectTest2.class");     fos.write(bytes);     fos.close();     fis.close(); } catch (Exception e) {     e.printStackTrace(); }    }}

 MyClassVisitor代码

    /**     * 用来观察(扫描)类信息,包括变量、方法     */    static class MyClassVisitor extends ClassVisitor { public MyClassVisitor(int api) {     super(api); } 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);     MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);     return new MyMethodVisitor(api, methodVisitor, access, name, descriptor); }    }

 MyMethodVisitor代码

    /**     * 用来观察(扫描)方法里的代码     */    static class MyMethodVisitor extends AdviceAdapter { protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {     super(api, methodVisitor, access, name, descriptor); } int startTimeIndex;//startTime在局部变量表的位置 int endTimeIndex;//endTime在局部变量表的位置 /**  * 方法进入的时候调用  */ @Override protected void onMethodEnter() {     super.onMethodEnter();     //调用System.currentTimeMillis方法,调用完后将返回值推到栈顶     invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));     //获取当前局部变量表可插入的位置,不能乱传,不然会影响下一个值的索引获取,这里你传DOUBLE_TYPE或者LONG_TYPE,都是一样的     startTimeIndex = newLocal(Type.LONG_TYPE);     //把栈顶的值(也就是System.currentTimeMillis的返回值)保存到局部变量表startTimeIndex位置     storeLocal(startTimeIndex);     System.out.println("onMethodEnter getName: " + getName() + " startIndex " + startTimeIndex); } /**  * 当方法退出的时候调用  */ @Override protected void onMethodExit(int opcode) {     super.onMethodExit(opcode);     //调用System.currentTimeMillis方法,调用完后将返回值推到栈顶     invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));     //获取当前局部变量表可插入的位置     endTimeIndex = newLocal(Type.LONG_TYPE);     //把栈顶的值(也就是System.currentTimeMillis的返回值)保存到局部变量表endTimeIndex位置     storeLocal(endTimeIndex);     //调用System.out,调用完后栈顶会有一个     getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));     //new 一个 StringBuilder();并将其引用值压入栈顶     newInstance(Type.getType("Ljava/lang/StringBuilder;"));     //复制栈顶数值并将复制值压入栈顶(对象StringBuilder的引用)     dup();     //调用StringBuilder的构造方法     invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"), new Method("", "()V"));     //将"execute time=" 从常量池中推送至栈顶     visitLdcInsn("execute time=");     //调用StringBuilder的append方法,将栈顶的值添加进StringBuilder     invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));     //把局部变量表中的endTimeIndex位置元素,压入栈顶     loadLocal(endTimeIndex);     //把局部变量表中的startTimeIndex位置元素,压入栈顶     loadLocal(startTimeIndex);     //将栈顶两元素进行相减,把结果再压入栈顶     math(SUB, Type.LONG_TYPE);     //调用append方法,将栈顶值添加     invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(J)Ljava/lang/StringBuilder;"));     //将" ms" 从常量池中推送至栈顶     visitLdcInsn(" ms");     调用StringBuilder的append方法,将栈顶的值添加进StringBuilder     invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;"));     调用StringBuilder的toString方法,调用完后,将结果推到栈顶     invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"), new Method("toString", "()Ljava/lang/String;"));     //调用println,打印栈顶的值     invokeVirtual(Type.getType("Ljava/io/PrintStream;"), new Method("println", "(Ljava/lang/String;)V;")); }    }

先看看效果

可以看到,每个方法都插入方法执行时间的统计(如何优化的问题留到最后后面🤭)。

看完结果,我们先抛开MyClassVisitor和MyMethodVisitor流程很简单,总共就三步

  1. 读取Class文件
  2. 使用ClassVisitor类对Class文件进行代码扫描,这一步就可以进行代码的修改和插入了。
  3. 代码修改插入结束后,自然就可以重新输出Class文件,这里我们给Class改个名为InjectTest2,方便观察。

可以看到,最重要的其实就是第二步,对字节码进行插桩(你也可以叫代码观察,代码扫描,你喜欢就行😀),如果你有第一节ASM如何生成代码的经验,上面的代码唯一不懂的也就是MyClassVisitor,其实这就一个对类里面代码进行扫描的类。相应的,还会MethodVisitorFiledVisitor,知道了ClassVisitor是什么意思后,这两个也不难理解。

接下来让我们一起看看ClassVisitorMethodVisitor的用法解释。如果知道的朋友,可以直接跳到优化章节了。

三、ClassVisitor

        看名字就能看出来,这是一个对Class文件进行观察(扫描)的工具类。因为它是一个抽象类,所以我们只能对其进行继承重写。

并且ClassVisitor所有的调用都是由ClassReader来进行回调,就是我们前面accept方法。而这个方法里,执行了对ClassVisitor源码扫描的回调,截图如下:

接下来我们看看ClassVisitor的构造方法

3.1 构造方法

  protected ClassVisitor(final int api, final ClassVisitor classVisitor)

参数解释:

  • api:代表是ASM API的版本 ,必须是如下的版本号,默认填最新(ASM9)即可。

        

      注意:编写代码和代码读取的版本号最好保持一致,不然可能会有一些兼容性的错误,例如下面,一些API版本的判断。

  • classVisitor:传一个ClassVisitor的子类,对字节码修改的内容进行保存。

上面这个解释可能会让你感到不可思议,我们可以先看我们上面代码这个参数传的是ClassWriter的实例,也就是说ClassWriter继承于ClassVisitor。其实也不难理解,这里ClassWriter的作用就是保存我们修改的信息,然后重新输出字节码,因为ClassVisitor就是一个纯粹的代码扫描类(可以自行去看他的源码,没有保存修改代码的地方)。那么这个字段的作用其实就是用来对字节码修改进行保存的,我们简单来看下ClassVisitor某个方法的回调。例如:visitMethod方法,因为我们就是在这里进行字节码修改的

  public MethodVisitor visitMethod(      final int access,      final String name,      final String descriptor,      final String signature,      final String[] exceptions) {    if (cv != null) {      return cv.visitMethod(access, name, descriptor, signature, exceptions);    }    return null;  }

可以看到,方法最终的调用都给了cv,而这个cv就是我们传进来的ClassWriter 。不仅是这个方法,在ClassVisitor中,除了构造方法外,其他所有的方法最终的执行都是给了cv,也就是说,ClassVisitor就是一个代理类。

接下来介绍几个常用回调方法。

3.2 visitMethod

作用:回调Class中的每一个方法

源代码如下:

 @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {     return super.visitMethod(access, name, descriptor, signature, exceptions); }

参数的解释这里就不过说明了,在上章节中有具体说明,这里说下如何使用来修改字节码。

其实也很简单,固定用法,删除某个方法

 @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {     System.out.println("读到Class的一个方法visitMethod: " + name);     if (name.equals("test")){  //删除test方法  return null;     }     return  super.visitMethod(access, name, descriptor, signature, exceptions); }

想对方法体修改:

  MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);     return new MyMethodVisitor(api, methodVisitor, access, name, descriptor);

为什么需要拿到super.visitMethod呢? 

因为这个拿到的methodVisitor不就是我们传入的ClassWriter的MethodVisitor,不拿到它,最后怎么把代码合成一份。

3.3 visitField

作用:回调Class中的每一个变量

使用如下,删除/修改一个变量

 public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {     System.out.println("读到Class的一个变量visitField: " + name);     if (name.equals("a")){  //删除a变量  return null;     }     if (name.equals("b")){  //修改b变量名词为d,值为10  return super.visitField(access, "d", descriptor, signature, 10);     }     return super.visitField(access, name, descriptor, signature, value); }

 添加一个变量

 @Override public void visitEnd() {     super.visitEnd();     FieldVisitor fv = cv.visitField(Opcodes.ACC_PRIVATE, "e", "I", null, null);     if (fv != null) {  fv.visitEnd();     } }

3.4 visitAnnotation

作用:回调Class上的每一个注解

 @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {     return super.visitAnnotation(descriptor, visible); }

参数解释:

  • descriptor:注解的描述符
  • visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false 

四、MethodVisitor

        类似于ClassVisitor,MethodVisitor是对方法进行扫描,包括方法上的注解,方法内的所有代码。

        在本文中,我们使用的是继承于AdviceAdapter的类,其实它最终也是继承于MethodVisitor,相比于MethodVisitor,它有更多封装好的功能,方便我们对代码进行查看修改(具体可以看源码),所以这里主要介绍AdviceAdapter的用法。

当然,你也可以直接继承MethodVisitor使用。

4.1 onMethodEnter

        作用:方法进入时调用,可以在方法执行前插入代码

        用法:根据JVM指令,使用下列指令进行代码插件修改

4.2 onMethodExit

        作用:方法退出时调用,可以在方法执行后插入代码

        用法:根据JVM指令,使用下列指令进行代码插件修改

 4.3  invokeStatic

        作用:调用一个静态方法,也就是static。实际调用的还是MethodVisitor的visitMethodInsn

例如:Java代码,调用系统静态方法

 long start = System.currentTimeMillis();

对应的JVM字节码

    INVOKESTATIC java/lang/System.currentTimeMillis ()J    LSTORE 1

注意1:如果调用的方法有返回值,会自动压入栈顶!! 

注意2:Long和Double类型的值保存进局部变量表会占用2个位置,也就是两个slot,一个slot是32位。slot是局部变量表的单位

        用法如下:

     invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));     int startIndex = newLocal(Type.LONG_TYPE);     storeLocal(startIndex);     //上面两句等同于下面这一句,不推荐下面这种     storeLocal(1)

注意1:这里newLocal只是为了方便类统计局部变量表的索引,传Type_LONG_TYPE和随便一个值他的返回值都是一样的,影响的只会是下一个调用的人,所以说要传正确的值类型。

同理,invokeConstructor,invokeVirtual,invokeDynamic,invokeInterface调用的还是MethodVisitor的visitMethodInsn。

4.4 newLocal

        作用:创建一个局部变量的类型,类会自动加上类型的Size,返回值是局部变量表里的索引。

        用法如下: 

newLocal(Type.LONG_TYPE);

 提示:如果此时局部变量表是空的,第一次调用上面语句后,会返回0的索引,下一次再调用上面语句,就是返回2的索引,因为Long占用两个slot。

4.5 storeLocal

        作用:将栈顶的值保存到局部变量表的指令,实际调用的还是MethodVisitor的visitVarInsn,最好就是搭配newLocal方法使用

        例如上面的invokeStatic 例子,当调用完inkeStatic命令完,栈顶有值,就可以执行storeLocal命令。如下:

     invokeStatic(Type.getType("Ljava/lang/System;"), new Method("currentTimeMillis", "()J"));     int startIndex = newLocal(Type.LONG_TYPE);     storeLocal(startIndex);

 提示:也不一定要调用newLocal,不过就需要你手动计算索引,填入正确的索引

4.6  push

        作用:将给定的值压入栈顶,一般要搭配storeLocal使用,实际调用的还是MethodVisitor的visitInsn

        例如:Java代码,创建一个变量

 String a = "hhhh"; long b = 2L; double c = 2.22D;

        对应的JVM字节码

   L0    LINENUMBER 16 L0    LDC "hhhh"    ASTORE 1   L1    LINENUMBER 17 L1    LDC 2    LSTORE 2   L2    LINENUMBER 18 L2    LDC 2.22    DSTORE 4

        用法如下:

     push("hhhh");     storeLocal(newLocal(Type.CHAR_TYPE));     push(2L);     storeLocal(newLocal(Type.LONG_TYPE));     push(2.22);     storeLocal(newLocal(Type.LONG_TYPE));

问题:我试过push(1),但是写入Class里的内容却是Boolean,没搞懂,有知道的小伙伴欢迎评论。

4.7  getStatic

        作用:获取一个静态的实例变量,实际调用的还是MethodVisitor的visitFieldInsn

        例如:

getStatic(Type.getType("Ljava/lang/System;"), "out", Type.getType("Ljava/io/PrintStream;"));

同理,getFiled就是获取非静态的实例变量,但是这场景没见过。

相应的,putStatic和putFiled就是给静态实例变量赋值和非静态变量赋值。

提示:不是直接调用就能赋值的,赋值前栈顶需要有对应的变量和值。

4.8  newInstance

        作用:创建一个对象,并将其引用值压入栈顶,实际调用的还是MethodVisitor的visitTypeInsn

4.9  dup

        作用:从栈顶的对象赋值一个引用,并将其引用值压入栈顶,实际调用的还是MethodVisitor的visitInsn

        例如下面代码:

 StringBuilder stringBuilder = new StringBuilder();

          用法:

     //new 一个 StringBuilder();并将其引用值压入栈顶     newInstance(Type.getType("Ljava/lang/StringBuilder;"));     //复制栈顶元素(对象StringBuilder的引用)     dup();

4.10  visitLdcInsn

        作用:将传入的值从常量池中推送至栈顶

4.11  loadLocal

        作用:把局部变量表中的元素,压入栈顶

4.12  math

        作用:将栈顶的两个元素进行计算(例如:加减乘除等),并把结果值重新压入栈顶。

五、FieldVisitor

                类似于ClassVisitor,FieldVisitor是对变量的信息进行扫描,包括变量上的注解,变量的参数。

5.1 visitAnnotation

        作用:回调变量上的每一个注解

源代码:

public AnnotationVisitor visitAnnotation(String descriptor, boolean visible)

参数解释:

  • descriptor:注解的描述符
  • visible:如果注解是运行时(也就是注解上的@Retention(RetentionPolicy.RUNTIME))返回true,否则就是false 

六、代码优化

        可以看到,生成出来的Class文件对每个方法都进行了代码插入,这很明显不是我们需要的。解决方法如下:

第一种,进行方法过滤

 @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {     System.out.println("读到Class的一个方法visitMethod: " + name);     if (!name.equals("test")) {  return super.visitMethod(access, name, descriptor, signature, exceptions);     }     MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);     return new MyMethodVisitor(api, methodVisitor, access, name, descriptor); }

       这种虽然很方便,但是好像也太Low了。我们再来看第二种

第二种,注解方式

        很简单,创建一个注解(@ASMTarget),然后在对应需要插入代码的方法上写上我们注解。例如:

ASMTarget代码

@Retention(RetentionPolicy.CLASS)@Target({ElementType.METHOD, ElementType.FIELD,ElementType.ANNOTATION_TYPE})public @interface ASMTarget {}

提前在需要插入代码的方法上写上@ASMTarget

    @ASMTarget    public static void main(String[] args) throws InterruptedException { Thread.sleep(1234);    }

 然后我们看看MethodVisitor代码要如何修改,如下::

 boolean needInsertCode = false;//标记是否需要插入代码      @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {     if ("Lasm/ASMTarget;".equals(descriptor)) {  needInsertCode = true;     }     return super.visitAnnotation(descriptor, visible); }

 然后在onMethodEnter和onMethodExit进行拦截即可,代码如下。

 @Override protected void onMethodEnter() {     super.onMethodEnter();     if (!needInsertCode) {  return;     } @Override protected void onMethodExit(int opcode) {     super.onMethodExit(opcode);     if (!needInsertCode) {  return;     }

好了,本篇到此结束。

湖北工具网