# 3.2 接口和组件

# 3.2.1 介绍

用于生成和转换已编译方法的 ASM API 是基于 MethodVisitor 抽象类的(见图 3.4),它由 ClassVisitor 的 visitMethod 方法返回。除了一些与注释和调试信息有关的方法之外(这些方法在下一章解释),这个类为每个字节代码指令类别定义了一个方法,其依据就是这些指令的参数个数和类型(这些类别并非对应于 3.1.2 节给出的类别)。这些方法必须按以下顺序调用(在 MethodVisitor 接口的 Javadoc 中还规定了其他一些约束条件):

visitAnnotationDefault?
( visitAnnotation | visitParameterAnnotation | visitAttribute )* ( visitCode
( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn |
visitLocalVariable | visitLineNumber )*
visitMaxs )?
visitEnd
1
2
3
4
5
6

这就意味着,对于非抽象方法,如果存在注释和属性的话,必须首先访问它们,然后是该方法的字节代码。对于这些方法,其代码必须按顺序访问,位于对 visitCode 的调用(有且仅有一个调用)与对 visitMaxs 的调用(有且仅有一个调用)之间。

abstract class MethodVisitor { // public accessors ommited MethodVisitor(int api);
    MethodVisitor(int api, MethodVisitor mv);

    AnnotationVisitor visitAnnotationDefault();

    AnnotationVisitor visitAnnotation(String desc, boolean visible);

    AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);

    void visitAttribute(Attribute attr);

    void visitCode();

    void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);

    void visitInsn(int opcode);

    void visitIntInsn(int opcode, int operand);

    void visitVarInsn(int opcode, int var);

    void visitTypeInsn(int opcode, String desc);

    void visitFieldInsn(int opc, String owner, String name, String desc);

    void visitMethodInsn(int opc, String owner, String name, String desc);

    void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs);

    void visitJumpInsn(int opcode, Label label);

    void visitLabel(Label label);

    void visitLdcInsn(Object cst);

    void visitIincInsn(int var, int increment);

    void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);

    void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);

    void visitMultiANewArrayInsn(String desc, int dims);

    void visitTryCatchBlock(Label start, Label end, Label handler, String type);

    void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);

    void visitLineNumber(int line, Label start);

    void visitMaxs(int maxStack, int maxLocals);

    void visitEnd();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

于是,visitCodevisitMaxs 方法可用于检测该方法的字节代码在一个事件序列中的开始与结束。和类的情况一样,visitEnd 方法也必须在最后调用,用于检测一个方法在一个事件序列中的结束。

可以将 ClassVisitor 和 MethodVisitor 类合并,生成完整的类:

ClassVisitor cv = ...; cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...); 
mv1.visitCode();
mv1.visitInsn(...);
...
mv1.visitMaxs(...); mv1.visitEnd();
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...); 
mv2.visitCode();
mv2.visitInsn(...);
...
mv2.visitMaxs(...); mv2.visitEnd(); cv.visitEnd();
1
2
3
4
5
6
7
8
9
10
11

注意,并不一定要在完成一个方法之后才能开始访问另一个方法。事实上,MethodVisitor实例是完全独立的,可按任意顺序使用(只要还没有调用 cv.visitEnd()):

ClassVisitor cv = ...; cv.visit(...);
MethodVisitor mv1 = cv.visitMethod(..., "m1", ...); 
mv1.visitCode();
mv1.visitInsn(...);
...
MethodVisitor mv2 = cv.visitMethod(..., "m2", ...); 
mv2.visitCode();
mv2.visitInsn(...);
...
mv1.visitMaxs(...); mv1.visitEnd();
...
mv2.visitMaxs(...); mv2.visitEnd();

cv.visitEnd();
1
2
3
4
5
6
7
8
9
10
11
12
13
14

ASM 提供了三个基于 MethodVisitor API 的核心组件,用于生成和转换方法:

  • ClassReader 类分析已编译方法的内容,在其 accept 方法的参数中传送了 ClassVisitor , ClassReader 类将针 对 这一 ClassVisitor 返回的 MethodVisitor 对象调用相应方法。
  • ClassWriter 的 visitMethod 方法返回 MethodVisitor 接口的一个实现,它直接以二进制形式生成已编译方法。
  • MethodVisitor 类将它接收到的所有方法调用委托给另一个MethodVisitor 方法。可以将它看作一个事件筛选器。
  1. ClassWriter 选项

