本文大纲:

  1. ams是做什么的
  2. asm使用

1.ASM是做什么的?

简单来说,asm是用来进行字节码插桩的。什么是字节码插桩? 字节码插桩就是修改节码文件(.class). 如同 gson框架是用来做操json数据结构的,那么asm框架就是用来操作.class文件结构的。

那么这有什么用处呢?

这个是一个很强大而且很高级的功能。我们可能知道反射hook技术,aspectJ技术,动态代理技术,我们一一来看他们的特点:

  • 反射hook: 在运行时通过反射某些属性或方法来修改原有流程。局限性:需要能够找到合适的hook点。
  • aspectJ:通过添加注解的方式在运行时或编译时进行插入代码逻辑。局限:需要添加注解,有一些侵入性。
  • 动态代理:在运行时代理一切实现某个接口的类,并在方法中可以添加自己的代码逻辑。局限:只能代理接口,并且只能修改接口声明的方法。

那么现在假如我们有这样一个需求,在每个方法中记录方法的执行时间。代码如下:

public class InjectTest {public static void main(String arg[]) {}public void sayGo(){System.out.println("大家好aaaaaaaaaaaa");}
}
  • 方案一:可以使用aspectJ,定义一个注解,并在注解处理器中写入记录时间的逻辑。 然后将注解放到方法上。 的确,在我们了解字节码插桩之前,这也许是最好的方法。
  • 方案二:使用asm字节码插桩。首先获取编译后的InjectTest.class文件,进行分析,在每个方法时中加入记录时间的逻辑,然后重新生成新的InjectTest.class文件。

可以发现,使用字节码插桩这种方式,在编译成class文件之前的逻辑我们不用考虑也不用修改,仅仅对class文件进行了修改,假如某天要去掉这个功能,那么我们直接注释掉插桩代码就ok了,而使用AspectJ方式要手动去掉注解。

还有很多场景能用到字节码插桩,比如第三方库的修改,尤其是当你无法拿到源码只能拿到字节码的时候。还有android4.4一下热修复时 CLASS_ISPREVERIFIED的问题 https://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&mid=400118620&idx=1&sn=b4fdd5055731290eef12ad0d17f39d4a。 总之,字节码插桩不仅是一个逼格很高的技术,还是一个十分实用的技术。

2.字节码插桩框架ASM的使用:

本次我们来完成一个字节码插桩的测试,目标就是对下面一段代码 在每个方法中插入计算方法执行时间的逻辑:

public class InjectTest {public static void main(String arg[]) {System.out.println("今晚上山打老虎");try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}public void sayGo(){System.out.println("大家好aaaaaaaaaaaa");}
}

2.1.准备工作:

①准备待插桩的class

因为asm操作的class文件,所以先将.java编译成.class

命令行执行命令: javac -encoding UTF-8 InjectTest.java

此时生成 D:\projects\DragonForest\ASMStudy\app\src\test\java\com\example\asmstudy\InjectTest.class

②引入asm依赖:

在AS中引入ASM

ASM可以直接从 jcenter()仓库中引入,所以我们可以进入:https://bintray.com/进行搜索

点击图中标注的工件进入,可以看到最新的正式版本为:7.1。

因此,我们可以在AS中加入:

同时,需要注意的是:我们使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。

AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。

2.2.使用asm框架,执行插桩。

在此之前,先来介绍ASM框架中几个重要的类和方法:

ClassReader 类

这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法

ClassWriter 类

ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

ClassVisitor 抽象类

  • void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 , 泛型信息 , 继承的父类 , 实现的接口)
  • AnnotationVisitor visitAnnotation(String desc, boolean visible) 该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在 JVM 中可见)。
  • FieldVisitor visitField(int access, String name, String desc, String signature, Object value)该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
  • MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 , 泛型信息 , 抛出的异常)
  • void visitEnd() 该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法

MethodVisitor & AdviceAdapter

MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。

AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。其中比较重要的几个方法如下:

  • void visitCode():表示 ASM 开始扫描这个方法
  • void onMethodEnter():进入这个方法
  • void onMethodExit():即将从这个方法出去
  • void onVisitEnd():表示方法扫码完毕

FieldVisitor 抽象类

FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍

ASM 操作流程

  1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
  2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
  3. 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor 对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了

接下来进行编码:

首先在android 项目的 test 下创建 AsmUtil类:

/*** 1、准备待分析的class*/FileInputStream fis = new FileInputStream("xxxxx/test/java/InjectTest.class");/*** 2、执行分析与插桩*///class字节码的读取与分析引擎ClassReader cr = new ClassReader(fis);// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问cr.accept(new ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);/*** 3、获得结果并输出*/byte[] newClassBytes = cw.toByteArray();File file = new File("xxx/test/java2/");file.mkdirs();FileOutputStream fos = new FileOutputStream("xxx/test/java2/InjectTest.class");fos.write(newClassBytes);fos.close();

