Android AOP编程——ASM基础

Posted yubo_725

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android AOP编程——ASM基础相关的知识,希望对你有一定的参考价值。

前言

在前面几篇博文中我记录了android AOP编程使用的一些库,主要是AspectJ和Javassist:

AspectJ和Javassist都能直接操作Class文件,本篇记录的是ASM,也是一个可以操作Java字节码的库,它的使用可能更复杂一些,本篇只做最基本的使用方法记录。

什么是ASM

ASM的官方地址是:https://asm.ow2.io/

官方对ASM的解释如下:

ASM是一个通用的 Java 字节码操作和分析框架。它可用于直接以二进制形式修改现有类或动态生成类。ASM 提供了一些常见的字节码转换和分析算法,可以从中构建自定义的复杂转换和代码分析工具。ASM 提供与其他 Java 字节码框架类似的功能,但侧重于 性能。因为它被设计和实现得尽可能小和尽可能快,所以它非常适合在动态系统中使用(但当然也可以以静态方式使用,例如在编译器中)。

另外,这个文档中有ASM详细用法的指南:https://asm.ow2.io/developer-guide.html

ASM使用方法

下面会以一个例子说明ASM的用法,该例子通过ASM操作字节码,修改Android工程MainActivity的onCreate方法,在该方法开始和结束记录时间并输出onCreate方法执行耗时。

  1. 创建Android工程,包名为com.example.asmdemo,在app/build.gradle文件中加入如下依赖:

    implementation 'org.ow2.asm:asm:5.0.4'
    implementation 'org.ow2.asm:asm-commons:5.0.4'
    
  2. 编译项目,直接点击AndroidStudio菜单栏的锤子图标即可,在app/build/intermediates/javac/debug/classes目录下可以看到MainActivity.java文件被编译成MainActivity.class文件

  3. 为了简单起见,本篇中不会记录使用Gradle插件和TransformAPI直接修改class字节码,而是使用单元测试的代码完成对MainActivity.class文件的修改,下面在app/src/test/java/下创建TestASM类,该类代码如下:

    package com.example.asmdemo;
    
    import org.junit.Test;
    import org.objectweb.asm.*;
    import org.objectweb.asm.commons.AdviceAdapter;
    
    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    
    public class TestASM 
    
        @Test
        public void test() throws Exception 
            // 要操作的class源文件,这里换成你本机的路径
            String originClzPath = "/Users/xxx/IdeaProjects/ASMDemo/app/build/intermediates/javac/debug/classes/com/example/asmdemo/MainActivity.class";
            FileInputStream fis = new FileInputStream(originClzPath);
            // ClassReader是ASM提供的读取字节码的工具
            ClassReader classReader = new ClassReader(fis);
            // ClassWriter是ASM提供的写入字节码的工具
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
            // 自定义类访问器,在其中完成对某个方法的字节码操作
            MyClassVisitor myClassVisitor = new MyClassVisitor(Opcodes.ASM5, classWriter);
            // 调用ClassReader的accept方法开始处理字节码
            classReader.accept(myClassVisitor, ClassReader.EXPAND_FRAMES);
            // 操作后的class文件写入到这个文件中,为了方便对比,这里创建了MainActivity2.class
            String destPath = "/Users/xxx/IdeaProjects/ASMDemo/app/build/intermediates/javac/debug/classes/com/example/asmdemo/MainActivity2.class";
            // 通过ClassWriter拿到处理后的字节码对应的字节数组
            byte[] bytes = classWriter.toByteArray();
            FileOutputStream fos = new FileOutputStream(destPath);
            // 写文件
            fos.write(bytes);
            // 关闭文件流
            fos.close();
            fis.close();
        
    
        class MyClassVisitor extends ClassVisitor 
    
            public MyClassVisitor(int api, ClassVisitor cv) 
                super(api, cv);
            
    
            @Override
            public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 
                super.visit(version, access, name, signature, superName, interfaces);
                // 访问类时会调用该方法
                // visit: name = com/example/asmdemo/MainActivity
                System.out.println("visit: name = " + name);
            
    
            @Override
            public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 
                // 访问类中的方法时会调用visitMethod
                // visitMethod: name = <init> // 代表构造方法
                // visitMethod: name = onCreate
                System.out.println("visitMethod: name = " + name);
                if ("onCreate".equals(name)) 
                    MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions);
                    return new MyAdviceAdapter(Opcodes.ASM5, methodVisitor, access, name, desc);
                
                return super.visitMethod(access, name, desc, signature, exceptions);
            
        
    
        class MyAdviceAdapter extends AdviceAdapter 
    
            private int startTimeId;
    
            protected MyAdviceAdapter(int api, MethodVisitor mv, int access, String name, String desc) 
                super(api, mv, access, name, desc);
            
    
            /**
             * 这个方法的目的是为了在onCreate方法开始处插入如下代码:
             * long v = System.currentTimeMillis();
             */
            @Override
            protected void onMethodEnter() 
                super.onMethodEnter();
                // 在方法开始处调用
                // 创建一个long类型的本地变量
                startTimeId = newLocal(Type.LONG_TYPE);
                // 调用System.currentTimeMillis()方法
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                // 将上一步中的结果保存到startTimeId指向的long类型变量中(不是保存到startTimeId)
                mv.visitIntInsn(LSTORE, startTimeId);
            
    
            /**
             * 这个方法的目的是在onCreate方法结束的地方插入如下代码:
             * long end = System.currentTimeMillis();
             * long delta = end - start;
             * System.out.println("execute onCreate() use time: " + delta);
             */
            @Override
            protected void onMethodExit(int opcode) 
                super.onMethodExit(opcode);
                // 在方法结束时调用
                // 创建一个long类型的本地变量
                int endTimeId = newLocal(Type.LONG_TYPE);
                // 调用System.currentTimeMillis()方法
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
                // 将上一步中的结果保存到endTimeId指向的long类型变量中(不是保存到endTimeId)
                mv.visitIntInsn(LSTORE, endTimeId);
                // 创建一个long类型的本地变量,deltaTimeId为这个变量的ID
                int deltaTimeId = newLocal(Type.LONG_TYPE);
                // 加载endTimeId指向的long类型的变量
                mv.visitIntInsn(LLOAD, endTimeId);
                // 加载startTimeId指向的long类型变量
                mv.visitIntInsn(LLOAD, startTimeId);
                // 将上面两个变量做减法(endTimeIdVal - startTimeIdVal)
                mv.visitInsn(LSUB);
                // 将减法的结果存在deltaTimeId指向的变量中
                mv.visitIntInsn(LSTORE, deltaTimeId);
                // 调用System静态方法out
                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                // 创建StringBuilder对象
                mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
                // 复制栈顶数值并将复制值压入栈顶
                mv.visitInsn(DUP);
                // 调用StringBuilder构造方法初始化
                mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
                // 将字符串推到栈顶
                mv.visitLdcInsn("execute onCreate() use time: ");
                // 调用StringBuilder的append方法
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
                // 加载deltaTimeId指向的long类型数据
                mv.visitVarInsn(LLOAD, deltaTimeId);
                // 调用StringBuilder的append方法
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
                // 调用StringBuilder的toString方法
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
                // 调用System.out的println方法
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            
        
    
    
    
    
  4. 在第三步中创建的TestASM类的test方法上鼠标右键–Run 'TestASM.test'即可执行我们使用@Test注解的单元测试方法,执行完毕之后,可以看到控制台有一些输出,主要是访问类和访问类中的方法时我们打印的name值,另外,在上面MainActivity.class同级目录下生成了新的MainActivity2.class文件,使用AndroidStudio打开该文件,可以看到源码如下:

    上图左侧之所以会在MainActivity2.class下再显示MainActivity,是因为文件名为MainActivity2而其中的Java类还是MainActivity,本例子中主要是为了对比使用ASM库操作字节码前后文件差异所以使用了新的文件名。