在 3.1.5 节已经看到,为一个方法计算栈映射帧并不是非常容易:必须计算所有帧,找出与跳转目标相对应的帧,或者跳在无条件跳转之后的帧,最后压缩剩余帧。与此类似,为一个方法计算局部变量与操作数栈部分的大小要容易一些,但依然算不上非常容易。

幸好 ASM 能为我们完成这一计算。在创建 ClassWriter 时,可以指定必须自动计算哪些内容:

  • 在使用 new ClassWriter(0)时,不会自动计算任何东西。必须自行计算帧、局部变量与操作数栈的大小。
  • 在使用 new ClassWriter(ClassWriter.COMPUTE_MAXS)时,将为你计算局部变量与操作数栈部分的大小。还是必须调用 visitMaxs,但可以使用任何参数:它们将被忽略并重新计算。使用这一选项时,仍然必须自行计算这些帧。
  • 在 new ClassWriter(ClassWriter.COMPUTE_FRAMES)时,一切都是自动计算。不再需要调用 visitFrame,但仍然必须调用 visitMaxs(参数将被忽略并重新计算)。

这些选项的使用很方便,但有一个代价:COMPUTE_MAXS 选项使 ClassWriter 的速度降低 10%,而使用 COMPUTE_FRAMES 选项则使其降低一半。这必须与我们自行计算时所耗费的时间进行比较:在特定情况下,经常会存在一些比 ASM 所用算法更容易、更快速的计算方法,但 ASM 使用的算法必须能够处理所有情况。

注意,如果选择自行计算这些帧,可以让 ClassWriter 为你执行压缩步骤。为此,只需要用 visitFrame(F_NEW, nLocals, locals, nStack, stack)访问未压缩帧,其中的 nLocals 和 nStack 是局部变量的个数和操作数栈的大小,locals 和 stack 是包含相应类型的数组(更多细节请参阅 Javadoc)。

还要注意,为了自动计算帧,有时需要计算两个给定类的公共超类。默认情况下, ClassWriter 类会在 getCommonSuperClass 方法中进行这一计算,它会将两个类加载到 JVM 中,并使用反射 API。如果我们正在生成几个相互引用的类,那可能会导致问题,因为被引用的类可能尚未存在。在这种情况下,可以重写 getCommonSuperClass 方法来解决这一问题。

# 3.2.2 生成方法

如果 mv 是一个 MethodVisitor,则 3.1.3 节定义的 getF 方法的字节代码可以用以下方法调用生成:

mv.visitCode(); mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I"); 
mv.visitInsn(IRETURN);
mv.visitMaxs(1, 1); mv.visitEnd();
1
2
3
4

第一个调用启动字节代码的生成过程。然后是三个调用,生成这一方法的三条指令(可以看出,字节代码与 ASM API 之间的映射非常简单)。对 visitMaxs 的调用必须在已经访问了所有这些指令后执行。它用于为这个方法的执行帧定义局部变量和操作数栈部分的大小。在 3.1.3 节 可以看出,这些大小为每部分 1 个槽,最后一次调用用于结束此方法的生成过程。

setF 方法和构造器的字节代码可以用一种类似方法生成。一个更有意义的示例是 checkAndSetF 方法:

mv.visitCode(); mv.visitVarInsn(ILOAD, 1); 
Label label = new Label(); 
mv.visitJumpInsn(IFLT, label); 
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1); 
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I"); 
Label end = new Label();
mv.visitJumpInsn(GOTO, end); 
mv.visitLabel(label); 
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException"); 
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V"); 
mv.visitInsn(ATHROW);
mv.visitLabel(end); 
mv.visitFrame(F_SAME, 0, null, 0, null); 
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2); 
mv.visitEnd();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

visitCodevisitEnd 调用之间,可以看到恰好映射到 3.1.5 节末尾所示字节代码的方法调用:每条指令、标记或帧分别有个调用(仅有的例外是 label 和 end Label 对象的声明和构造)。

注意:Label 对象规定了跟在这一标记的 visitLabel 之后的指令。例如,end 规定了 RETURN 指令, 而不是随后马上要访问的帧,因为它不是一条指令。用几条标记指定同一指令是完全合法的,但一个标记只能恰好指定一条指令。换句话说,有可能用不同标记对 visitLabel 进行连续调用,但一条指令中的一个标记则必须用 visitLabel 恰好访问一次。最后一条约束是,标记不能共享,每个方法都必须拥有自己的标记。
1