重点在于第2步,把class数据交给 ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数 ClassAdapterVisitor。

public class ClassAdapterVisitor extends ClassVisitor {public ClassAdapterVisitor(ClassVisitor cv) {super(Opcodes.ASM7, cv);}@Overridepublic 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 MethodAdapterVisitor(api,mv, access, name, desc);}
}

分析结果通过 ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此 ClassReader会将调用 ClassAdapterVisitor中对应的 visitMethod、 visitAnnotation、 visitField这些 visitXX方法。

我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中我们返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在 MethodVisitor中进行分析与处理

package com.enjoy.asminject.example;import com.enjoy.asminject.ASMTest;import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;/*** AdviceAdapter: 子类* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析*/
public class MethodAdapterVisitor extends AdviceAdapter {// 是否需要插桩private boolean inject;protected MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {super(api, methodVisitor, access, name, descriptor);}/*** 分析方法上面的注解* <p>* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩* 没使用,就不管了。** @param desc* @param visible* @return*/@Overridepublic AnnotationVisitor visitAnnotation(String desc, boolean visible) {// 如果方法加上了ASMTest.class的注解,才会执行插桩if (Type.getDescriptor(ASMTest.class).equals(desc)) {System.out.println(desc);inject = true;}return super.visitAnnotation(desc, visible);}// 记录开始时间 定义成全局变量以便在后面执行加减法的时候可以方便获取到private int start;/*** 在方法开始时插入代码:* long start = System.currentTimeMillis();*/@Overrideprotected void onMethodEnter() {super.onMethodEnter();if (inject) {//执行完了怎么办?记录到本地变量中invokeStatic(Type.getType("Ljava/lang/System;"),new Method("currentTimeMillis", "()J"));start = newLocal(Type.LONG_TYPE); //创建本地 LONG类型变量//记录 方法执行结果给创建的本地变量storeLocal(start);}}/*** 在方法结束时插入代码:* long end = System.currentTimeMillis();* System.out.println("方法耗时:"+(end-start));*/@Overrideprotected void onMethodExit(int opcode) {super.onMethodExit(opcode);if (inject){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;"));//分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuildernewInstance(Type.getType("Ljava/lang/StringBuilder;"));dup();invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new Method("<init>","()V"));visitLdcInsn("execute:");invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));//减法loadLocal(end);loadLocal(start);math(SUB,Type.LONG_TYPE);invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new Method("append","(J)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"));}}
}

其中我还在visitAnnotation()中判断了该方法是否有AmsTest注解,通过此来判断是否需要插桩。

看到这里,onMethodEnter() 和 onMethodExit() 里面的代码可能会看不懂。

其实onMethodEnter()就是插入了

long start = System.currentTimeMillis();

而 onMethodExit() 就是插入了

long end = System.currentTimeMillis();
System.out.println("方法耗时:"+(end-start));

只不过这些java代码是翻译成汇编指令的形式了。

现在我们查看一下汇编指令。

首先安装 ASM Bytecode Viewer插件:

然后我们写好一个记录时间的代码,如下:

public void sayHello(){long start = System.currentTimeMillis();System.out.println("大家好");long end = System.currentTimeMillis();System.out.println("方法耗时:"+(end-start));
}

查看他的汇编指令,右键--ASM ByteCode Viewer:

右侧显示出汇编指令和对应的行号:

 // access flags 0x1public sayHello()VL0/*** long start = System.currentTimeMillis();对应的汇编指令*/LINENUMBER 19 L0INVOKESTATIC java/lang/System.currentTimeMillis ()JLSTORE 1L1LINENUMBER 20 L1GETSTATIC java/lang/System.out : Ljava/io/PrintStream;LDC "\u5927\u5bb6\u597d"INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL2/*** long end= System.currentTimeMillis();* System.out.println("方法耗时:"+(end-start));对应的汇编指令*/LINENUMBER 21 L2INVOKESTATIC java/lang/System.currentTimeMillis ()JLSTORE 3L3LINENUMBER 22 L3GETSTATIC java/lang/System.out : Ljava/io/PrintStream;NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VLDC "\u65b9\u6cd5\u8017\u65f6\uff1a"INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;LLOAD 3LLOAD 1LSUBINVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)VL4LINENUMBER 23 L4RETURNL5LOCALVARIABLE this Lcom/example/asmstudy/InjectTest; L0 L5 0LOCALVARIABLE start J L1 L5 1LOCALVARIABLE end J L3 L5 3MAXSTACK = 6MAXLOCALS = 5
}

再去找onMethodEnter() 和 onMethodExit() 中的代码,他们是对应的。其中的每个指令的具体含义,可以去参考jvm中文指令手册。

