本文属于[Java ASM系列一:Core API](https://blog.51cto.com/lsieun/2924583)当中的一篇。
对于`AnalyzerAdapter`类来说,它的特点是“可以模拟frame的变化”,或者说“可以模拟local variables和operand stack的变化”。
The `AnalyzerAdapter` is a `MethodVisitor` that keeps track of stack map frame changes between `visitFrame(int, int, Object[], int, Object[])` calls.
This `AnalyzerAdapter` adapter must be used with the `Cla***eader.EXPAND_FRAMES` option.
This method adapter computes the **stack map frames** before each instruction, based on the frames visited in `visitFrame`.
Indeed, `visitFrame` is only called before some specific instructions in a method, in order to save space,
and because "the other frames can be easily and quickly inferred from these ones".
This is what this adapter does.
![JVM Stack Frame](http://www.icode9.com/i/li/?n=2&i=images/20210623/1624446269398923.png?,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
## 1. AnalyzerAdapter类
### 1.1 class info
第一个部分,`AnalyzerAdapter`类的父类是`MethodVisitor`类。
```java
public class AnalyzerAdapter extends MethodVisitor {
}
```
### 1.2 fields
第二个部分,`AnalyzerAdapter`类定义的字段有哪些。
我们将以下列出的字段分成3个组:
- 第1组,包括`locals`、`stack`、`maxLocals`和`maxStack`字段,它们是与local variables和operand stack直接相关的字段。
- 第2组,包括`labels`和`uninitializedTypes`字段,它们记录的是未初始化的对象类型,是属于一些特殊情况。
- 第3组,是`owner`字段,表示当前类的名字。
```java
public class AnalyzerAdapter extends MethodVisitor {
// 第1组字段:local variables和operand stack
public List
locals;
public List stack;
private int maxLocals;
private int maxStack;
// 第2组字段:uninitialized类型
private List labels;
public Map<Object, Object> uninitializedTypes;
// 第3组字段:类的名字
private String owner;
}
```
### 1.3 constructors
第三个部分,`AnalyzerAdapter`类定义的构造方法有哪些。
有一个问题:`AnalyzerAdapter`类的构造方法,到底是想实现一个什么样的代码逻辑呢?回答:它想构建方法刚进入时的Frame状态。在方法刚进入时,Frame的初始状态是什么样的呢?其中,operand stack上没有任何元素,而local variables则需要考虑存储`this`和方法的参数信息。在`AnalyzerAdapter`类的构造方法中,主要就是围绕着`locals`字段来展开,它需要将`this`和方法参数添加进入。
```java
public class AnalyzerAdapter extends MethodVisitor {
public AnalyzerAdapter(String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
this(Opcodes.ASM9, owner, access, name, descriptor, methodVisitor);
}
protected AnalyzerAdapter(int api, String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(api, methodVisitor);
this.owner = owner;
locals = new ArrayList<>();
stack = new ArrayList<>();
uninitializedTypes = new HashMap<>();
// 首先,判断是不是static方法、是不是构造方法,来更新local variables的初始状态
if ((access & Opcodes.ACC_STATIC) == 0) {
if ("".equals(name)) {
locals.add(Opcodes.UNINITIALIZED_THIS);
} else {
locals.add(owner);
}
}
// 其次,根据方法接收的参数,来更新local variables的初始状态
for (Type argumentType : Type.getArgumentTypes(descriptor)) {
switch (argumentType.getSort()) {
case Type.BOOLEAN:
case Type.CHAR:
case Type.BYTE:
case Type.SHORT:
case Type.INT:
locals.add(Opcodes.INTEGER);
break;
case Type.FLOAT:
locals.add(Opcodes.FLOAT);
break;
case Type.LONG:
locals.add(Opcodes.LONG);
locals.add(Opcodes.TOP);
break;
case Type.DOUBLE:
locals.add(Opcodes.DOUBLE);
locals.add(Opcodes.TOP);
break;
case Type.ARRAY:
locals.add(argumentType.getDescriptor());
break;
case Type.OBJECT:
locals.add(argumentType.getInternalName());
break;
default:
throw new AssertionError();
}
}
maxLocals = locals.size();
}
}
```
### 1.4 methods
第四个部分,`AnalyzerAdapter`类定义的方法有哪些。
#### 1.4.1 execute方法
在`AnalyzerAdapter`类当中,多数的`visitXxxInsn()`方法都会去调用`execute()`方法;而`execute()`方法是模拟每一条instruction对于local variables和operand stack的影响。
```java
public class AnalyzerAdapter extends MethodVisitor {
private void execute(final int opcode, final int intArg, final String stringArg) {
// ......
}
}
```
#### 1.4.2 return和throw
当遇到`return`或`throw`时,会将`locals`字段和`stack`字段设置为`null`。如果遇到`return`之后,就代表了“正常结束”,方法的代码执行结束了;如果遇到`throw`之后,就代表了“出现异常”,方法处理不了某种情况而退出。
```java
public class AnalyzerAdapter extends MethodVisitor {
// 这里对应return语句
public void visitInsn(final int opcode) {
super.visitInsn(opcode);
execute(opcode, 0, null);
if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW) {
this.locals = null;
this.stack = null;
}
}
}
```
#### 1.4.3 jump
当遇到`goto`、`switch`(`tableswitch`和`lookupswitch`)时,也会将`locals`字段和`stack`字段设置为`null`。遇到jump相关的指令,意味着代码的逻辑要进行“跳转”,从一个地方跳转到另一个地方执行。
```java
public class AnalyzerAdapter extends MethodVisitor {
// 这里对应goto语句
public void visitJumpInsn(final int opcode, final Label label) {
super.visitJumpInsn(opcode, label);
execute(opcode, 0, null);
if (opcode == Opcodes.GOTO) {
this.locals = null;
this.stack = null;
}
}
// 这里对应switch语句
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
super.visitTableSwitchInsn(min, max, dflt, labels);
execute(Opcodes.TABLESWITCH, 0, null);
this.locals = null;
this.stack = null;
}
// 这里对应switch语句
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
super.visitLookupSwitchInsn(dflt, keys, labels);
execute(Opcodes.LOOKUPSWITCH, 0, null);
this.locals = null;
this.stack = null;
}
}
```
#### 1.4.4 visitFrame方法
当遇到jump相关的指令后,程序的代码会发生跳转。那么,跳转到新位置之后,就需要给local variables和operand stack重新设置一个新的状态;而`visitFrame()`方法,是将local variables和operand stack设置成某一个状态。跳转之后的代码,就是在这个新状态的基础上发生变化。
```java
public class AnalyzerAdapter extends MethodVisitor {
public void visitFrame(int type, int numLocal, Object[] local, int numStack, Object[] stack) {
if (type != Opcodes.F_NEW) { // Uncompressed frame.
throw new IllegalArgumentException("AnalyzerAdapter only accepts expanded frames (see Cla***eader.EXPAND_FRAMES)");
}
super.visitFrame(type, numLocal, local, numStack, stack);
if (this.locals != null) {
this.locals.clear();
this.stack.clear();
} else {
this.locals = new ArrayList<>();
this.stack = new ArrayList<>();
}
visitFrameTypes(numLocal, local, this.locals);
visitFrameTypes(numStack, stack, this.stack);
maxLocals = Math.max(maxLocals, this.locals.size());
maxStack = Math.max(maxStack, this.stack.size());
}
private static void visitFrameTypes(int numTypes, Object[] frameTypes, List result) {
for (int i = 0; i < numTypes; ++i) {
Object frameType = frameTypes[i];
result.add(frameType);
if (frameType == Opcodes.LONG || frameType == Opcodes.DOUBLE) {
result.add(Opcodes.TOP);
}
}
}
}
```
#### 1.4.5 new和invokespecial
在执行程序代码的时候,有些特殊的情况需要处理:
- 当遇到`new`时,会创建`Label`对象来表示“未初始化的对象”,并将label存储到`uninitializedTypes`字段内;
- 当遇到`invokespecial`时,会把“未初始化的对象”从`uninitializedTypes`字段内取出来,转换成“经过初始化之后的对象”,然后同步到`locals`字段和`stack`字段内。
```java
public class AnalyzerAdapter extends MethodVisitor {
// 对应于new
public void visitTypeInsn(final int opcode, final String type) {
if (opcode == Opcodes.NEW) {
if (labels == null) {
Label label = new Label();
labels = new ArrayList<>(3);
labels.add(label);
if (mv != null) {
mv.visitLabel(label);
}
}
for (Label label : labels) {
uninitializedTypes.put(label, type);
}
}
super.visitTypeInsn(opcode, type);
execute(opcode, 0, type);
}
// 对应于invokespecial
public void visitMethodInsn(int opcodeAndSource, String owner, String name, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcodeAndSource, owner, name, descriptor, isInterface);
int opcode = opcodeAndSource & ~Opcodes.SOURCE_MASK;
if (this.locals == null) {
labels = null;
return;
}
pop(descriptor);
if (opcode != Opcodes.INVOKESTATIC) {
Object value = pop();
if (opcode == Opcodes.INVOKESPECIAL && name.equals("")) {
Object initializedValue;
if (value == Opcodes.UNINITIALIZED_THIS) {
initializedValue = this.owner;
} else {
initializedValue = uninitializedTypes.get(value);
}
for (int i = 0; i < locals.size(); ++i) {
if (locals.get(i) == value) {
locals.set(i, initializedValue);
}
}
for (int i = 0; i < stack.size(); ++i) {
if (stack.get(i) == value) {
stack.set(i, initializedValue);
}
}
}
}
pushDescriptor(descriptor);
labels = null;
}
}
```
## 2. 工作原理
在上面的内容,我们分别介绍了`AnalyzerAdapter`类的各个部分的信息,那么在这里,我们的目标是按照一个抽象的逻辑顺序来将各个部分组织到一起。那么,这个抽象的逻辑是什么呢?就是local variables和operand stack的状态变化,从初始状态,到中间状态,再到结束状态。
一个类能够为外界提供什么样的“信息”,只要看它的`public`成员就可以了。如果我们仔细观察一下`AnalyzerAdapter`类,就会发现:除了从`MethodVisitor`类继承的`visitXxxInsn()`方法,`AnalyzerAdapter`类自己只定义了三个`public`类型的字段,即`locals`、`stack`和`uninitializedTypes`。如果我们想了解和使用`AnalyzerAdapter`类,只要把握住这三个字段就可以了。
`AnalyzerAdapter`类的主要作用就是记录stack map frame的变化情况;在frame当中,有两个重要的结构,即local variables和operand stack。结合刚才的三个字段,其中`locals`和`stack`分别表示local variables和operand stack;而`uninitializedTypes`则是记录一种特殊的状态,这个状态就是“对象已经通过new创建了,但是还没有调用它的构造方法”,这个状态只是一个“临时”的状态,等后续调用它的构造方法之后,它就是一个真正意义上的对象了。举一个例子,一个人拿到了大学录取通知书,可以笼统的叫作”大学生“,但是还不是真正意义上的”大学生“,是一种”临时“的过渡状态,等到去大学报到之后,才成为真正意义上的大学生。
```java
public class AnalyzerAdapter extends MethodVisitor {
// 第1组字段:local variables和operand stack
public List locals;
public List stack;
// 第2组字段:uninitialized类型
private List labels;
public Map<Object, Object> uninitializedTypes;
}
```
我们在研究local variables和operand stack的变化时,遵循下面的思路就可以了:
- 首先,初始状态。也就是说,最开始的时候,local variables和operand stack是如何布局的。
- 其次,中间状态。local variables和operand stack会随着Instruction的执行而发生变化。按照Instruction执行的顺序,我们这里又分成两种情况:
- 第一种情况,Instruction按照顺序一条一条的向下执行。在这第一种情况里,还有一种特殊情况,就是new对象时,出现的特殊状态下的对象,也就是“已经分配内存空间,但还没有调用构造方法的对象”。
- 第二种情况,遇到jump相关的Instruction,程序代码逻辑要发生跳转。
- 最后,结束状态。方法退出,可以是正常退出(return),也可以异常退出(throw)。
这三种状态,可以与“生命体”作一个类比。在这个世界上,大多数的生命体,都会经历出生、成长、衰老和死亡的变化。
---
在Java语言当中,流程控制语句有三种,分别是顺序(sequential structure)、选择(selective structure)和循环(cycle structure)。但是,如果进入到ByteCode层面或Instruction层面,那么选择(selective structure)和循环(cycle structure)本质上是一样的,都是跳转(Jump)。
---
### 2.1 初始状态
首先,就是local variables和operand stack的初始状态,它是通过`AnalyzerAdapter`类的构造方法来为`locals`和`stack`字段赋值。
```java
public class AnalyzerAdapter extends MethodVisitor {
protected AnalyzerAdapter(int api, String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(api, methodVisitor);
this.owner = owner;
locals = new ArrayList<>();
stack = new ArrayList<>();
uninitializedTypes = new HashMap<>();
// 首先,判断是不是static方法、是不是构造方法,来更新local variables的初始状态
if ((access & Opcodes.ACC_STATIC) == 0) {
if ("".equals(name)) {
locals.add(Opcodes.UNINITIALIZED_THIS);
} else {
locals.add(owner);
}
}
// 其次,根据方法接收的参数,来更新local variables的初始状态
for (Type argumentType : Type.getArgumentTypes(descriptor)) {
switch (argumentType.getSort()) {
case Type.BOOLEAN:
case Type.CHAR:
case Type.BYTE:
case Type.SHORT:
case Type.INT:
locals.add(Opcodes.INTEGER);
break;
case Type.FLOAT:
locals.add(Opcodes.FLOAT);
break;
case Type.LONG:
locals.add(Opcodes.LONG);
locals.add(Opcodes.TOP);
break;
case Type.DOUBLE:
locals.add(Opcodes.DOUBLE);
locals.add(Opcodes.TOP);
break;
case Type.ARRAY:
locals.add(argumentType.getDescriptor());
break;
case Type.OBJECT:
locals.add(argumentType.getInternalName());
break;
default:
throw new AssertionError();
}
}
maxLocals = locals.size();
}
}
```
在上面的构造方法中,operand stack的初始状态是空的;而local variables的初始状态需要考虑两方面的内容:
- 第一方面,当前方法是不是static方法、当前方法是不是`()`方法。
- 第二方面,方法接收的参数。
### 2.2 中间状态
#### 2.2.1 顺序执行
接着,就是instruction的执行会使得local variables和operand stack状态发生变化。在这个过程中,`visitXxxInsn()`方法大多是通过调用`execute(opcode, intArg, stringArg)`方法来完成。
```java
public class AnalyzerAdapter extends MethodVisitor {
private void execute(final int opcode, final int intArg, final String stringArg) {
// ......
}
}
```
#### 2.2.2 发生跳转
当遇到jump相关的指令时,程序代码会从一个地方跳转到另一个地方。
当程序跳转完成之后,需要通过`visitFrame()`方法为`locals`和`stack`字段赋一个新的初始值。再往下执行,可能就进入到“顺序执行”的过程了。
#### 2.2.3 特殊情况:new对象
对于“未初始化的对象类型”,我们来举个例子,比如说`new String()`会创建一个`String`类型的对象,但是对应到ByteCode层面是3条instruction:
```text
NEW java/lang/String
DUP
INVOKESPECIAL java/lang/String. ()V
```
- 第1条instruction,是`NEW java/lang/String`,会为即将创建的对象分配内存空间,确切的说是在堆(heap)上分配内存空间,同时将一个`reference`放到operand stack上,这个`reference`就指向这块内存空间。由于这块内存空间还没有进行初始化,所以这个`reference`对应的内容并不能确切的叫作“对象”,只能叫作“未初始化的对象”,也就是“uninitialized object”。
- 第2条instruction,是`DUP`,会将operand stack上的原有的`reference`复制一份,这时候operand stack上就有两个`reference`,这两个`reference`都指向那块未初始化的内存空间,这两个`reference`的内容都对应于同一个“uninitialized object”。
- 第3条instruction,是`INVOKESPECIAL java/lang/String. ()V`,会将那块内存空间进行初始化,同时会“消耗”掉operand stack最上面的`reference`,那么就只剩下一个`reference`了。由于那块内存空间进行了初始化操作,那么剩下的`reference`对应的内容就是一个“经过初始化的对象”,就是一个平常所说的“对象”了。
### 2.3 结束状态
从JVM内存空间的角度来说,每一个方法都有对应的frame内存空间:当方法开始的时候,就会创建相应的frame内存空间;当方法结束的时候,就会清空相应的frame内存空间。换句话说,当方法结束的时候,frame内存空间的local variables和operand stack也就被清空了。所以,从JVM内存空间的角度来说,结束状态,就是local variables和operand stack所占用的内存空间都“消失了”。
从Java代码的角度来说,方法的退出,就对应于`visitInsn(opcode)`方法中`return`和`throw`的情况。
对于local variables和operand stack的结束状态,它又重要,又不重要:
- 它不重要,是因为它的内存空间被回收了或“消失了”,不需要我们花费太多的时间去思考它,这是从“自身所包含内容的多与少”的角度来考虑。
- 它重要,是因为它在“初始状态-中间状态-结束状态”这个环节当中是必不可少的一部分,这是从“整体性”的角度上来考虑。
## 3. 示例:打印方法的Frame
### 3.1 预期目标
假如有一个`HelloWorld`类,代码如下:
```java
import java.util.Random;
public class HelloWorld {
public HelloWorld() {
super();
}
public boolean getFlag() {
Random rand = new Random();
return rand.nextBoolean();
}
public void test(boolean flag) {
if (flag) {
System.out.println("value is true");
}
else {
System.out.println("value is false");
}
}
public static void main(String[] args) {
HelloWorld instance = new HelloWorld();
boolean flag = instance.getFlag();
instance.test(flag);
}
}
```
我们想实现的预期目标:打印出`HelloWorld`类当中各个方法的frame变化情况。
### 3.2 编码实现
```java
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AnalyzerAdapter;
import java.util.Arrays;
import java.util.List;
public class MethodStackMapFrameVisitor extends ClassVisitor {
private String owner;
public MethodStackMapFrameVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.owner = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodStackMapFrameAdapter(api, owner, access, name, descriptor, mv);
}
private static class MethodStackMapFrameAdapter extends AnalyzerAdapter {
private final String methodName;
private final String methodDesc;
public MethodStackMapFrameAdapter(int api, String owner, int access, String name, String descriptor, MethodVisitor methodVisitor) {
super(api, owner, access, name, descriptor, methodVisitor);
this.methodName = name;
this.methodDesc = descriptor;
}
@Override
public void visitCode() {
super.visitCode();
System.out.println();
System.out.println(methodName + methodDesc);
printStackMapFrame();
}
@Override
public void visitInsn(int opcode) {
super.visitInsn(opcode);
printStackMapFrame();
}
@Override
public void visitIntInsn(int opcode, int operand) {
super.visitIntInsn(opcode, operand);
printStackMapFrame();
}
@Override
public void visitVarInsn(int opcode, int var) {
super.visitVarInsn(opcode, var);
printStackMapFrame();
}
@Override
public void visitTypeInsn(int opcode, String type) {
super.visitTypeInsn(opcode, type);
printStackMapFrame();
}
@Override
public void visitFieldInsn(int opcode, String owner, String name, String descriptor) {
super.visitFieldInsn(opcode, owner, name, descriptor);
printStackMapFrame();
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
printStackMapFrame();
}
@Override
public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) {
super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
printStackMapFrame();
}
@Override
public void visitJumpInsn(int opcode, Label label) {
super.visitJumpInsn(opcode, label);
printStackMapFrame();
}
@Override
public void visitLdcInsn(Object value) {
super.visitLdcInsn(value);
printStackMapFrame();
}
@Override
public void visitIincInsn(int var, int increment) {
super.visitIincInsn(var, increment);
printStackMapFrame();
}
@Override
public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels) {
super.visitTableSwitchInsn(min, max, dflt, labels);
printStackMapFrame();
}
@Override
public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
super.visitLookupSwitchInsn(dflt, keys, labels);
printStackMapFrame();
}
@Override
public void visitMultiANewArrayInsn(String descriptor, int numDimensions) {
super.visitMultiANewArrayInsn(descriptor, numDimensions);
printStackMapFrame();
}
@Override
public void visitTryCatchBlock(Label start, Label end, Label handler, String type) {
super.visitTryCatchBlock(start, end, handler, type);
printStackMapFrame();
}
private void printStackMapFrame() {
String locals_str = locals == null ? "[]" : list2Str(locals);
String stack_str = stack == null ? "[]" : list2Str(stack);
String line = String.format("%s %s", locals_str, stack_str);
System.out.println(line);
}
private String list2Str(List list) {
int size = list.size();
String[] array = new String[size];
for (int i = 0; i < size - 1; i++) {
Object item = list.get(i);
array[i] = item2Str(item);
}
if (size > 0) {
int lastIndex = size - 1;
Object item = list.get(lastIndex);
array[lastIndex] = item2Str(item);
}
return Arrays.toString(array);
}
private String item2Str(Object obj) {
if (obj == Opcodes.TOP) {
return "top";
}
else if (obj == Opcodes.INTEGER) {
return "int";
}
else if (obj == Opcodes.FLOAT) {
return "float";
}
else if (obj == Opcodes.DOUBLE) {
return "double";
}
else if (obj == Opcodes.LONG) {
return "long";
}
else if (obj == Opcodes.NULL) {
return "null";
}
else if (obj == Opcodes.UNINITIALIZED_THIS) {
return "uninitialized_this";
}
else if (obj instanceof Label) {
Object value = uninitializedTypes.get(obj);
if (value == null) {
return obj.toString();
}
else {
return "uninitialized_" + value;
}
}
else {
return obj.toString();
}
}
}
}
```
### 3.3 验证结果
```java
import lsieun.utils.FileUtils;
import org.objectweb.asm.Cla***eader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
public class HelloWorldFrameCore {
public static void main(String[] args) {
String relative_path = "sample/HelloWorld.class";
String filepath = FileUtils.getFilePath(relative_path);
byte[] bytes1 = FileUtils.readBytes(filepath);
//(1)构建Cla***eader
Cla***eader cr = new Cla***eader(bytes1);
//(2)构建ClassWriter
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
//(3)串连ClassVisitor
int api = Opcodes.ASM9;
ClassVisitor cv = new MethodStackMapFrameVisitor(api, cw);
//(4)结合Cla***eader和ClassVisitor
int parsingOptions = Cla***eader.EXPAND_FRAMES; // 注意,这里使用了EXPAND_FRAMES
cr.accept(cv, parsingOptions);
//(5)生成byte[]
byte[] bytes2 = cw.toByteArray();
FileUtils.writeBytes(filepath, bytes2);
}
}
```
## 4. 总结
本文对`AnalyzerAdapter`类进行介绍,内容总结如下:
- 第一点,了解`AnalyzerAdapter`类的各个不同部分。
- 第二点,理解`AnalyzerAdapter`类的代码原理,它是围绕着local variables和operand stack如何变化来展开的。
- 第三点,需要注意的一点是,在使用`AnalyzerAdapter`类时,要记得用`Cla***eader.EXPAND_FRAMES`选项。
`AnalyzerAdapter`类,更多的是具有“学习特性”,而不是“实用特性”。所谓的“学习特性”,具体来说,就是`AnalyzerAdapter`类让我们能够去学习local variables和operand stack随着instruction的向下执行而发生变化。所谓的“实用特性”,就是像`AdviceAdapter`类那样,它有明确的使用场景,能够在“方法进入”的时候和“方法退出”的时候来添加一些代码逻辑。