# 3.2.3 转换方法

你现在应当已经猜到,方法可以像类一样进行转换,也就是使用一个方法适配器将它收到的方法调用转发出去,并进行一些修改:改变参数可用于改变各具体指令;不转发某一收到的调用将删除一条指令;在接收到的调用之间插入调用,将增加新的指令。MethodVisitor 类提供了这样一种方法适配器的基本实现,它只是转发它接收到的所有方法,而未做任何其他事情。

为了理解可以如何使用方法适配器,让我们考虑一种非常简单的适配器,删除方法中的 NOP 指令(因为它们不做任何事情,所以删除它们没有任何问题):

public class RemoveNopAdapter extends MethodVisitor {
    public RemoveNopAdapter(MethodVisitor mv) {
        super(ASM4, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if (opcode != NOP) {
            mv.visitInsn(opcode);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

这个适配器可以在一个类适配器内部使用,如下所示:

public class RemoveNopClassAdapter extends ClassVisitor {
    public RemoveNopClassAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv;
        mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (mv != null) {
            mv = new RemoveNopAdapter(mv);
        }
        return mv;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

换言之,类适配器只是构造一个方法适配器(封装链中下一个类访问器返回的方法访问器),并返回这个适配器。其效果就是构造了一个类似于类适配器链的方法适配器链(见图 3.5)。

图 3.5 RemoveNopAdapter 的程序图

但注意,这种相似性并非强制的:完全有可能构造一个与类适配器链不相似的方法适配器链。每种方法甚至还可以有一个不同的方法适配器链。例如,类适配器可以选择仅删除方法中的 NOP, 而不移除构造器中的该指令。可以执行如下:

mv=cv.visitMethod(access,name,desc,signature,exceptions);
if(mv != null && !name.equals("<init>")){
    mv  new RemoveNopAdapter(mv);
}
...
1
2
3
4
5

在这种情况下,构造器的适配器链更短一些。与之相反,构造器的适配器链也可以更长一些, 在 visitMethod 内部创建几个链接在一起的适配器。方法适配器链的拓扑结构甚至都可以不同于类适配器。例如,类适配器可能是线性的,而方法适配器链具有分支:

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv1, mv2;
    mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
    mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
    return new MultiMethodAdapter(mv1, mv2);
}
1
2
3
4
5
6

现在已经明白了如何使用方法适配器,将它们合并在一个类适配器内部,现在就来看看如何实现一个比 RemoveNopAdapter 更有意义的适配器。

# 3.2.4 无状态转换

假设我们需要测量一个程序中的每个类所花费的时间。我们需要在每个类中添加一个静态计时器字段,并需要将这个类中每个方法的执行时间添加到这个计时器字段中。换句话说,有这样一个类 C:

public class C {
    public void m() throws Exception 
    {
        Thread.sleep(100);
    }
}
1
2
3
4
5
6

我们希望将它转换为:

public class C {
    public static long timer;

    public void m() throws Exception {
        timer -= System.currentTimeMillis();
        Thread.sleep(100);
        timer += System.currentTimeMillis();
    }
}
1
2
3
4
5
6
7
8
9

为了了解可以如何在 ASM 中实现它,可以编译这两个类,并针对这两个版本比较 TraceClassVisitor 的输出(或者是使用默认的 Textifier 后端,或者是使用 ASMifier 后端)。使用默认后端时,得到下面的差异之处(以粗体表示):

GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J LSUB
PUTSTATIC C.timer : J
LDC 100
INVOKESTATIC java/lang/Thread.sleep(J)V
GETSTATIC C.timer : J
INVOKESTATIC java/lang/System.currentTimeMillis()J LADD
PUTSTATIC C.timer : J
RETURN MAXSTACK = 4
MAXLOCALS = 1
1
2
3
4
5
6
7
8
9
10

可以看到,我们必须在方法的开头增加四条指令,在返回指令之前添加四条其他指令。还需要更新操作数栈的最大尺寸。此方法代码的开头部分用 visitCode 方法访问。因此,可以通过重写方法适配器的这一方法,添加前四条指令:

public void visitCode() {
    mv.visitCode();
    mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
            "currentTimeMillis", "()J");
    mv.visitInsn(LSUB);
    mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
1
2
3
4
5
6
7
8

其中的 owner 必须被设定为所转换类的名字。现在必须在任意 RETURN 之前添加其他四条指令,还要在任何 xRETURN 或 ATHROW 之前添加,它们都是终止该方法执行过程的指令。这些指令没有任何参数,因此在 visitInsn 方法中访问。于是,可以重写这一方法,以增加指令:

public void visitInsn(int opcode) {
    if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
        mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                "currentTimeMillis", "()J");
        mv.visitInsn(LADD);
        mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
    }
    mv.visitInsn(opcode);
}
1
2
3
4
5
6
7
8
9
10

最后,必须更新操作数栈的最大大小。我们添加的指令压入两个 long 值,因此需要操作数 栈中的四个槽。在此方法的开头,操作数栈初始为空,所以我们知道在开头添加的四条指令需要一个大小为 4 的栈。还知道所插入的代码不会改变栈的状态(因为它弹出的值的数目与压入的数目相同)。因此,如果原代码需要一个大小为 s 的栈,那转换后的方法所需栈的最大大小为 max(4, s)。遗憾的是,我们还在返回指令前面添加了四条指令,我们并不知道操作数栈恰在执行这些指令之前时的大小。只知道它小于或等于 s。因此,我们只能说,在返回指令之前添加的代码可能要求操作数栈的大小达到 s+4。这种最糟情景在实际中很少发生:使用常见编译器时,RETURN 之前的操作数栈仅包含返回值,即,它的大小最多为 0、1 或 2。但如果希望处理所有可能情景, 那就需要考虑最糟情景。①必须重写 visitMaxs 方法如下:

public void visitMaxs(int maxStack, int maxLocals) {
    mv.visitMaxs(maxStack + 4, maxLocals);
}
1
2
3

当然,也可以不需要为最大栈大小操心,而是依赖 COMPUTE_MAXS 选项,此外,它会计算最优值,而不是最差情景中的值。但对于这种简单的转换,以人工更新 maxStack 并不需要花费太多精力。

现在就出现一个很有意义的问题:栈映射帧怎么样呢?原代码不包含任何帧,转换后的代码也没有包含,但这是因为我们用作示例的特定代码造成的吗?是否在某些情况下必须更新帧呢? 答案是否定的,因为 1)插入的代码并没有改变操作数栈,2) 插入代码中没有包含跳转指令,3) 原代码的跳转指令(或者更正式地说,是控制流图)没有被修改。这意味着原帧没有发生变化,而且不需要为插入代码存储新帧,所以压缩后的原帧也没有发生变化。

