版权声明:本文为CSDN博主「孟芳芳」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/zenmela2011…
(如有侵权,请联系删除)
1.ASM
ASM是一个字节码操作框架,可用来动态生成字节码或者对现有的类进行增强。ASM可以直接生成二进制的class字节码,也可以在class被加载进虚拟机前动态改变其行为,比如方法执行前后插入代码、添加成员变量、修改父类、添加接口等等。
插桩就是将一段代码插入或者替换原本的代码。字节码插桩就是在编写的java源码编译成class字节码后,在Android下生成dex之前修改class文件,修改或者增强原有代码逻辑的操作。
编写好的代码经过编译后的class文件如下:
然后经过字节码插桩后如下:
ASM框架就是操作java字节码的框架之一,按照class文件的格式,解析、修改、生成class,可以动态生成类或者增强现有类的功能。热修复、systrace都使用了字节码插桩。
这跟gson很像,因为JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。
2.ASM的使用
①引入ASM依赖:
使用testImplementation引入,表示只能在Java的单元测试中使用这个框架,对Android中的依赖关系没有任何影响。
注:AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。
②准备待插桩Class
在 test/java下面创建一个Java类:
InjectTest.java:
public class InjectTest {
public static void main(String\[] args) throws InterruptedException{
Thread.sleep(1000);
}
}
由于我们操作的是字节码插桩,也就是class文件,所以需要进入 test/java下面使用 javac对这个java类进行编译生成对应的class文件,具体操作是:在Android studio底部Terminal窗口,通过cd进入到test/java目录下,然后执行以下命令:
javac com/demo/test/InjectTest.java
执行上面的命令编译后,就会在test/java下面生成对应的InjectTest.class文件,这个class文件就是待插桩的文件。
③执行插桩
待插桩的class文件准备好了,接下来写个单元测试来执行插桩吧。利用ASM向main方法中插入一开始图中的记录函数执行时间的日志输出。
在test/java下新建ASMUnitTest.java文件:
public class ASMUnitTest {
@Test
public void test() throws IOException {
//1 准备待分析的class
FileInputStream fis = new FileInputStream("src/test/java/com/demo/test/InjectTest.class");
//2 执行分析与插桩
ClassReader cr = new ClassReader(fis); // ClassReader是class字节码的读取与分析引擎
ClassWriter cw = new ClassWriter( ClassWriter.COMPUTE_FRAMES); // 写出器, COMPUTE_FRAMES表示自动计算栈帧和局部变量表的大小
cr.accept(new MyClassVisitor(cw), ClassReader.EXPAND_FRAMES); //执行分析,处理结果写入cw, EXPAND_FRAMES表示栈图以扩展格式进行访问
//3、获得执行了插桩之后的字节码数据
byte[] newClassBytes = cw.toByteArray();
FileOutputStream fos = new FileOutputStream("src/test/java/com/demo/test/InjectTest1.class");
fos.write(newClassBytes);
fos.close();
}
}
首先获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java目录下的InjectTest1.class文件。其中关键点就在于第2步中,即如何进行插桩:
把class数据交给ClassReader进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数MyClassVisitor。
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor(ClassVisitor cv) {
super(Opcodes.ASM7, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,String[] exceptions) {
System.out.println("方法:" + name + " 签名:" + desc);
MethodVisitor mv = super.visitMethod( access, name, desc, signature, exceptions);
return new MyMethodVisitor(api,mv, access, name, desc);
}
}
分析结果通过MyClassVisitor获得。一个类中会存在方法、注解、属性等,因此ClassReader将会调用MyClassVisitor中对应的visitMethod、 visitAnnotation、 visitField这些 visitXX方法。
我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在MethodVisitor中进行分析与处理。
//AdviceAdapter: 子类,对MethodVisitor进行了扩展, 能更加轻松的进行方法分析
public class MyMethodVisitor extends AdviceAdapter {
protected MyMethodVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
}
private int start;
@override
protected void onMethodEnter() {
super.onMethodEnter();
//进入方法时,插入 long l = System.currentTimeMillis();
invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); //执行System.currentTimeMillis();
start = newLocal(Type.LONG_TYPE); //创建本地LONG类型变量
storeLocal(start); //将上一步方法执行结果保存到创建的本地变量中
}
@override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode);
//退出方法时,插入 long e = System.currentTimeMillis();
invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J"));
int end = newLocal(Type.LONG_TYPE);
storeLocal(end);
//退出方法时,插入System.out.println( "execute" + (e - l) + "ms.");
getStatic(Type.getType( "Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;")); //执行System.out
newInstance(Type.getType( "Ljava/lang/StringBuilder;")); // 执行new StringBuilder分配内存
dup(); //dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
invokeConstructor(Type.getType( "Ljava/lang/StringBuilder;"),new Method("<init>","()V")); //调用StringBuilder的构造方法
visitLdcInsn("execute:");
invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;) Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法
loadLocal(end); // 加载方法结束的时间
loadLocal(start); //加载方法开始的时间
math(SUB,Type.LONG_TYPE); //减法
invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法
invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;")); // 调用StringBuilder的toString方法
invokeVirtual(Type.getType( "Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V")); // 调用StringBuilder的println方法
}
@override
public AnnotationVisitor visitAnnotation(String description, boolean visible) { //获取注解
System.out.println("方法名为:" + getName() + "对应的注解为:" + description);
return super.visitAnnotation(description, visible);
}
}
MyMethodVisitor继承自AdviceAdapter,其实就是MethodVisitor的子类, AdviceAdapter封装了指令插入方法,更为直观与简单。
上述代码中onMethodEnter在进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入 longs=System.currentTimeMillis();。在 onMethodExit中即方法最后插入输出代码。
onMethodEnter和onMethodExit方法里的代码怎么写?其实onMethodEnter方法里就是 long s = System.currentTimeMillis();这句代码的相对的指令。而onMethodExit方法里就是long e = System.currentTimeMillis(); System.out.println("execute" + (e - s) + "ms.");这两句代码相对应的指令。
我们可以先写一份代码:
void test(){
//进入方法时插入的代码
long s = System.currentTimeMillis();
// 退出方法时插入的代码
long e = System.currentTimeMillis();
System.out.println("execute" + (e - s) + "ms.");
}
然后使用javac编译成class再使用javap-c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。
安装完成之后,可以在需要插桩的类源码中点击右键:
点击ASM Bytecode Viewer之后会弹出
所以第20行代码: longs=System.currentTimeMillis();会包含两个指令: INVOKESTATIC与 LSTORE。
再回到 onMethodEnter方法中:
@override
protected void onMethodEnter() {
super.onMethodEnter();
//invokeStatic指令,调用静态方法
invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J")); // 相当于java中System.currentTimeMillis();这一句代码
//用一个本地变量接收上一步的执行结果
int start = newLocal(Type.LONG_TYPE); //start表示当前long类型的本地变量的索引
storeLocal(start); //store指令,将方法执行结果从操作数栈存储到局部变量
}
invokeStatic指令涉及到几个名词:
①类型描述符
Java代码中的类型,在字节码中有相应的表示协议:
Java Type Type description
boolean Z
char C
byte B
short S
int I
float F
long J
double D
object Ljava/lang/Object;
int[] [I Object\[]\[] \[\[Ljava/lang/Object; void V 引用类型 L (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。
同样,onMethodExit也根据指令去编写代码:
onMethodExit中需要插入的代码在ASMByteCode中的格式如下:
对应的代码如下:
invokeStatic(Type.getType( "Ljava/lang/System;"), new Method( "currentTimeMillis", "()J"));
```
int end = newLocal(Type.LONG\_TYPE);
storeLocal(end);
getStatic(Type.getType( "Ljava/lang/System;"),"out",Type.getType("Ljava/io" +"/PrintStream;")); //执行System.out
newInstance(Type.getType( "Ljava/lang/StringBuilder;")); // 执行new StringBuilder分配内存
dup(); //dup压入栈顶,让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
invokeConstructor(Type.getType( "Ljava/lang/StringBuilder;"),new Method("<init>","()V")); //调用StringBuilder的构造方法
visitLdcInsn("execute:");
invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;) Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法
loadLocal(end); // 加载方法结束的时间
loadLocal(start); //加载方法开始的时间
math(SUB,Type.LONG\_TYPE); //减法
invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("append","(J)Ljava/lang/StringBuilder;")); // 调用StringBuilder的append方法
invokeVirtual(Type.getType( "Ljava/lang/StringBuilder;"),new Method("toString","()Ljava/lang/String;")); // 调用StringBuilder的toString方法
invokeVirtual(Type.getType( "Ljava/io/PrintStream;"),new Method("println","(Ljava/lang/String;)V")); // 调用StringBuilder的println方法
最终执行完插桩之后,就可以获得修改后的class数据。
3.有选择性的插桩
现在存在一个问题,就是待插桩的class里所有的方法都被加入了插桩的代码。
插桩后生成的的InjectTest.class如下:
public class InjectTest {
public InjectTest() {
long var1 = System.currentTimeMillis();
long var3 = System.currentTimeMillis();
System.out.println("execute:" + (var3 - var1) + "ms.";
}
public static void main(String[] var0) throws InterruptedException {
long var1 = System.currentTimeMillis();
Thread.sleep(1000L);
long var3 = System.currentTimeMillis();
System.out.println("execute:" + (var3 - var1) + "ms.";
}
}
如果只想在main方法里插桩,而不想在构造方法里插桩,这时候可以使用注解。
①创建注解类
新建ASMTest类:
ASMTest.java:
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface ASMTest{
}
②在需要插桩的方法上面添加注解
InjectTest.java:
public class InjectTest {
@ASMTest
public static void main(String[] var0) throws InterruptedException {
Thread.sleep(1000L);
}
public void aa() { //新增一个方法,没有添加注解,因此不会执行插桩代码
}
}
接下来修改MyMethodVisitor类,在MyMethodVisitor里重写visitAnnotation方法:
public class MyMethodVisitor extends AdviceAdapter {
……
boolean inject = false;
@override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
if("Lcom/demo/test/ASMTest;".equals( descriptor) {
inject = true;// 这个方法的注解为Lcom/demo/test/ASMTest;的时候,将inject置为true表示需要插桩
}
return super.visitAnnotation(descriptor, visible);
}
@override
protected void onMethodEnter(){
if(!inject) {
return;
}
……
}
@override
protected void onMethodExit(int opcode){
if(!inject) {
return;
}
……
}
}
通过visitAnnotation方法可以判断每个方法的注解,然后在每个方法的onMethodEnter和onMethodExit里根据是否有这个注解来判断是否需要执行插桩。
修改后,插桩生成的的InjectTest.class如下:
public class InjectTest {
public static void main(String\[] var0) throws InterruptedException {
long var1 = System.currentTimeMillis();
Thread.sleep(1000L);
long var3 = System.currentTimeMillis();
System.out.println("execute:" + (var3 - var1) + "ms.";
}
public void aa() {
}
}
这样,就实现了只在加了注解的main方法里插入了代码。
4.Android的实现
在Android中执行插桩,第一个问题就是如何获得所有编译好的class文件。
首先看一下Android工程的构建过程:
①Android Resources–>通过aapt–>R.java
②aidl Files–>通过aidl–>java interface
③(R.java、Android Resouce code、java interface)–>java compile–>.class Files
④(.class Files、3rd Party Libraries and class Files)–>dex 编译器–>.dex Files
⑤(dex Files、Other Resources)–>Apk Builder–>Android Package(.apk)–>jar signer–>Signed Apk
字节码操作框架的作用在于生成或者修改class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的class才需要打包进入APK中。它的工作时机在上图Android打包流程中的生成Class之后,打包dex之前。
要获得所有编译好的class文件,这里需要用到Transform。
Transform是Android 官方插件提供给开发者在项目构建阶段由class到dex转换之前修改class文件的一套API。目前典型的应用场景就是字节码插桩。通过Transform可以得到所有的class字节码,自定义的Transfrom会先执行,执行的结果做为参数进行传递。
①新建一个插件类
public class APMPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
BaseExtension android = project.getExtensions().getByType(BaseExtension.class);
//注册一个Transform
android.registerTransform(new ASMTransfrom());
}
}
android 插件能够获得所有的class,并通过接口的形式暴露出来。
②创建一个ASM
public class ASMTransform extends Transform {
@Override
public String getName() {
return "ms_asm";
}
// 处理所有class
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
//范围仅仅是主项目所有的类
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.PROJECT_ONLY;
}
//不使用增量
@Override
public boolean isIncremental() {
return false;
}
//android插件将所有的class通过这个方法告诉给我们
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
//清理上次缓存的文件信息
outputProvider.deleteAll();
//得到所有的输入
Collection<TransformInput> inputs = transformInvocation.getInputs();
for (TransformInput input : inputs) {
// 处理class目录
for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
// 直接复制输出到对应的目录
String dirName = directoryInput.getName();
File src = directoryInput.getFile();
System.out.println("输出class文件:" + src);
String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
//得到输出class文件的目录
File dest = outputProvider.getContentLocation(dirName + md5Name,
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
//执行插桩操作
processInject(src, dest);
}
// 处理jar(依赖)的class
for (JarInput jarInput : input.getJarInputs()) {
String jarName = jarInput.getName();
File src = jarInput.getFile();
System.out.println("输出jar包:" + src);
String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
if (jarName.endsWith(".jar")) {
jarName = jarName.substring(0, jarName.length() - 4);
}
File dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
FileUtils.copyFile(src, dest);
}
}
}
private void processInject(File src, File dest) throws IOException {
String dir = src.getAbsolutePath();
FluentIterable<File> allFiles = FileUtils.getAllFiles(src);
for (File file : allFiles) {
//得到文件输入流
FileInputStream fis = new FileInputStream(file);
//得到字节码Reader
ClassReader cr = new ClassReader(fis);
//得到写出器
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//将注入的时间信息,写入
cr.accept(new ClassInjectTimeVisitor( cw,file.getName()), ClassReader.EXPAND_FRAMES);
byte[] newClassBytes = cw.toByteArray();
String absolutePath = file.getAbsolutePath();
String fullClassPath = absolutePath.replace(dir, "");
//将得到的字节码信息 写如输出目录
File outFile = new File(dest, fullClassPath);
FileUtils.mkdirs(outFile.getParentFile());
FileOutputStream fos = new FileOutputStream(outFile);
fos.write(newClassBytes);
fos.close();
}
}
}
继承Transform 重写父类的方法(com.android.build.api.transform.Transform)是这个别弄错了。
getName() 返回transfrom的方法名,这个随便定义。
getInputTypes() 得到需要处理的内容类型,TransformManager.CONTENT\_CLASS 这个表示字节码。
getScopes() 返回的是处理范围,比如是整个项目还是仅仅主app等。
isIncremental() 是否增量。
transform() 这个方法会回调我们需要的所有类信息。
③创建字节码注入时间方法器
public class ClassInjectTimeVisitor extends ClassVisitor {
//得到类名
private String mClassName;
public ClassInjectTimeVisitor(ClassVisitor cv, String fileName) {
super(Opcodes.ASM5, cv);
mClassName = fileName.substring(0, fileName.lastIndexOf("."));
}
/**
* 访问方法
* @param access 方法的访问flag
* @param name 方法名
* @param desc 描述信息
* @param signature 签名信息
* @param exceptions
* @return
*/
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return new MethodAdapterVisitor(mv, access, name, desc, mClassName);
}
}
visitMethod这个方法负责拦截所有的方法,并初始化一个方法适配器MethodAdapterVisitor。
MethodAdapterVisitor,负责具体的插桩代码逻辑。
④准备需要插入的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.");
}
插入方法耗时统计:
需要在方法开始插入这行代码:long start = System.currentTimeMillis();
结尾处插入:
long end = System.currentTimeMillis();
System.out.println(“execute:”+(end-start)+" ms.");
将上面的java信息转成ASM Bytecode,以便方法的进行插桩注入。
{
methodVisitor = classWriter.visitMethod(ACC\_PUBLIC | ACC\_STATIC, "main", "(\[Ljava/lang/String;)V", null, new String\[]{"java/lang/InterruptedException"});
methodVisitor.visitCode();
//long start = System.currentTimeMillis();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(7, label0);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 1);
// 下面是执行的 Thread.sleep(1_000);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(9, label1);
methodVisitor.visitLdcInsn(new Long(1000L));
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Thread", "sleep", "(J)V", false);
//下面是执行的 long end = System.currentTimeMillis();和System.out.println("execute:"+(end-start)+" ms.");
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLineNumber(11, label2);
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
methodVisitor.visitVarInsn(LSTORE, 3);
Label label3 = new Label();
methodVisitor.visitLabel(label3);
methodVisitor.visitLineNumber(12, label3);
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", "<init>", "()V", false);
methodVisitor.visitLdcInsn("execute:");
methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
methodVisitor.visitVarInsn(LLOAD, 3);
methodVisitor.visitVarInsn(LLOAD, 1);
methodVisitor.visitInsn(LSUB);
methodVisitor.visitMethodInsn( INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
methodVisitor.visitLdcInsn(" ms.");
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);
//其他内容不需要处理
Label label4 = new Label();
methodVisitor.visitLabel(label4);
methodVisitor.visitLineNumber(13, label4);
methodVisitor.visitInsn(RETURN);
Label label5 = new Label();
methodVisitor.visitLabel(label5);
methodVisitor.visitLocalVariable("args", "[Ljava/lang/String;", null, label0, label5, 0);
methodVisitor.visitLocalVariable("start", "J", null, label1, label5, 1);
methodVisitor.visitLocalVariable("end", "J", null, label3, label5, 3);
methodVisitor.visitMaxs(6, 5);
methodVisitor.visitEnd();
}
分别在方法的执行最前面和最后面插入相关的代码逻辑就好,这部分是辅助内容。
⑤创建方法访问者适配器
public class MethodAdapterVisitor extends AdviceAdapter {
private String mClassName;
private String mMethodName;
private boolean mInject;
private int mStart, mEnd;
protected MethodAdapterVisitor(MethodVisitor mv, int access, String name, String desc, String className) {
super(Opcodes.ASM5, mv, access, name, desc);
mMethodName = name;
this.mClassName = className;
}
//拦截注解方法
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
if ("Lcom/meishe/ms_asminject/MSTim eAnalysis;".equals(desc)) {
mInject = true;
}
return super.visitAnnotation(desc, visible);
}
//方法进入
@Override
protected void onMethodEnter() {
if (mInject) {
//执行方法currentTimeMillis 得到startTime
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mStart = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, mStart);
}
}
//方法结束
@Override
protected void onMethodExit(int opcode) {
if (mInject) {
//执行 currentTimeMillis 得到end time
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
mEnd =newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, mEnd);
//得到静态成员 out
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
//new //class java/lang/StringBuilder
mv.visitTypeInsn(NEW, "java/lang/StringBuilder");
//引入类型 分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法
mv.visitInsn(DUP);
//执行init方法 (构造方法)
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
//把常量压入栈顶
mv.visitLdcInsn("execute "+ mMethodName +" :");
//执行append方法,使用栈顶的值作为参数
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
// 获得存储的本地变量
mv.visitVarInsn(LLOAD, mEnd);
mv.visitVarInsn(LLOAD, mStart);
// lsub 减法指令
mv.visitInsn(LSUB);
//把减法结果append
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
//拼接常量
mv.visitLdcInsn(" ms.");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
//执行StringBuilder 的toString方法
mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
//执行println方法
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
}
}
继承AdviceAdapter,并重写相关的方法(org.objectweb.asm.commons.AdviceAdapter)是这个别继承错了。
visitAnnotation() 这个会输出方法拥有的注解信息,这里只对我们添加注解的方法进行注入操作。
onMethodEnter() 方法进入的时候会回调,在这里插入long start = System.currentTimeMillis();
onMethodExit(int opcode) 方法结束的时候回调,在这里插入:
long end = System.currentTimeMillis();
System.out.println(“execute:”+(end-start)+" ms.");
⑥上面用的注解
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface MSTimeAnalysis {
}
声明一个编译器注解即可,由于上边用到了,贴出来。
这样就完成了字节码插桩的全部工作。
⑧进行测试工作
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testApp();
}
@MSTimeAnalysis
public void testApp(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
声明一个testApp()方法,增加@MSTimeAnalysis方法,并调用。运行就可以看到输出:
2022-05-03 11:08:10.824 27788-27788/com.meishe.ms\_asminject I/System.out: execute testApp :1000 ms.
在build/intermediates/transforms/ms\_asm/debug/1/com/meishe/ms\_asminject/MainActivity.class可以查看编译生成字节码:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
this.setContentView(2131427356);
this.testApp();
}
@MSTimeAnalysis
public void testApp() {
long var1 = System.currentTimeMillis();
try {
Thread.sleep(1000L);
} catch (InterruptedException var6) {
var6.printStackTrace();
}
long var4 = System.currentTimeMillis();
System.out.println("execute testApp :" + (var4 - var1) + " ms.");
}
证明确实插桩成功了。