通过上面的代码,可以发现ASM的使用还是比较复杂的,我们为了生成几行代码,用了十多行代码去生成,这里面有一些知识点需要注意一下。

JVM指令说明

上面的代码中,在使用ASM的API生成Java代码时,用到了一些JVM指令,下面整理如下:

指令说明
INVOKESTATIC调用静态方法
LSTORE将栈顶long型数值存入指定本地变量
LLOAD将指定的long型本地变量推送至栈顶
LSUB将栈顶两long型数值相减并将结果压入栈顶
GETSTATIC获取指定类的静态域,并将其值压入栈顶
NEW创建一个对象,并将其引用值压入栈顶
DUP复制栈顶数值并将复制值压入栈顶
INVOKESPECIAL调用超类构造方法,实例初始化方法,私有方法
INVOKEVIRTUAL调用实例方法

更多JVM的指令可以查看这篇文章:JVM指令集整理

Java Class文件描述符

Class文件描述符主要用于描述字段的数据类型、方法的参数列表和返回值。

基本数据类型(byte char double float int long short boolean)以及代表无返回值的void类型都用一个大写字符( Type Signature)来表示

对象类型则用字符“L”加对象的全限定名来表示,一般对象类型末尾都会加一个“;”来表示全限定名的结束。

Type SignatureJava Type
Zboolean
Bbyte
Cchar
Sshort
Iint
Jlong
Ffloat
Ddouble
Lfully-qualified-class ;fully-qualified-class
[ typetype[]
( arg-types ) ret-typemethod type

下面举例说明:

Java签名
String[][java\\lang/String;
int[][I;
void init()()V
void setText(String s)(Ljava/lang/String)V;
java.lang.String toString()()Ljava/lang/String;
long f(int n, String s, int[] arr)f(ILjava/lang/String;[I)J

通过以上例子可以发现ASM的使用更多的是对字节码的原始操作,不像AspectJ或者Javassist,直接可以通过Java代码来完成对字节码的操作。在Intellij IDEA中我们可以安装ASM插件帮助我们来完成代码编写,这里我是在插件应用市场直接搜索的ASM,如下图:

我安装的是ASM Bytecode Viewer这个插件,虽然上面一个下载量更高,但是很久没更新了。
安装完插件后,我们可以在app/build/intermediates/javac/debug/classes目录下找到MainActivity.class,然后鼠标右键–ASM Bytecode Viewer,就可以在IDE的右侧打开字节码视图查看了,如下图所示:

其中的每一行的指令,跟使用ASM库的API来操作字节码,是一一对应的。

源码

本篇的源码放在GitHub上:https://github.com/yubo725/asm-demo

参考

以上是关于Android AOP编程——ASM基础的主要内容,如果未能解决你的问题,请参考以下文章

Android AOP编程——ASM基础

Android AOP编程——Gradle插件+TransformAPI+字节码插桩实战

Android AOP编程——Gradle插件+TransformAPI+字节码插桩实战

Android AOP编程——Gradle插件+TransformAPI+字节码插桩实战

Android——面向AOP编程

Android——面向AOP编程