现在可以将所有元素一起放入相关联的 ClassVisitorMethodVisitor 子类中:

public class AddTimerAdapter extends ClassVisitor {
    private String owner;
    private boolean isInterface;

    public AddTimerAdapter(ClassVisitor cv) {
        super(ASM4, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        cv.visit(version, access, name, signature, superName, interfaces);
        owner = name;
        isInterface = (access & ACC_INTERFACE) != 0;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if (!isInterface && mv != null && !name.equals("<init>")) {
            mv = new AddTimerMethodAdapter(mv);
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        if (!isInterface) {
            FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer", "J", null, null);
            if (fv != null) {
                fv.visitEnd();
            }
        }
        cv.visitEnd();
    }

    class AddTimerMethodAdapter extends MethodVisitor {
        public AddTimerMethodAdapter(MethodVisitor mv) {
            super(ASM4, mv);
        }

        @Override
        public void visitCode() {
            mv.visitCode();
            mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                    "currentTimeMillis", "()J");
            mv.visitInsn(LSUB);
            mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
                        "currentTimeMillis", "()J");
                mv.visitInsn(LADD);
                mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
            }
            mv.visitInsn(opcode);
        }

        @Override
        public void visitMaxs(int maxStack, int maxLocals) {
            mv.visitMaxs(maxStack + 4, maxLocals);
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

① 幸好,并不一定要给出最优操作数栈大小。有可能给出任何大于或等于这个最优值的值,尽管这样可能会浪费该线程执行栈上的内存。

这个类适配器用于实例化方法适配器(构造器除外),还用于添加计时器字段,并将被转换的类的名字存储在一个可以由方法适配器访问的字段中。

# 3.2.5 有状态转换

上一节看到的转换是局部的,不会依赖于在当前指令之前访问的指令:在开头添加的代码总是相同的,而且总会被添加,对于在每个 RETURN 指令之前添加的代码也是如此。这种转换称为无状态转换。它们的实现很简单,但只有最简单的转换具有这一性质。

更复杂的转换需要记忆在当前指令之前已访问指令的状态。例如,考虑这样一个转换,它将删除所有出现的 ICONST_0 IADD 序列,这个序列的操作就是加入 0,没有什么实际效果。显然, 在访问一条 IADD 指令时,只有当上一条被访问的指令是 ICONST_0 时,才必须删除该指令。这就要求在方法适配器中存储状态。因此,这种转换被称为有状态转换。

让我们更仔细地研究一下这个例子。在访问 ICONST_0 时,只有当下一条指令是 IADD 时才必须将其删除。问题是,下一条指令还是未知的。解决方法是将是否删除它的决定推迟到下一条指令:如果下一指令是 IADD,则删除两条指令,否则,发出 ICONST_0 和当前指令。

要实现一些删除或替代某一指令序列的转换,比较方便的做法是引入一个 MethodVisitor 子类,它的 visitXxx Insn 方法调用一个公用的 visitInsn() 方法:

public abstract class PatternMethodAdapter extends MethodVisitor {
    protected final static int SEEN_NOTHING = 0;
    protected int state;

    public PatternMethodAdapter(int api, MethodVisitor mv) {
        super(api, mv);
    }

    @Overrid
    public void visitInsn(int opcode) {
        visitInsn();
        mv.visitInsn(opcode);
    }

    @Override
    public void visitIntInsn(int opcode, int operand) {
        visitInsn();
        mv.visitIntInsn(opcode, operand);
    }
...

    protected abstract void visitInsn();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

然后,上述转换可实现如下:

public class RemoveAddZeroAdapter extends PatternMethodAdapter {
    private static int SEEN_ICONST_0 = 1;

    public RemoveAddZeroAdapter(MethodVisitor mv) {
        super(ASM4, mv);
    }

    @Override
    public void visitInsn(int opcode) {
        if (state == SEEN_ICONST_0) {
            if (opcode == IADD) {
                state = SEEN_NOTHING;
                return;
            }
        }
        visitInsn();
        if (opcode == ICONST_0) {
            state = SEEN_ICONST_0;
            return;
        }
        mv.visitInsn(opcode);
    }

    @Override
    protected void visitInsn() {
        if (state == SEEN_ICONST_0) {
            mv.visitInsn(ICONST_0);
        }
        state = SEEN_NOTHING;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

visitInsn(int) 方法首先判断是否已经检测到该序列。在这种情况下,它重新初始化 state,并立即返回,其效果就是删除该序列。在其他情况下,它会调用公用的 visitInsn 方法,如果 ICONST_0 是最后一条被访问序列,它就会发出该指令。于是,如果当前指令是 ICONST_0,它会记住这个事实并返回,延迟关于这一指令的决定。在所有其他情况下,当前指令都被转发到下一访问器。

  1. 标记和帧

在前几节已经看到,对标记和帧的访问是恰在它们的相关指令之前进行。换句话说,尽管它们本身并不是指令,但它们是与指令同时受到访问的。这对于检测指令序列的转换会有影响,但这一影响实际上是一种优势。事实上,如果删除的指令之一是一条跳转指令的目标,会发生什么情况呢?如果某一指令可能跳转到 ICONST_0,这意味着有一个指定这一指令的标记。在删除了这两条指令后,这个标记将指向跟在被删除 IADD 之后的指令,这正是我们希望的。但如果某一指令可能跳转到 IADD,我们就不能删除这个指令序列(不能确保在这一跳转之前, 已经在栈中压入了一个 0)。幸好,在这种情况下,ICONST_0IADD 之间必然有一个标记,可以很轻松地检测到它。

这一推理过程对于栈映射帧是一样的:如果访问介于两条指令之间的一个栈映射帧,那就不能删除它们。要处理这两种情况,可以将标记和帧看作是模型匹配算法中的指令。这一点可以在 PatternMethodAdapter 中完成(注意,visitMaxs 也会调用公用的 visitInsn 方法;它用于处理的情景是:方法的末尾是必须被检测序列的一个前缀):

public abstract class PatternMethodAdapter extends MethodVisitor {
    ...

    @Override
    public void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack) {
        visitInsn();
        mv.visitFrame(type, nLocal, local, nStack, stack);
    }

    @Override
    public void visitLabel(Label label) {
        visitInsn();
        mv.visitLabel(label);
    }

    @Override
    public void visitMaxs(int maxStack, int maxLocals) {
        visitInsn();
        mv.visitMaxs(maxStack, maxLocals);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在下一章将会看到,编译后的方法中可能包含有关源文件行号的信息,比如用于异常栈轨迹。这一信息用 visitLineNumber 方法访问,它也与指令同时被调用。但是,在一个指令序列的中间给出行号,对于转换或删除该指令的可能性不会产生任何影响。解决方法是在模式匹配算法中完全忽略它们。

  1. 一个更复杂的例子

上面的例子可以很轻松地推广到更复杂的指令序列。例如,考虑一个转换,它会删除对字段进行自我赋值的操作,这种操作通常是因为键入错误,比如 f = f;,或者是在字节代码中,ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f。在实现这一转换之前,最好是将状态机设计为能够识别这一序列(见图 3.6)。

图 3.6 ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f 的状态机

每个转换都标有一个条件(当前指令的值)和一个操作(必须发出的指令序列,以粗体表示)。例如,如果当前指令不是 ALOAD 0,则由 S1 转换到 S0。在这种情况下,导致进入这一状态的ALOAD 0 将被发出。注意从 S2 到其自身的转换:在发现三个或三个以上的连续 ALOAD 0 时会发生这一情况。在这种情况下,将停留在已经访问两个 ALOAD 0 的状态中,并发出第三个 ALOAD 0。找到状态机之后,相应方法适配器的编写就简单了。(8 种 Switch 情景对应于图中的 8 种转换):

class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {
    private final static int SEEN_ALOAD_0 = 1;
    private final static int SEEN_ALOAD_0ALOAD_0 = 2;
    private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;
    private String fieldOwner;
    private String fieldName;
    private String fieldDesc;

    public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {
        super(mv);
    }

    @Override
    public void visitVarInsn(int opcode, int var) {
        switch (state) {
            case SEEN_NOTHING: // S0 -> S1
                if (opcode == ALOAD && var == 0) {
                    state = SEEN_ALOAD_0;
                    return;
                }
                break;
            case SEEN_ALOAD_0: // S1 -> S2
                if (opcode == ALOAD && var == 0) {
                    state = SEEN_ALOAD_0ALOAD_0;
                    return;
                }
            case SEEN_ALOAD_0ALOAD_0: // S2 -> S2
                if (opcode == ALOAD && var == 0) {
                    mv.visitVarInsn(ALOAD, 0);
                    return;
                }
                break;
        }
        visitInsn();
        mv.visitVarInsn(opcode, var);
    }

    @Override
    public void visitFieldInsn(int opcode, String owner, String name, String desc) {
        switch (state) {
            case SEEN_ALOAD_0ALOAD_0: // S2 -> S3
                if (opcode == GETFIELD) {
                    state = SEEN_ALOAD_0ALOAD_0GETFIELD;
                    fieldOwner = owner;
                    fieldName = name;
                    fieldDesc = desc;
                    return;
                }
                break;
            case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
                if (opcode == PUTFIELD && name.equals(fieldName)) {
                    state = SEEN_NOTHING;
                    return;
                }
                break;
        }
        visitInsn();
        mv.visitFieldInsn(opcode, owner, name, desc);
    }

    @Override
    protected void visitInsn() {
        switch (state) {
            case SEEN_ALOAD_0: // S1 -> S0 mv.visitVarInsn(ALOAD, 0); break;
            case SEEN_ALOAD_0ALOAD_0: // S2 -> S0
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 0);
                break;
            case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0
                mv.visitVarInsn(ALOAD, 0);
                mv.visitVarInsn(ALOAD, 0);
                mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);
                break;
        }
        state = SEEN_NOTHING;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77

注意,出于和 3.2.4 节中 AddTimerAdapter 同样的原因,本节给出的有状态转换也不需要转换栈映射帧:原帧在转换后仍然有效。它们甚至不需要转换局部变量和操作数栈大小。最后, 还必须注意,有状态转换并不限于检测和转换指令序列的转换。许多其他类型的转换也是有状态的。比如,下一节介绍的方法适配器就属于这种情景。