前言

成为一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。

《深入探索编译插桩技术(二、AspectJ)》:juejin.im/post/5e8438…

一文中我们深入学习了 AspectJ 在 Android 下的使用。可以看到 AspectJ 非常强大,但是它也只能实现 50% 的字节码操作场景,如果想要实现 100% 的字节码操作场景,那么就不得不使用 ASM。

此外,AspectJ 有着一系列弊端: 由于其基于规则,所以其切入点相对固定,对于字节码文件的操作自由度以及开发的掌控度就大打折扣。并且,他会额外生成一些包装代码,对性能以及包大小有一定影响

而 ASM 基本上可以实现任何对字节码的操作,也就是自由度和开发的掌控度很高。它提供了 访问者模式来访问字节码文件,并且只注入我们想要注入的代码

ASM 最初起源于一个博士的研究项目,在 2002 年开源,并从 5.x 版本便开始支持 Java 8。并且,ASM 是诸多 JVM 语言钦定的字节码生成库,它在效率与性能方面的优势要远超其它的字节码操作库如 javassist、AspectJ

思维导图大纲

目录

  • 一、ASM 的优势和逆势

    • 1、ASM 的优势
    • 2、ASM 的逆势
  • 二、ASM 的对象模型(ASM Tree API)
    • 1、优点
    • 2、缺点
    • 3、获取节点
    • 4、操控操作码
  • 三、ASM 的事件模型(ASM Core API)
    • 2、类读取(解析)者 ClassVisitor
    • 3、小结
  • 四、综合实战训练
    • 1、使用 ASM Bytecode Outline
    • 2、使用 ASM 编译插桩统计方法耗时
    • 3、全局替换项目中所有的 new Thread
  • 五、总结

一、ASM 的优势和逆势

使用 ASM 操作字节码的优势与逆势都 比较明显,其分别如下所示。

1、ASM 的优势

  • 1)、内存占用很小
  • 2)、运行速度非常快
  • 3)、操作灵活:对于字节码的操作非常地灵活,可以进行插入、删除、修改等操作
  • 4)、想象空间大,能够借用它提升生产力
  • 5)、丰富的文档与众多社区的支持

2、ASM 的逆势

上手难度较大,需要对 Java 字节码有比较充分的了解

对于 ASM 而言,它提供了 两种模型:对象模型和事件模型

下面,我们就先来讲讲 ASM 的对象模型。

二、ASM 的对象模型(ASM Tree API)

对象模型的 本质 是一个 被封装过后的事件模型,它 使用了树状图的形式来描述一个类,其中包含多个节点,例如方法节点、字段节点等等,而每个节点又有子节点,例如方法节中有操作码子节点 等等。下面我们先来了解下由这种树状图模式实现的对象模型的利弊。

1、优点

  • 1)、适宜处理简单类的修改
  • 2)、学习成本较低
  • 3)、代码量较少

2、缺点

  • 1)、处理大量信息会使代码变得复杂
  • 2)、代码难以复用

在对象模型下的 ASM 有 两类操作纬度,分别如下所示:

  • 1)、获取节点获取指定类、字段、方法节点
  • 2)、操控操作码(针对方法节点)获取操作码位置、替换、删除、插入操作码、输出字节码

下面我们就分别来了解下 ASM 的这两类操作。

3、获取节点

1)、获取指定类的节点

获取一个类节点的代码如下所示:

ClassNode classNode = new ClassNode();
// 1
ClassReader classReader = new ClassReader(bytes);
// 2
classReader.accept(classNode, 0);

在注释1处,将字节数组传入一个新创建的 ClassReader,这时 ASM 会使用 ClassReader 来解析字节码。接着,在注释2处,ClassReader 在解析完字节码之后便可以通过 accept 方法来将结果写入到一个 ClassNode 对象之中

那么,一个 ClassNode 具体又包含哪些信息呢?

如下所示:

类节点信息

类型 名称 说明
int version class文件的major版本(编译的java版本)
int access 访问级
String name 类名,采用全地址,如java/lang/String
String signature 签名,通常是null
String superName 父类类名,采用全地址
List interfaces 实现的接口,采用全地址
String sourceFile 源文件,可能为null
String sourceDebug debug源,可能为null
String outerClass 外部类
String outerMethod 外部方法
String outerMethodDesc 外部方法描述(包括方法参数和返回值)
List visibleAnnotations 可见的注解
List invisibleAnnotations 不可见的注解
List attrs 类的Attribute
List innerClasses 类的内部类列表
List fields 类的字段列表
List methods 类的方法列表