值得注意的是

1.对象的类型的Type 前面需要加L, 比如 ‘java/lang/StringBuilder’ ,它的type是 ‘Ljava/lang/StringBuilder’

2.Type 后面必须加 ‘;’,不然编译出来的class会报错。

关于java类型签名,可以参考一下下表:

然后在单元测试中加入插桩的代码:

public class ExampleUnitTest {@Testpublic void addition_isCorrect() {assertEquals(4, 2 + 2);}@Testpublic void testASM(){AsmUtil asmUtil=new AsmUtil();asmUtil.inject();}
}

一顿操作之后,查看生成的class,已经成功插入了。

查看 编译生成的InjectTest.class:

字节码插桩框架ASM(一)相关推荐

  1. Java ASM框架与字节码插桩的常见用法(生成类,修改类,方法插桩,方法注入)

    前言 ASM 是一款读写Java字节码的工具,可以达到跳过源码编写,编译,直接以字节码的形式创建类,修改已经存在类(或者jar中的class)的属性,方法等. 通常用来开发一些Java开发的辅助框架, ...

  2. aop 获取方法入参出参_ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称及入参和出参结果并记录方法耗时...

    作者:小傅哥 博客:bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获! ❞ 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了 ...

  3. ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时

    作者:小傅哥 博客:bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了多少代码 ...

  4. Android程序员的硬通货——ASM字节码插桩

    作者:享学课堂Lance老师 转载请声明出处! 一.什么是插桩 QQ空间曾经发布的<热修复解决方案>中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPR ...

  5. 【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )

    文章目录 一." 字节码插桩 " 技术简介 二.AspectJ 插桩工具 三.ASM 插桩工具 一." 字节码插桩 " 技术简介 性能优化 , 插件化 , 热修 ...

  6. 字节码插桩(javassist)之插入代码块|IOC框架(Hilt)之对象注入~研究

    Hilt对象注入 | javassist插桩 研究 Hilt对象注入 javassist字节码插桩 创建buildSrc的module 重写Transform 熟悉TransformInvocatio ...

  7. 调研字节码插桩技术,用于系统监控设计和实现

    作者:小傅哥 博客:https://bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获!???? ❞ 目录 一.来自深夜的电话! 二.准备工作 三.使用 AOP 做个切面监控 1. ...

  8. Android AOP之字节码插桩

    背景   本篇文章基于<网易乐得无埋点数据收集SDK>总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实 ...

  9. 看完这一篇,你也可以自如地掌握字节码插桩

    /   今日科技快讯   / 近日,一些国家的黑客频繁对俄罗斯发动网络攻击,以阻止它们正常运行.未来几天,俄罗斯可能与全球互联网断开.针对网络威胁,俄罗斯政府准备启动自己的"大局域网&quo ...

最新文章

  1. 查找重复文件_重复文件快速查找删除
  2. WebX5 button tabs的bind-text属性设置
  3. android tools ignore,android 中tools:ignore=UselessParent这个属性的含义是什么?
  4. 力士乐伺服电机编码器调零_力士乐伺服电机故障与维修排除备份构成
  5. Spark SQL(九)之基于用户的推荐公式
  6. IMP-00041: 警告: 创建的对象带有编译警告解决办法
  7. Ubuntu16.04下部署 nginx+uwsgi+django1.9.7(虚拟环境pyenv+virtualenv)
  8. isbool php,PHP PHPUnit assertIsBool()用法及代码示例
  9. 浅谈设备租赁管理系统的选型之路
  10. java sendredirect 参数_使用response.sendRedirect()传递隐藏参数
  11. ylbtech-LanguageSamples-Indexers(索引器)
  12. 版本管理-SVN本地版本管理
  13. 从零开始学UC(1)之Microsoft Lync Server介绍
  14. HenCoder自定义View学习 - 自定义绘制学习笔记
  15. MD5加密——使用Java自带的MessageDigest工具类实现
  16. Python实时爬取斗鱼弹幕
  17. Chrome浏览器查看Axure原型图文件,提示Axure RP Extension for Chrome
  18. Java Instrument(一) Java Agent
  19. 创业公司做数据分析(一)开篇
  20. 网络基础(二)之HTTP与HTTPS

热门文章

  1. Google gflags安装
  2. Python Access学习(一)
  3. 获取支付宝infoStr
  4. GIS二次开发(C#+AE)
  5. Android高级图片滚动控件,编写3D版的图片轮播器
  6. 线上数据库迁移的几种方法
  7. 构建一个简单的go-web镜像
  8. leetcode.447 回旋镖的数量
  9. BFO Publisher轻松将HTML转换为PDF
  10. 聊一聊CSS3的渐变——gradient