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方法执行耗时。
-
创建Android工程,包名为com.example.asmdemo,在app/build.gradle文件中加入如下依赖:
implementation 'org.ow2.asm:asm:5.0.4' implementation 'org.ow2.asm:asm-commons:5.0.4'
-
编译项目,直接点击AndroidStudio菜单栏的锤子图标即可,在app/build/intermediates/javac/debug/classes目录下可以看到MainActivity.java文件被编译成MainActivity.class文件
-
为了简单起见,本篇中不会记录使用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);
-
在第三步中创建的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 Signature | Java Type |
---|---|
Z | boolean |
B | byte |
C | char |
S | short |
I | int |
J | long |
F | float |
D | double |
L | fully-qualified-class ;fully-qualified-class |
[ type | type[] |
( arg-types ) ret-type | method 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编程——Gradle插件+TransformAPI+字节码插桩实战
Android AOP编程——Gradle插件+TransformAPI+字节码插桩实战