2)、获取指定字段的节点

获取一个字段节点的代码如下所示:

for(FieldNode fieldNode : (List)classNode.fields) {// 1if(fieldNode.name.equals("password"))  {// 2fieldNode.access = Opcodes.ACC_PUBLIC;}
}

字段节点列表 fields 是一个 ArrayList,它储存着类节点的所有字段。在注释1处,我们通过遍历 fields 集合的方式来找到目标字段节点。接着,在注释2处,我们将目标字段节点的访问权限置为 public。

除此之外,我们还可以为类添加需要的字段,代码如下所示:

FieldNode fieldNode = new FieldNode(Opcodes.ACC_PUBLIC | Opcodes.ACC_STATIC, "JsonChao", "I", null, null);
classNode.fields.add(fieldNode);

在上述代码中,我们直接给目标类节点添加了一个 "public static int JsonChao" 的字段,需要注意的是,第三个参数的 "I" 表示的是 int 的类型描述符。

那么,对于一个字段节点,又包含有哪些字段信息呢?

如下所示:

字段信息

类型 名称 说明
int access 访问级
String name 字段名
String signature 签名,通常是 null
String desc 类型描述,例如 Ljava/lang/String、D(double)、F(float)
Object value 初始值,通常为 null
List visibleAnnotations 可见的注解
List invisibleAnnotations 不可见的注解
List attrs 字段的 Attribute

接下来,我们看看如何获取一个方法节点。

3)、获取指定的方法节点

获取指定的方法节点的代码如下所示:

for(MethodNode methodNode : (List)classNode.methods) {// 1、判断方法名是否匹配目标方法if(methodNode.name.equals("getName")) {// 2、进行操作}
}

methods 同 fields 一样,也是一个 ArrayList,通过遍历并判断方法名的方式即可匹配到目标方法。

对于一个方法节点来说,它包含有如下信息:

方法节点包含的信息

类型 名称 说明
int access 访问级
String name 方法名
String desc 方法描述,其包含方法的返回值和参数
String signature 签名,通常是null
List exceptions 可能返回的异常列表
List visibleAnnotations 可见的注解列表
List invisibleAnnotations 不可见的注解列表
List attrs 方法的Attribute列表
Object annotationDefault 默认的注解
List[] visibleParameterAnnotations 可见的参数注解列表
List[] invisibleParameterAnnotations 不可见的参数注解列表
InsnList instructions 操作码列表
List tryCatchBlocks try-catch块列表
int maxStack 最大操作栈的深度
int maxLocals 最大局部变量区的大小
List localVariables 本地(局部)变量节点列表

4、操控操作码

在操控字节码之前,我们必须先了解下 instructions,即 操作码列表,它是 方法节点中用于存储操作码的地方,其中 每一个元素都代表一行操作码

ASM 将一行字节码封装为一个 xxxInsnNode(Insn 表示的是 Instruction 的缩写,即指令/操作码),例如 ALOAD/ARestore 指令被封装入变量操作码节点 VarInsnNode,INVOKEVIRTUAL 指令则会被封入方法操作码节点 MethodInsnNode 之中

对于所有的指令节点 xxxInsnNode 来说,它们都继承自抽象操作码节点 AbstractInsnNode。其所有的派生类使用详情如下所示。

所有的指令码节点说明

