用于生成和转换方法的 ASM 树 API 是基于 MethodNode 类的(见图 7.1)。
图 7.1 MethodNode 类(仅给出字段)
public class MethodNode ... {
public int access;
public String name;
public String desc;
public String signature;
public List<String> exceptions;
public List<AnnotationNode> visibleAnnotations;
public List<AnnotationNode> invisibleAnnotations;
public List<Attribute> attrs;
public Object annotationDefault;
public List<AnnotationNode>[] visibleParameterAnnotations;
public List<AnnotationNode>[] invisibleParameterAnnotations;
public InsnList instructions;
public List<TryCatchBlockNode> tryCatchBlocks;
public List<LocalVariableNode> localVariables;
public int maxStack;
public int maxLocals;
}
这个类的大多数字段都类似于 ClassNode 的对应字段。最重要的是从 instructions 字段开始的最后几个。这个 instructions 字段是一个指令列表,用一个 InsnList 对象管理, 它的公共 API 如下:
public class InsnList { // public accessors omitted
int size();
AbstractInsnNode getFirst();
AbstractInsnNode getLast();
AbstractInsnNode get(int index);
boolean contains(AbstractInsnNode insn);
int indexOf(AbstractInsnNode insn);
void accept(MethodVisitor mv);
ListIterator iterator();
ListIterator iterator(int index);
AbstractInsnNode[] toArray();
void set(AbstractInsnNode location, AbstractInsnNode insn);
void add(AbstractInsnNode insn);
void add(InsnList insns);
void insert(AbstractInsnNode insn);
void insert(InsnList insns);
void insert(AbstractInsnNode location, AbstractInsnNode insn);
void insert(AbstractInsnNode location, InsnList insns);
void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
void insertBefore(AbstractInsnNode location, InsnList insns);
void remove(AbstractInsnNode insn);
void clear();
}
InsnList 是一个由指令组成的双向链表,它们的链接存储在 AbstractInsnNode 对象本身中。这一点极为重要,因为它对于必须如何使用指令对象和指令列表的方式有许多影响:
- 一个 AbstractInsnNode 对象在一个指令列表中最多出现一次。
- 一个 AbstractInsnNode 对象不能同时属于多个指令列表。
- 一个结果是:如果一个 AbstractInsnNode 属于某个列表,要将它添加到另一列表, 必须先将其从原列表中删除。
- 另一结果是:将一个列表中的所有元素都添加到另一个列表中,将会清空第一个列表。
AbstractInsnNode 类是表示字节代码指令的类的超类。它的公共 API 如下:
public abstract class AbstractInsnNode {
public int getOpcode();
public int getType();
public AbstractInsnNode getPrevious();
public AbstractInsnNode getNext();
public void accept(MethodVisitor cv);
public AbstractInsnNode clone(Map labels);
}
它的子类是 Xxx InsnNode 类,对应于 MethodVisitor 接口的 visitXxx Insn 方法, 而且其构造方式完全相同。例如,VarInsnNode 类对应于 visitVarInsn 方法,且具有以下结构:
public class VarInsnNode extends AbstractInsnNode {
public int var;
public VarInsnNode(int opcode, int var) {
super(opcode);
this.var = var;
}
...
}
标记与帧,还有行号,尽管它们并不是指令,但也都用 AbstractInsnNode 类的子类表示,即 LabelNode、FrameNode 和 LineNumberNode 类。这样就允许将它们恰好插在列表中对应的真实指令之前,与核心 API 中一样(在核心 API 中,就是恰在相应的指令之前访问标记和帧)。因此,很容易使用 AbstractInsnNode 类提供的 getNext 方法找到跳转指令的目标:这是目标标记之后第一个是真正指令的 AbstractInsnNode。另一个结果是:与核心 API 一样,只要标记保持不变,删除指令并不会破坏跳转指令。
用树 API 生成一个方法包括:创建一个 MethodNode,初始化其字段。最重要的部分是方法代码的生成。比如,3.1.5 节的 checkAndSetF 方法可生成如下:
MethodNode mn = new MethodNode(...);
InsnList il = mn.instructions;
il.add(new VarInsnNode(ILOAD, 1));
LabelNode label = new LabelNode();
il.add(new JumpInsnNode(IFLT, label));
il.add(new VarInsnNode(ALOAD, 0));
il.add(new VarInsnNode(ILOAD, 1));
il.add(new FieldInsnNode(PUTFIELD, "pkg/Bean", "f", "I"));
LabelNode end = new LabelNode();
il.add(new JumpInsnNode(GOTO, end));
il.add(label);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new TypeInsnNode(NEW, "java/lang/IllegalArgumentException"));
il.add(new InsnNode(DUP));
il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V"));
il.add(new InsnNode(ATHROW));
il.add(end);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new InsnNode(RETURN));
mn.maxStack = 2;
mn.maxLocals = 2;
和类的情景一样,使用树 API 来生成方法时,花费的时间和占用的内存都要多于使用核心 API 的情况。但可以按照任意顺序来生成其内容。具体来说,这些指令可按非顺序方式生成,这在一些情况下是很有用的。
比如,考虑一个压缩编译器。通常,要编译表达式 e1+e2,首先发送 e1 的代码,然后发出 e2 的代码,然后发出将这两个值相加的代码。但如果 e1 和 e2 不是同一基元类型,必须恰在 e1 的代码之后插入一个转换操作,恰在 e2 的代码之后插入另一个。但是究竟发出哪些转换操作取决于 e1 和 e2 的类型。
现在,如果表达式的类型是由发出已编译代码的方法返回的,那在使用核心 API 时就会存在一个问题:只有在已经编译了 e2 之后才能知道必须插在 e1 之后的转换,但这时已经太晚了, 因为我们不能在之前访问的指令之间插入指令。①在使用树 API 时不存在这一问题。例如,一种可能性是使用比如下面所示的 compile 方法:
public Type compile(InsnList output) {
InsnList il1 = new InsnList();
InsnList il2 = new InsnList();
Type t1 = e1.compile(il1);
Type t2 = e2.compile(il2);
Type t = ...; // 计算 t1 和 t2 的公共超类型
output.addAll(il1); // 在常量时间内完成
output.add(...); // 由 t1 到t 的转换指令
output.addAll(il2); // 在常量时间内完成
output.add(...); // 由 t2 到t 的转换指令
output.add(new InsnNode(t.getOpcode(IADD)));
return t;
}
用树 API 转换方法只需要修改一个 MethodNode 对象的字段,特别是 instructions 列表。尽管这个列表可以采用任意方式修改,但常见做法是通过迭代修改。事实上,与通用 ListIterator 约定不同,InsnList 返回的 ListIterator 支持许多并发列表修改①。事实上,可以使用 InsnList 方法删除包括当前元素在内的一或多个元素,删除下一个元素之后的一或多个元素(也就是说,不是紧随当今元素之后的元素,而是它后面一个元素之后的元素),或者在当前元素之前或其后续者之后插入一或多个元素。这些修改将反映在迭代器中,即在下一元素之后插入(或删除)的元素将在迭代器中被看到(或不被看到)。
如果需要在一个列表的指令 i 之后插入几条指令,那另一种修改指令列表的常见做法是将这些新指令插入一个临时指令列表中,再在一个步骤内将这个临时列表插到主列表中:
InsnList il = new InsnList();
il.add(...);
...
il.add(...);
mn.instructions.insert(i, il);
逐条插入指令也是可行的,但却非常麻烦,因为必须在每次插之后更新插入点。
让我们用一些示例来具体看看如何用树 API 转换方法。为了看出核心 API 和树 API 之间的区 别 , 重新实现 3.2.4 节的 AddTimerAdapter 示例和 3.2.5 节的 RemoveGetFieldPutFieldAdapter 是有意义的。计时器示例可实现如下:
public class AddTimerTransformer extends ClassTransformer {
public AddTimerTransformer(ClassTransformer ct) {
super(ct);
}
@Override
public void transform(ClassNode cn) {
for (MethodNode mn : (List<MethodNode>) cn.methods) {
if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {
continue;
}
InsnList insns = mn.instructions;
if (insns.size() == 0) {
continue;
}
Iterator<AbstractInsnNode> j = insns.iterator();
while (j.hasNext()) {
AbstractInsnNode in = j.next();
int op = in.getOpcode();
if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
InsnList il = new InsnList();
il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J"));
il.add(new InsnNode(LADD));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
insns.insert(in.getPrevious(), il);
}
}
InsnList il = new InsnList();
il.add(new FieldInsnNode(GETSTATIC, cn.name, "timer", "J"));
il.add(new MethodInsnNode(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J"));
il.add(new InsnNode(LSUB));
il.add(new FieldInsnNode(PUTSTATIC, cn.name, "timer", "J"));
insns.insert(il);
mn.maxStack += 4;
}
int acc = ACC_PUBLIC + ACC_STATIC;
cn.fields.add(new FieldNode(acc, "timer", "J", null, null));
super.transform(cn);
}
}
在这里可以看出上一节讨论的用于在指令列表中插入若干指令的模式,其中包含了使用临时指令列表。这个示例还表明,有可能在迭代一个指令表的时候向当前指令之前插入指令。注意, 在使用核心 API 和树 API 时,实现这一适配器所需要的代码数量大体相同。
(如果假定 MethodTransformer 类似于上一章的 MethodTransformer 类,)删除了字段自我赋值的方法适配器(见 3.2.5 节)可实现如下:
public class RemoveGetFieldPutFieldTransformer extends MethodTransformer {
public RemoveGetFieldPutFieldTransformer(MethodTransformer mt) {
super(mt);
}
@Override
public void transform(MethodNode mn) {
InsnList insns = mn.instructions;
Iterator<AbstractInsnNode> i = insns.iterator();
while (i.hasNext()) {
AbstractInsnNode i1 = i.next();
if (isALOAD0(i1)) {
AbstractInsnNode i2 = getNext(i1);
if (i2 != null && isALOAD0(i2)) {
AbstractInsnNode i3 = getNext(i2);
if (i3 != null && i3.getOpcode() == GETFIELD) {
AbstractInsnNode i4 = getNext(i3);
if (i4 != null && i4.getOpcode() == PUTFIELD) {
if (sameField(i3, i4)) {
while (i.next() != i4) {
}
insns.remove(i1);
insns.remove(i2);
insns.remove(i3);
insns.remove(i4);
}
}
}
}
}
}
super.transform(mn);
}
private static AbstractInsnNode getNext(AbstractInsnNode insn) {
do {
insn = insn.getNext();
if (insn != null && !(insn instanceof LineNumberNode)) {
break;
}
} while (insn != null);
return insn;
}
private static boolean isALOAD0(AbstractInsnNode i) {
return i.getOpcode() == ALOAD && ((VarInsnNode) i).var == 0;
}
private static boolean sameField(AbstractInsnNode i, AbstractInsnNode j) {
return ((FieldInsnNode) i).name.equals(((FieldInsnNode) j).name);
}
}
在这里再次看到,有可能在对一个指令清单迭代时从中删除指令。但要注意 while (i.next() != i4)循环:必须将迭代器放在必须删除的指令之后(因为不可能删除恰在当前指令之后的指令)。基于访问器和基于树的实现都可以在被检测序列的中部检测到标记和帧,在这种情况下,不要删除它。但要忽略序列中的行号(见 getNext 方法),使用基于树的 API 时的代码数量要多于使用核心 API 的情况。但是,这两种实现之间的主要区别是:在使用树 API 时,不需要状态机。特别是有三个或更多个连续 ALOAD 0 指令的特殊情景(它很容易被忽视),不再成为问题了。
利用上述实现,一条给定指令可能会被查看多次,这是因为在 while 循环中的每一步,i2、 i3 和 i4 也可能会在这一迭代中被查看(在未来迭代中还会查看它们)。事实上,有可能使用一种更高效的实现,使每条指令最多被查看一次:
public class RemoveGetFieldPutFieldTransformer2 extends MethodTransformer {
...
@Override
public void transform(MethodNode mn) {
InsnList insns = mn.instructions;
Iterator i = insns.iterator();
while (i.hasNext()) {
AbstractInsnNode i1 = (AbstractInsnNode) i.next();
if (isALOAD0(i1)) {
AbstractInsnNode i2 = getNext(i);
if (i2 != null && isALOAD0(i2)) {
AbstractInsnNode i3 = getNext(i);
while (i3 != null && isALOAD0(i3)) {
i1 = i2;
i2 = i3;
i3 = getNext(i);
}
if (i3 != null && i3.getOpcode() == GETFIELD) {
AbstractInsnNode i4 = getNext(i);
if (i4 != null && i4.getOpcode() == PUTFIELD) {
if (sameField(i3, i4)) {
insns.remove(i1);
insns.remove(i2);
insns.remove(i3);
insns.remove(i4);
}
}
}
}
}
}
super.transform(mn);
}
private static AbstractInsnNode getNext(Iterator i) {
while (i.hasNext()) {
AbstractInsnNode in = (AbstractInsnNode) i.next();
if (!(in instanceof LineNumberNode)) {
return in;
}
}
return null;
}
...
}
与上一个实现的区别在于 getNext 方法,它现在是对列表迭代器进行操作。当序列被识别出来时,迭代器恰好位于它的后面,所以不再需要 while (i.next() != i4)循环。但这里再次出现了三个或多个连续 ALOAD 0 指令的特殊情况(见 while (i3 != null)循环)。
到目前为止,我们看到的所有方法转换都是局部的,甚至有状态的转换也是如此,所谓“局部”是指,一条指令 i 的转换仅取决于与 i 有固定距离的指令。但还存在一些全局转换,在这种转换中,指令 i 的转换可能取决于与 i 有任意距离的指令。对于这些转换,树 API 真的很有帮助, 也就是说,使用核心 API 实现它们将会非常非常复杂。
下面的转换就是这样一个例子:用向 label 的跳转代替向 GOTO label 指令的跳转,然后用一个 RETURN 指令代替指向这个 RETURN 指令的 GOTO。实际中,一个跳转指令的目标与这条指令的距离可能为任意远,可能在它的前面,也可能在其之后。这样一个转换可实现如下:
public class OptimizeJumpTransformer extends MethodTransformer {
public OptimizeJumpTransformer(MethodTransformer mt) {
super(mt);
}
@Override
public void transform(MethodNode mn) {
InsnList insns = mn.instructions;
Iterator<AbstractInsnNode> i = insns.iterator();
while (i.hasNext()) {
AbstractInsnNode in = i.next();
if (in instanceof JumpInsnNode) {
LabelNode label = ((JumpInsnNode) in).label;
AbstractInsnNode target;
// 当 target == goto l,用l 代替 label
while (true) {
target = label;
while (target != null && target.getOpcode() < 0) {
target = target.getNext();
}
if (target != null && target.getOpcode() == GOTO) {
label = ((JumpInsnNode) target).label;
} else {
break;
}
}
// 更新目标
((JumpInsnNode) in).label = label;
// 在可能时,用目标指令代替跳转
if (in.getOpcode() == GOTO && target != null) {
int op = target.getOpcode();
if ((op >= IRETURN && op <= RETURN) || op == ATHROW) {
// replace ’in’ with clone of ’target’
insns.set(in, target.clone(null));
}
}
}
}
super.transform(mn);
}
}
此代码的工作过程如下:当找到一条跳转指令 in 时,它的目标被存储在 label 中。然后用最内层的 while 循环查找紧跟在这个标记之后出现的指令( 不代表实际指令的 AbstractInsnNode 对象,比如 FrameNode 或 LabelNode,其“操作码”为负)。只要这条指令是 GOTO,就用这条指令的目标代替 label,然后重复上述步骤。最后,用这个更新后的 label 值来代替in 的目标标记,如果in 本身是一个GOTO,并且其更新后的目标是一条RETURN指令,则 in 用这个返回指令的克隆副本代替(回想一下,一个指令对象在一个指令列表中不能出现一次以上)。
对于 3.1.5 节定义的 checkAndSetF 方法,这一转换的效果如下:
之前 ILOAD 1 | 之后 ILOAD 1 |
---|---|
IFLT label ALOAD 0 ILOAD 1 PUTFIELD ... | IFLT label ALOAD 0 ILOAD 1 PUTFIELD ... |
GOTO end label: | RETURN label: |
F_SAME NEW ... DUP INVOKESPECIAL ATHROW end: | F_SAME NEW ... DUP INVOKESPECIAL ... ATHROW end: |
F_SAME RETURN | F_SAME RETURN |
注意,尽管这个转换改变了跳转指令(更正式地说,是改变了控制流图),但它不需要更新方法的帧。事实上,在每条指令处,执行帧的状态保持不变,而且由于没有引用新的跳转目标, 所以并不需要访问新的帧。但是,可能会出现不再需要某个帧的情况。例如在上面的例子中,转换后不再需要 end 标记,它后面的 F_SAME 帧和 RETURN 指令也是如此。幸好,访问帧数超出必需数量是完全合法的,在方法中包含未被使用的代码(称为死代码或不可及代码)也是合法的。因此,上述方法适配器是正确的,尽管可对其进行改进,删除死代码和帧。