名称 说明 参数
FieldInsnNode 用于 GETFIELD 和 PUTFIELD 之类的字段操作的字节码 String owner 字段所在的类
String name 字段的名称
String desc 字段的类型
FrameNode 栈映射帧的对应的帧节点 待补充
IincInsnNode 用于 IINC 变量自加操作的字节码 int var:目标局部变量的位置
int incr: 要增加的数
InsnNode 一切无参数值操作的字节码,例如 ALOAD_0,DUP(注意不包含 POP)
IntInsnNode 用于 BIPUSH、SIPUSH 和 NEWARRAY 这三个直接操作整数的操作 int operand:操作的整数值
InvokeDynamicInsnNode 用于 Java7 新增的 INVOKEDYNAMIC 操作的字节码 String name:方法名称
String desc:方法描述
Handle bsm:句柄
Object[] bsmArgs:参数常量
JumpInsnNode 用于 IFEQ 或 GOTO 等跳转操作字节码 LabelNode lable:目标 lable
LabelNode 一个用于表示跳转点的 Label 节点
LdcInsnNode 使用 LDC 加载常量池中的引用值并进行插入的字节码 Object cst:引用值
LineNumberNode 表示行号的节点 int line:行号
LabelNode start:对应的第一个 Label
LookupSwitchInsnNode 用于实现 LOOKUPSWITCH 操作的字节码 LabelNode dflt:default 块对应的 Lable
List keys 键列表
List labels:对应的 Label 节点列表
MethodInsnNode 用于 INVOKEVIRTUAL 等传统方法调用操作的字节码,不适用于 Java7 新增的 INVOKEDYNAMIC String owner :方法所在的类
String name :方法名称
String desc:方法描述
MultiANewArrayInsnNode 用于 MULTIANEWARRAY 操作的字节码 String desc:类型描述
int dims:维数
TableSwitchInsnNode 用于实现 TABLESWITCH 操作的字节码 int min:键的最小值
int max:键的最大值
LabelNode dflt:default 块对应的 Lable
List labels:对应的 Label 节点列表
TypeInsnNode 用于实现 NEW、ANEWARRAY 和 CHECKCAST 等类型相关操作的字节码 String desc:类型
VarInsnNode 用于实现 ALOAD、ASTORE 等局部变量操作的字节码 int var:局部变量

下面,我们就开始来讲解下字节码操控有哪几种常见的方式。

1、获取操作码的位置

获取指定操作码位置的代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {if(ainNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode)ainNode).operand == 16) {....//进行操作}
}

由于一般情况下我们都无法确定操作码在列表中的具体位置,因此 通常会通过遍历的方式去判断其关键特征,以此来定位指定的操作码,上述代码就能定位到一个 SIPUSH 16 的字节码,需要注意的是,有时一个方法中会有多个相同的指令,这是我们需要靠判断前后字节码识别其特征来定位,也可以记下其命中次数然后设定在某一次进行操作,一般情况下我们都是使用的第二种

2、替换指定的操作码

替换指定的操作码的代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));}
}

这里我们 直接调用了 InsnList 的 set 方法就能替换指定的操作码对象,我们在获取了 "BIPUSH 64" 字节码的位置后,便将封装它的操作码替换为一个新的 VarInsnNode 操作码,这个新操作码封装了 "ALOAD 1" 字节码, 将原程序中 将值设为16 替换为 将值设为局部变量1

3、删除指定的操作码

methodNode.instructions.remove(xxx);

xxx 表示的是要删除的操作码实例,我们直接调用用 InsnList 的 remove 方法将它移除掉即可。

4、插入指定的操作码

InsnList 主要提供了 四类 方法用于插入字节码,如下所示:

  • 1)、add(AbstractInsnNode insn)将一个操作码添加到 InsnList 的末尾
  • 2)、insert(AbstractInsnNode insn)将一个操作码插入到这个 InsnList 的开头
  • 3)、insert(AbstractInsnNode insnNode,AbstractInsnNode insn)将一个操作码插入到另一个操作码的下面
  • 4)、insertBefore(AbstractInsnNode insnNode,AbstractInsnNode insn) 将一个操作码插入到另一个操作码的上面

接下来看看如何使用这些方法插入指定的操作码,代码如下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {if(ainNode.getOpcode() == Opcodes.BIPUSH && ((IntInsnNode)ainNode).operand == 16) {methodNode.instructions.insert(ainNode, new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/awt/image/BufferedImage", "getWidth", "(Ljava/awt/image/ImageObserver;)I"));methodNode.instructions.insert(ainNode, new InsnNode(Opcodes.ACONSTNULL));methodNode.instructions.set(ainNode, new VarInsnNode(Opcodes.ALOAD, 1));}
}

这样,我们就能将

BIPUSH 16
```java替换为```java
ALOAD 1
ACONSTNULL
INVOKEVIRTUAL java/awt/image/BufferedImage.getWidth(Ljava/awt/image/ImageObserver;)I
```java**当我们操控完指定的类节点之后,就可以使用 ASM 的 ClassWriter 类来输出字节码**,代码如下所示:```java
// 1、让 ClassWriter 自行计算最大栈深度和栈映射帧等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();

关于 ClassWriter 的具体用法,我们会在 ASM Core API 这部分来进行逐步讲解。下面

深入探索编译插桩技术(四、ASM 探秘)相关推荐

  1. 深入探索编译插桩技术(一、编译基础)

    前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 现如今,Gradle + 编译插桩 的应用场景越来越多,无论是 各种性能优化中的插件工具制作 ...

  2. aspectj 获取方法入参_深入探索编译插桩技术(二、AspectJ)

    本文来自jsonchao的投稿,个人微信:bcce5360 现如今,编译插桩技术已经深入 Android 开发中的各个领域,而 AOP 技术正是一种高效实现插桩的模式,它的出现正好给处于黑暗中的我们带 ...

  3. 深入探索编译插桩技术(二、AspectJ)

    前言 成为一名优秀的Android开发,需要一份完备的知识体系,在这里,让我们一起成长为自己所想的那样~. 现如今,编译插桩技术已经深入 Android 开发中的各个领域,而 AOP 技术正是一种高效 ...

  4. 深入探索编译插桩技术(四、ASM 探秘,android中文api文档

    从字节码的视角中,一个 Java 类由很多组件凝聚而成,而这之中便包括超类.接口.属性.域和方法等等.当我们在使用 ASM 进行操控时,可以将它们视为一个个与之对应的事件.因此 ASM 提供了一个 类 ...

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

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

  6. Android编译插桩

    背景:这一次分享一下关于android编译插桩这个话题,在正常编写代码实现程序的逻辑外,还要使用一点点黑科技,拿起操作代码无所不能的武器. 一.Android常用的能动态改变代码逻辑的方法有两种 1. ...

  7. 使用插桩技术解决慢查询测试问题

    原文由zlulu发表于TesterHome社区,原文链接 缘起 前段时间,我负责测试的系统在生产环境运行出现问题.该系统对于响应时间要求较高,问题发生的时候并发很高,出现大量请求超时,超时请求比例随时 ...

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

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

  9. 【网上的都不靠谱?还是得改源码】用Javasisst的字节码插桩技术,彻底解决Gson转Map时,Int变成double问题...

    一.探究原由 首先申明一下,我们要解决的问题有两个: Json串转Map时,int变double问题 Json串转对象时,对象属性中的Map,int变double问题 然后,我们来了解一下,Gson实 ...

最新文章

  1. [重构到模式-Chain of Responsibility Pattern]把Fizz Buzz招式重构到责任链模式
  2. neutron plugin 笔记
  3. Linux Centos下SQL Server 2017安装和配置
  4. 我的欧拉工程之路_3
  5. Github | 商汤出品-可在视频里追踪单个对象PySOT
  6. C语言的EOF是什么?getchar()!=EOF返回的是什么?
  7. FPGA RAM时分复用减少逻辑资源
  8. 腾讯网游加速器大升级!5月31日起仅支持国服游戏加速 你用过吗?
  9. centos php rpm下载源,CentOS 6.2 使用第三方yum源安装更多rpm软件包 | 系统运维
  10. 实验二+065+方绎杰
  11. PB调用WebService
  12. 基于matlab的平面切割、旋转曲面等动图制作
  13. pytorch中为Module和Tensor指定GPU
  14. Android 调用第三方地图类App (高德 百度 百度网页版)
  15. Android中跳转到系统设置界面
  16. 【Linux记录】Linux 可以telnet通localhost,不能telnet ip,telnet localhost正常,telnet ip失败。
  17. js设置延时加载事件
  18. 百度网盘网页版倍速播放
  19. 【续】数学模型——人口增长模型
  20. UI设计培训技术教程之字体排版规则

热门文章

  1. linux 内核CMA笔记
  2. falcon常用参数解析
  3. java 分班_CoreJava分班测试试卷答案
  4. UIL 算法学习 Structure Based User Identification across Social Networks
  5. 通过velocity自定义模板字符串实现可配置的外部调用查询接口
  6. VS2008编程软件过期的问题,过期弹出须要升级窗体的解决的方法
  7. web前端之sass简介
  8. 量子计算机模拟人类,量子物理学家模拟推测:我们三维世界的演变可能早已被编程好...
  9. Redis 的常用命令
  10. 第1章CRM核心业务介绍