深度解析插樁技術(四)ASM 探秘

成為一名優秀的Android開發,需要一份完備的知識體系,在這里,讓我們一起成長為自己所想的那樣~。

一、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) {
        // 1
        if(fieldNode.name.equals("password"))  {
            // 2
            fieldNode.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 的事件模型。

三、ASM 的事件模型(ASM Core API)

對象模型是由事件模型封裝而成,因此事件模型的上手難度會更大一些。

對于事件模型來說,它 采用了設計模式中的訪問者模式。它的出現是為了更好地解決這樣一種需求:有 A 個元素和 N 種算法,每個算法都能作用于任意一個元素,并且在不同的元素上有不同的運行方式

在訪問者模式出現之前,我們通常會在每一個元素對應的類中添加 N 個方法,然后在每一個方法中去實現一個算法,但是,這樣的做法容易導致代碼耦合性過高,并且可維護性差。

因此,訪問者模式應運而生,我們可以 建立 N 個訪問者,并且每一個訪問者擁有一個算法及其內部的 A 種運行方式。當我們需要調用一個算法時,就讓相應的訪問者去訪問元素,然后讓訪問者根據被訪問對象選擇相應的算法

需要注意的是,訪問者并沒有直接去操作元素,而是先讓元素類調用 accept 方法接收訪問者,然后,訪問者在元素類的內部方法中開始調用 visit 方法訪問當前的元素類。這樣,訪問者便能直接訪問元素類中的內部私有成員,其優勢在于 避免了暴露不必要的內部細節

要理解 ASM 的事件模型,我們就需要對其中的 兩個重要成員的工作原理 有較深的了解。它們便是 類訪問者 ClassVisitor 與 類讀取(解析)者 ClassReader

從字節碼的視角中,一個 Java 類由很多組件凝聚而成,而這之中便包括超類、接口、屬性、域和方法等等。當我們在使用 ASM 進行操控時,可以將它們視為一個個與之對應的事件。因此 ASM 提供了一個 類訪問者 ClassVisitor,以通過它來訪問當前類的各個組件,當解析器 ClassReader 依次遇到上述的各個組件時,ClassVisitor 上對應的 visitor 事件處理器方法均會被一一調用

與類相似,方法也是由多個組件凝聚而成的,其對應著方法屬性、注解及編譯后的代碼(Class 字節碼)。ASM 的 MethodVisitor 提供了一種 hook(鉤子)機制,以便能夠訪問方法中的每一個操作碼,這樣我們便能夠對字節碼文件進行細粒度地修改

下面,我們便來一一分析下它們。

1、類訪問者 ClassVisitor

通常我們在使用 ASM 的訪問者模式有一個模板代碼,如下所示:

    InputStream is = new FileInputStream(classFile);
    // 1
    ClassReader classReader = new ClassReader(is);
    // 2
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    // 3
    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
    // 4
    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
復制代碼

首先,在注釋1處,我們 將目標文件轉換為流的形式,并將它融入類讀取器 ClassReader 之中。然后,在注釋2處,我們 構建了一個類寫入器 ClassWriter,其參數 COMPUTE_MAXS 的作用是將自動計算本地變量表最大值和操作數棧最大值的任務托付給了ASM。接著,在注釋3處,新建了一個自定義的類訪問器,這個自定義的 ClassVisitor 的作用是為了在每一個方法的開始和結尾處插入相應的記時代碼,以便統計出每一個方法的耗時。最后,在注釋4處,類讀取器 ClassReader 實例這個被訪問者調用了自身的 accept 方法接收了一個 classVisitor 實例,需要注意的是,第二個參數指定了 EXPAND_FRAMES,旨在說明在讀取 class 的時候需要同時展開棧映射幀(StackMap Frame),如果我們需要使用自定義的 MethodVisitor 去修改方法中的指令時必須要指定這個參數,。

上面,我們說到了棧映射幀(StackMap Frame),它到底是什么呢?

棧映射幀 StackMap Frame

它是 Java 6 以后引入的一種驗證機制,用于 檢驗 Java 字節碼的正確性。它的工作方式是 記錄每一個關鍵步驟完成后其方法中操作數棧的理論狀態,然后,在實際運行的時候,ASM 會將其實際狀態和理論狀態對比,如果狀態不一致則表明出現了錯誤

但棧映射幀的實現并不簡單,因此通過調用 classReader 實例的 accept 方法我們便可以讓 ASM 自動去計算棧映射幀,盡管這 會增加 50% 的額外運算。此外,可能會有小概率的情況遇到 棧映射幀驗證失敗 的情況,例如:VerifyError: Inconsistent stackmap frames at branch target 這個錯誤。

最常見的原因可能就是由于 字節碼寫錯造成的,此時,我們應該去檢查對應的字節碼實現代碼。此外,也可能是 JDK 版本的支持問題或是 ASM 自身的缺陷,但是,這種情況幾乎不會發生。

2、類讀取(解析)者 ClassVisitor

現在,讓我們再回到上述注釋4處的代碼,在這里,我們調用了 classReader 的 accept 方法接收了一個訪問者 classVisitor,下面,我們來看看其內部的實現,代碼如下所示(源碼實現較長,這里我們只需關注注釋處的代碼即可:

    /**
     * Makes the given visitor visit the Java class of this {@link ClassReader}
     * . This class is the one specified in the constructor (see
     * {@link #ClassReader(byte[]) ClassReader}).
     * 
     * @param classVisitor
     *            the visitor that must visit this class.
     * @param flags
     *            option flags that can be used to modify the default behavior
     *            of this class. See {@link #SKIP_DEBUG}, {@link #EXPAND_FRAMES}
     *            , {@link #SKIP_FRAMES}, {@link #SKIP_CODE}.
     */
    public void accept(final ClassVisitor classVisitor, final int flags) {
        accept(classVisitor, new Attribute[0], flags);
    }
復制代碼

在 accept 方法中又繼續調用了 classReader 的另一個 accept 重載方法,如下所示:

    public void accept(final ClassVisitor classVisitor,
            final Attribute[] attrs, final int flags) {
        int u = header; // current offset in the class file
        char[] c = new char[maxStringLength]; // buffer used to read strings

        Context context = new Context();
        context.attrs = attrs;
        context.flags = flags;
        context.buffer = c;

        // 1、讀取類的描述信息,例如 access、name 等等
        int access = readUnsignedShort(u);
        String name = readClass(u + 2, c);
        String superClass = readClass(u + 4, c);
        String[] interfaces = new String[readUnsignedShort(u + 6)];
        u += 8;
        for (int i = 0; i < interfaces.length; ++i) {
            interfaces[i] = readClass(u, c);
            u += 2;
        }

        // 2、讀取類的屬性信息,例如簽名 signature、sourceFile 等等。
        String signature = null;
        String sourceFile = null;
        String sourceDebug = null;
        String enclosingOwner = null;
        String enclosingName = null;
        String enclosingDesc = null;
        int anns = 0;
        int ianns = 0;
        int tanns = 0;
        int itanns = 0;
        int innerClasses = 0;
        Attribute attributes = null;

        u = getAttributes();
        for (int i = readUnsignedShort(u); i > 0; --i) {
            String attrName = readUTF8(u + 2, c);
            // tests are sorted in decreasing frequency order
            // (based on frequencies observed on typical classes)
            if ("SourceFile".equals(attrName)) {
                sourceFile = readUTF8(u + 8, c);
            } else if ("InnerClasses".equals(attrName)) {
                innerClasses = u + 8;
            } else if ("EnclosingMethod".equals(attrName)) {
                enclosingOwner = readClass(u + 8, c);
                int item = readUnsignedShort(u + 10);
                if (item != 0) {
                    enclosingName = readUTF8(items[item], c);
                    enclosingDesc = readUTF8(items[item] + 2, c);
                }
            } else if (SIGNATURES && "Signature".equals(attrName)) {
                signature = readUTF8(u + 8, c);
            } else if (ANNOTATIONS
                    && "RuntimeVisibleAnnotations".equals(attrName)) {
                anns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeVisibleTypeAnnotations".equals(attrName)) {
                tanns = u + 8;
            } else if ("Deprecated".equals(attrName)) {
                access |= Opcodes.ACC_DEPRECATED;
            } else if ("Synthetic".equals(attrName)) {
                access |= Opcodes.ACC_SYNTHETIC
                        | ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
            } else if ("SourceDebugExtension".equals(attrName)) {
                int len = readInt(u + 4);
                sourceDebug = readUTF(u + 8, len, new char[len]);
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleAnnotations".equals(attrName)) {
                ianns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleTypeAnnotations".equals(attrName)) {
                itanns = u + 8;
            } else if ("BootstrapMethods".equals(attrName)) {
                int[] bootstrapMethods = new int[readUnsignedShort(u + 8)];
                for (int j = 0, v = u + 10; j < bootstrapMethods.length; j++) {
                    bootstrapMethods[j] = v;
                    v += 2 + readUnsignedShort(v + 2) << 1;
                }
                context.bootstrapMethods = bootstrapMethods;
            } else {
                Attribute attr = readAttribute(attrs, attrName, u + 8,
                        readInt(u + 4), c, -1, null);
                if (attr != null) {
                    attr.next = attributes;
                    attributes = attr;
                }
            }
            u += 6 + readInt(u + 4);
        }

        // 3、訪問類的描述信息
        classVisitor.visit(readInt(items[1] - 7), access, name, signature,
                superClass, interfaces);

        // 4、訪問源碼和 debug 信息
        if ((flags & SKIP_DEBUG) == 0
                && (sourceFile != null || sourceDebug != null)) {
            classVisitor.visitSource(sourceFile, sourceDebug);
        }

        // 5、訪問外部類
        if (enclosingOwner != null) {
            classVisitor.visitOuterClass(enclosingOwner, enclosingName,
                    enclosingDesc);
        }

        // 6、訪問類注解和類型注解
        if (ANNOTATIONS && anns != 0) {
            for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitAnnotation(readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && ianns != 0) {
            for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitAnnotation(readUTF8(v, c), false));
            }
        }
        if (ANNOTATIONS && tanns != 0) {
            for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && itanns != 0) {
            for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        classVisitor.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), false));
            }
        }

        // 7、訪問類的屬性
        while (attributes != null) {
            Attribute attr = attributes.next;
            attributes.next = null;
            classVisitor.visitAttribute(attributes);
            attributes = attr;
        }

        // 8、訪問內部類
        if (innerClasses != 0) {
            int v = innerClasses + 2;
            for (int i = readUnsignedShort(innerClasses); i > 0; --i) {
                classVisitor.visitInnerClass(readClass(v, c),
                        readClass(v + 2, c), readUTF8(v + 4, c),
                        readUnsignedShort(v + 6));
                v += 8;
            }
        }

        // 9、訪問字段和方法
        u = header + 10 + 2 * interfaces.length;
        for (int i = readUnsignedShort(u - 2); i > 0; --i) {
            u = readField(classVisitor, context, u);
        }
        u += 2;
        for (int i = readUnsignedShort(u - 2); i > 0; --i) {
            u = readMethod(classVisitor, context, u);
        }

        // 訪問當前類結束時調用
        classVisitor.visitEnd();
    }
復制代碼

首先,在 classReader 實例的 accept 方法中的注釋1和注釋2處,我們會 先開始進行類相關的字節碼解析的工作:讀取了類的描述和屬性信息。接著,在注釋3 ~ 注釋8處,我們調用了 classVisitor 一系列的 visitxxx 方法訪問 classReader 解析完字節碼后保存在內存的信息。然后,在注釋9處,分別調用了 readField 方法和 readMethod 方法去訪問類中的方法和字段。最后,調用 classVisitor 的 visitEnd 標識已訪問結束

1)、類內字段的解析

這里,我們先來看看 readField 的源碼實現,如下所示:

    /**
     * Reads a field and makes the given visitor visit it.
     * 
     * @param classVisitor
     *            the visitor that must visit the field.
     * @param context
     *            information about the class being parsed.
     * @param u
     *            the start offset of the field in the class file.
     * @return the offset of the first byte following the field in the class.
     */
    private int readField(final ClassVisitor classVisitor,
            final Context context, int u) {
        // 1、讀取字段的描述信息
        char[] c = context.buffer;
        int access = readUnsignedShort(u);
        String name = readUTF8(u + 2, c);
        String desc = readUTF8(u + 4, c);
        u += 6;

        // 2、讀取字段的屬性
        String signature = null;
        int anns = 0;
        int ianns = 0;
        int tanns = 0;
        int itanns = 0;
        Object value = null;
        Attribute attributes = null;

        for (int i = readUnsignedShort(u); i > 0; --i) {
            String attrName = readUTF8(u + 2, c);
            // tests are sorted in decreasing frequency order
            // (based on frequencies observed on typical classes)
            if ("ConstantValue".equals(attrName)) {
                int item = readUnsignedShort(u + 8);
                value = item == 0 ? null : readConst(item, c);
            } else if (SIGNATURES && "Signature".equals(attrName)) {
                signature = readUTF8(u + 8, c);
            } else if ("Deprecated".equals(attrName)) {
                access |= Opcodes.ACC_DEPRECATED;
            } else if ("Synthetic".equals(attrName)) {
                access |= Opcodes.ACC_SYNTHETIC
                        | ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
            } else if (ANNOTATIONS
                    && "RuntimeVisibleAnnotations".equals(attrName)) {
                anns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeVisibleTypeAnnotations".equals(attrName)) {
                tanns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleAnnotations".equals(attrName)) {
                ianns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleTypeAnnotations".equals(attrName)) {
                itanns = u + 8;
            } else {
                Attribute attr = readAttribute(context.attrs, attrName, u + 8,
                        readInt(u + 4), c, -1, null);
                if (attr != null) {
                    attr.next = attributes;
                    attributes = attr;
                }
            }
            u += 6 + readInt(u + 4);
        }
        u += 2;

        // 3、訪問字段的聲明
        FieldVisitor fv = classVisitor.visitField(access, name, desc,
                signature, value);
        if (fv == null) {
            return u;
        }

        // 4、訪問字段的注解和類型注解
        if (ANNOTATIONS && anns != 0) {
            for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        fv.visitAnnotation(readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && ianns != 0) {
            for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        fv.visitAnnotation(readUTF8(v, c), false));
            }
        }
        if (ANNOTATIONS && tanns != 0) {
            for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        fv.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && itanns != 0) {
            for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        fv.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), false));
            }
        }

        // 5、訪問字段的屬性
        while (attributes != null) {
            Attribute attr = attributes.next;
            attributes.next = null;
            fv.visitAttribute(attributes);
            attributes = attr;
        }

        // 訪問字段結束時調用
        fv.visitEnd();

        return u;
    }
復制代碼

同讀取類信息的時候類似,首先,在注釋1和注釋2處,會 先開始進行字段相關的字節碼解析的工作:讀取了字段的描述和屬性信息。然后,在注釋3 ~ 注釋5處 按順序訪問了字段的描述、注解、類型注解及其屬性信息。最后,調用了 FieldVisitor 實例的 visitEnd 方法結束了字段信息的訪問

2)、類內方法的解析

下面,我們在看看 readMethod 的實現代碼,如下所示:

    /**
     * Reads a method and makes the given visitor visit it.
     * 
     * @param classVisitor
     *            the visitor that must visit the method.
     * @param context
     *            information about the class being parsed.
     * @param u
     *            the start offset of the method in the class file.
     * @return the offset of the first byte following the method in the class.
     */
    private int readMethod(final ClassVisitor classVisitor,
            final Context context, int u) {
        // 1、讀取方法描述信息
        char[] c = context.buffer;
        context.access = readUnsignedShort(u);
        context.name = readUTF8(u + 2, c);
        context.desc = readUTF8(u + 4, c);
        u += 6;

        // 2、讀取方法屬性信息
        int code = 0;
        int exception = 0;
        String[] exceptions = null;
        String signature = null;
        int methodParameters = 0;
        int anns = 0;
        int ianns = 0;
        int tanns = 0;
        int itanns = 0;
        int dann = 0;
        int mpanns = 0;
        int impanns = 0;
        int firstAttribute = u;
        Attribute attributes = null;

        for (int i = readUnsignedShort(u); i > 0; --i) {
            String attrName = readUTF8(u + 2, c);
            // tests are sorted in decreasing frequency order
            // (based on frequencies observed on typical classes)
            if ("Code".equals(attrName)) {
                if ((context.flags & SKIP_CODE) == 0) {
                    code = u + 8;
                }
            } else if ("Exceptions".equals(attrName)) {
                exceptions = new String[readUnsignedShort(u + 8)];
                exception = u + 10;
                for (int j = 0; j < exceptions.length; ++j) {
                    exceptions[j] = readClass(exception, c);
                    exception += 2;
                }
            } else if (SIGNATURES && "Signature".equals(attrName)) {
                signature = readUTF8(u + 8, c);
            } else if ("Deprecated".equals(attrName)) {
                context.access |= Opcodes.ACC_DEPRECATED;
            } else if (ANNOTATIONS
                    && "RuntimeVisibleAnnotations".equals(attrName)) {
                anns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeVisibleTypeAnnotations".equals(attrName)) {
                tanns = u + 8;
            } else if (ANNOTATIONS && "AnnotationDefault".equals(attrName)) {
                dann = u + 8;
            } else if ("Synthetic".equals(attrName)) {
                context.access |= Opcodes.ACC_SYNTHETIC
                        | ClassWriter.ACC_SYNTHETIC_ATTRIBUTE;
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleAnnotations".equals(attrName)) {
                ianns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleTypeAnnotations".equals(attrName)) {
                itanns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeVisibleParameterAnnotations".equals(attrName)) {
                mpanns = u + 8;
            } else if (ANNOTATIONS
                    && "RuntimeInvisibleParameterAnnotations".equals(attrName)) {
                impanns = u + 8;
            } else if ("MethodParameters".equals(attrName)) {
                methodParameters = u + 8;
            } else {
                Attribute attr = readAttribute(context.attrs, attrName, u + 8,
                        readInt(u + 4), c, -1, null);
                if (attr != null) {
                    attr.next = attributes;
                    attributes = attr;
                }
            }
            u += 6 + readInt(u + 4);
        }
        u += 2;

        // 3、訪問方法描述信息
        MethodVisitor mv = classVisitor.visitMethod(context.access,
                context.name, context.desc, signature, exceptions);
        if (mv == null) {
            return u;
        }

        /*
         * if the returned MethodVisitor is in fact a MethodWriter, it means
         * there is no method adapter between the reader and the writer. If, in
         * addition, the writers constant pool was copied from this reader
         * (mw.cw.cr == this), and the signature and exceptions of the method
         * have not been changed, then it is possible to skip all visit events
         * and just copy the original code of the method to the writer (the
         * access, name and descriptor can have been changed, this is not
         * important since they are not copied as is from the reader).
         */
        if (WRITER && mv instanceof MethodWriter) {
            MethodWriter mw = (MethodWriter) mv;
            if (mw.cw.cr == this && signature == mw.signature) {
                boolean sameExceptions = false;
                if (exceptions == null) {
                    sameExceptions = mw.exceptionCount == 0;
                } else if (exceptions.length == mw.exceptionCount) {
                    sameExceptions = true;
                    for (int j = exceptions.length - 1; j >= 0; --j) {
                        exception -= 2;
                        if (mw.exceptions[j] != readUnsignedShort(exception)) {
                            sameExceptions = false;
                            break;
                        }
                    }
                }
                if (sameExceptions) {
                    /*
                     * we do not copy directly the code into MethodWriter to
                     * save a byte array copy operation. The real copy will be
                     * done in ClassWriter.toByteArray().
                     */
                    mw.classReaderOffset = firstAttribute;
                    mw.classReaderLength = u - firstAttribute;
                    return u;
                }
            }
        }

        // 4、訪問方法參數信息
        if (methodParameters != 0) {
            for (int i = b[methodParameters] & 0xFF, v = methodParameters + 1; i > 0; --i, v = v + 4) {
                mv.visitParameter(readUTF8(v, c), readUnsignedShort(v + 2));
            }
        }

        // 5、訪問方法的注解信息
        if (ANNOTATIONS && dann != 0) {
            AnnotationVisitor dv = mv.visitAnnotationDefault();
            readAnnotationValue(dann, c, null, dv);
            if (dv != null) {
                dv.visitEnd();
            }
        }
        if (ANNOTATIONS && anns != 0) {
            for (int i = readUnsignedShort(anns), v = anns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        mv.visitAnnotation(readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && ianns != 0) {
            for (int i = readUnsignedShort(ianns), v = ianns + 2; i > 0; --i) {
                v = readAnnotationValues(v + 2, c, true,
                        mv.visitAnnotation(readUTF8(v, c), false));
            }
        }
        if (ANNOTATIONS && tanns != 0) {
            for (int i = readUnsignedShort(tanns), v = tanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        mv.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), true));
            }
        }
        if (ANNOTATIONS && itanns != 0) {
            for (int i = readUnsignedShort(itanns), v = itanns + 2; i > 0; --i) {
                v = readAnnotationTarget(context, v);
                v = readAnnotationValues(v + 2, c, true,
                        mv.visitTypeAnnotation(context.typeRef,
                                context.typePath, readUTF8(v, c), false));
            }
        }
        if (ANNOTATIONS && mpanns != 0) {
            readParameterAnnotations(mv, context, mpanns, true);
        }
        if (ANNOTATIONS && impanns != 0) {
            readParameterAnnotations(mv, context, impanns, false);
        }

        // 6、訪問方法的屬性信息
        while (attributes != null) {
            Attribute attr = attributes.next;
            attributes.next = null;
            mv.visitAttribute(attributes);
            attributes = attr;
        }

        // 7、訪問方法代碼對應的字節碼信息
        if (code != 0) {
            mv.visitCode();
            readCode(mv, context, code);
        }

        // 8、visits the end of the method
        mv.visitEnd();

        return u;
    }
復制代碼

同類和字段的讀取、訪問套路一樣,首先,在注釋1和注釋2處,會 先開始進行方法相關的字節碼解析的工作:讀取了方法的描述和屬性信息。然后,在注釋3 ~ 注釋7處 按順序訪問了方法的描述、參數、注解、屬性、方法代碼對應的字節碼信息。需要注意的是,在 readCode 方法中,也是先讀取了方法內部代碼的字節碼信息,例如頭部、屬性等等,然后,便會訪問對應的指令集。最后,在注釋8處 調用了 MethodVisitor 實例的 visitEnd 方法結束了方法信息的訪問

從以上對 ClassVisitor 與 ClassReader 的分析看來,ClassVisitor 被定義為了一個能接收并解析 ClassReader 傳入信息的類。當在 accpet 方法中 ClassVisitor 訪問 ClassReader 時,ClassReader 便會先開始字節碼的解析工作,并將保存在內存中的結果源源不斷地通過調用各種 visitxxx 方法傳入到 ClassVisitor 之中

需要注意的是,其中 只有 visit 這個方法一定會被調用一次,因為它 獲取了類頭部的描述信息,顯然易見,它必不可少,而對于其它的 visitxxx 方法來說都不能確定。例如其中的 visitMethod 方法,只有當 ClassReader 解析出一個方法的字節碼時,才會調用一次 visitMethod 方法,并由此生成一個方法訪問者 MethodVisitor 的實例。

然后,這個 MethodVisitor 的實例便會同 ClassVisitor 一樣開始訪問當前方法的屬性信息,對于 ClassVisitor 來說,它只處理和類相關的事,而方法的事情被外包給了 MethodVisitor 進行處理。這正是訪問者的一大優勢:將訪問一個復雜事物的職責通過各個不同類型但又相互關聯的訪問者分割開來

由前可知,對象模型是事件模型的一個封裝。其中的 ClassNode 其實就是 ClassVisitor 的一個子類,它負責將 ClassReader 傳進來的信息進行分類儲存。同樣,MethodNode 也是 MethodVisitor 的一個子類,它負責將 ClassReader 傳進來的操作碼指令信息連接成一個列表并保存其中

而 ClassWriter 也是 ClassVisitor 的一個子類,但是,它并不會儲存信息,而是馬上會將傳入的信息轉譯成字節碼,并在之后隨時輸出它們。對于 ClassReader 這個被訪問者來說,它負責讀取我們傳入的類文件中的字節流數據,并提供解析流中包含的一切類屬性信息的操作

最后,為了更進一步地將我們上面所講解的 ClassReader 與 ClassVisitor 的工作機制更加形象化,這里借用 hakugyokurou 的一張流程圖用于回顧梳理,如下所示:

注意:第二個"實例化,通過構造函數..."需要去掉

3、小結

ASM Core API 類似于解析 XML 文件中的 SAX 方式,直接用流式的方法來處理字節碼文件,而不需要把這個類的整個結構讀進內存之中。其好處是能夠盡可能地節約內存,難度在于編程時需要有一定的 JVM 字節碼基礎。由于它的性能較好,所以通常情況下我們都會直接使用 Core API。下面,我們再來回顧下 事件模型中 Core API 的關鍵組件,如下所示:

  • 1)、ClassReader用于讀取已經編譯好的 .class 文件
  • 2)、ClassWriter用于重新構建編譯后的類,如修改類名、屬性以及方法,也可以生成新的類的字節碼文件
  • 3)、各種 Visitor 類如上所述,Core API 根據字節碼從上到下依次處理,對于字節碼文件中不同的區域有不同的 Visitor,比如用于訪問方法的 MethodVisitor、用于訪問類變量的 FieldVisitor、用于訪問注解的 AnnotationVisitor 等等。為了實現 AOP,其重點是要靈活運用 MethodVisitor

四、綜合實戰訓練

在開始使用 ASM Core API 之前,我們需要先了解一下 ASM Bytecode Outline 工具的使用。

1、使用 ASM Bytecode Outline

當我們使用 ASM 手寫字節碼的時候,通常會寫一系列 visitXXXXInsn() 方法來寫對應的助記符,所以 需要先將每一行源代碼轉化對應的助記符,然后再通過 ASM 的語法轉換為與之對應的 visitXXXXInsn()。為了解決這個繁瑣耗時的流程,因此,ASM Bytecode Outline 便應運而生。

首先,我們需要安裝 ASM Bytecode Outline gradle 插件,安裝完成后,我們就可以 直接在目標類中右鍵選擇下拉框底部區域的 Show Bytecode outline然后,AS 的右側就會出現目標類對應的字節碼與 ASM 信息查看區域。我們直接 在新標簽頁中選擇 ASMified 這個 tab 即可看到其與之對應的 ASM 代碼,如下圖所示:

為了更好地在實踐中理解上面所學到的知識,我們可以 使用 ASM 插樁實現方法耗時的統計替換項目中所有的 new Thread。這里直接給出 Android 開發高手課的 ASM實戰項目地址

注意:如果當前需使用 ASM Outline 的類引用了工程中其它的 .java 文件,需要先使用 ASM Outline 將其編譯成 .class 文件。

2、使用 ASM 編譯插樁統計方法耗時

使用 ASM 編譯插樁統計方法耗時主要可以細分為如下三個步驟:

  • 1)、首先,我們需要通過自定義 gradle plugin 的形式來干預編譯過程
  • 2)、然后,在編譯過程中獲取到所有的 class 文件和 jar 包,然后遍歷他們
  • 3)、最后,利用 ASM 來修改字節碼,達到插樁的目的

剛開始的時候,我們可以在 Application 的 onCreate 方法 先寫下要插樁之后的代碼,如下所示:

    @Override
    public void onCreate() {
        long startTime = System.currentTimeMillis();
        super.onCreate();
        long endTime = System.currentTimeMillis() - startTime;
        StringBuilder sb = new StringBuilder();
        sb.append("com/sample/asm/SampleApplication.onCreate time: ");
        sb.append(endTime);
        Log.d("MethodCostTime", sb.toString());
    }
復制代碼

這樣便于 之后能使用 ASM Bytecode Outline 的 ASMified 下的 Show differences 去展示相鄰兩次修改的代碼差異,其修改之后 ASM 代碼對比圖如下所示:

在右圖中所示的差異代碼就是我們需要添加的 ASM 代碼。這里我們直接使用 ASM 的事件模式,即 ASM 的 Core API 來進行字節碼的讀取與修改,代碼如下所示:

    ClassReader classReader = new ClassReader(is);
    ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    // 1
    ClassVisitor classVisitor = new TraceClassAdapter(Opcodes.ASM5, classWriter);
    classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
復制代碼

上面的實現代碼我們在上面已經詳細分析過了,當 classReader 調用 accept 方法時就會對類文件進行讀取和被 classVisitor 訪問。那么,我們是如何對方法中的字節碼進行操作的呢?

在注釋1處,我們 自定義了一個 ClassVisitor,其中的奧秘之處就在其中,其實現代碼如下所示:

    public static class TraceClassAdapter extends ClassVisitor {

        private String className;

        TraceClassAdapter(int i, ClassVisitor classVisitor) {
            super(i, 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.className = name;

        }

        @Override
        public void visitInnerClass(final String s, final String s1, final String s2, final int i) {
            super.visitInnerClass(s, s1, s2, i);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc,
                                         String signature, String[] exceptions) {

            MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
            return new TraceMethodAdapter(api, methodVisitor, access, name, desc, this.className);
        }

        @Override
        public void visitEnd() {
            super.visitEnd();
        }
    }
復制代碼

由于我們只需要對方法的字節碼進行操作,直接處理 visitMethod 這個方法即可。在這里我們直接將類觀察者 ClassVisitor 通過訪問得到的 MethodVisitor 進行了封裝,使用了自定義的 AdviceAdapter 的方式來實現,而 AdviceAdapter 也是 MethodVisitor 的子類,不同于 MethodVisitor的是,它自身提供了 onMethodEnter 與 onMethodExit 方法,非常便于我們去實現方法的前后插樁。其實現代碼如下所示:

    private int timeLocalIndex = 0;

    @Override
    protected void onMethodEnter() {
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        // 1
        timeLocalIndex = newLocal(Type.LONG_TYPE);
        mv.visitVarInsn(LSTORE, timeLocalIndex);
    }

    @Override
    protected void onMethodExit(int opcode) {
        mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
        mv.visitVarInsn(LLOAD, timeLocalIndex);
        // 此處的值在棧頂
        mv.visitInsn(LSUB);
        // 因為后面要用到這個值所以先將其保存到本地變量表中
        mv.visitVarInsn(LSTORE, timeLocalIndex);

        int stringBuilderIndex = newLocal(Type.getType("java/lang/StringBuilder"));
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "<init>", "()V", false);
        // 需要將棧頂的 stringbuilder 指針保存起來否則后面找不到了
        mv.visitVarInsn(Opcodes.ASTORE, stringBuilderIndex);
        mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
        mv.visitLdcInsn(className + "." + methodName + " time:");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitInsn(Opcodes.POP);
        mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
        mv.visitVarInsn(Opcodes.LLOAD, timeLocalIndex);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(J)Ljava/lang/StringBuilder;", false);
        mv.visitInsn(Opcodes.POP);
        mv.visitLdcInsn("Geek");
        mv.visitVarInsn(Opcodes.ALOAD, stringBuilderIndex);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        // 注意: Log.d 方法是有返回值的,需要 pop 出去
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        // 2
        mv.visitInsn(Opcodes.POP);
    }
復制代碼

首先,在 onMethodEnter 方法中的注釋1處,我們調用了 AdviceAdapter 的其中一個父類 LocalVariablesSorter 的 newLocal 方法,它會根據指定的類型創建一個新的本地變量,并直接分配一個本地變量的引用 index,其優勢在于可以盡量復用以前的局部變量,而不需要我們考慮本地變量的分配和覆蓋問題。然后,在 onMethodExit 方法中我們便可以將之前的差異代碼拿過來適當修改調試即可,需要注意的是,在注釋2處,即 onMethodExit 方法的最后需要保證棧的清潔,避免在棧頂遺留下不使用的數據,如果在棧頂還留有數據的話,不僅會導致后續代碼的異常,也會對其他框架處理字節碼造成影響,因此如果操作數棧還有數據的話需要消耗掉或者 POP 出去

3、全局替換項目中所有的 new Thread

首先,我們先將 MainActivity 的 startThread 方法里面的 Thread 對象改變成 CustomThread,然后通過 ASM Bytecode Outline 的 Show differences 查看在字節碼上面的差異,如下圖所示:

我們注意到,這里首先調用了 NEW 操作碼創建了 thread 實例,然后才調用了 InvokeVirtual 操作碼去執行 thread 實例的構造方法。通常情況下這兩條指令是成對出現的,但是,偶爾會遇到從其他某個位置傳遞過來一個已經存在的實例,并直接強制調用構造方法的情況。因此,我們 需要在代碼里面判斷 new 和 InvokeSpecial 是否是成對出現的。其實現代碼如下所示:

    private final String methodName;
    private final String className;
    // 標識是否遇到了 new 指令
    private boolean find = false;

    protected TraceMethodAdapter(int api, MethodVisitor mv, int access, String name, String desc, String className) {
        super(api, mv, access, name, desc);
        this.className = className;
        this.methodName = name;
    }

    @Override
    public void visitTypeInsn(int opcode, String s) {
        // 方法可以像類一樣就行轉換,例如,通過使用一個方法適配器來轉發 
        // 那些帶有修改的方法調用:改變參數可以被用來變更指令,不轉發
        // 某個方法調用可以刪除一 個指令,插入新的調用可以添加新的指令
        if (opcode == Opcodes.NEW && "java/lang/Thread".equals(s)) {
            // 遇到 new 指令
            find = true;
            mv.visitTypeInsn(Opcodes.NEW, "com/sample/asm/CustomThread");
                return;
        }
        super.visitTypeInsn(opcode, s);
    }

    @Override
    public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
        //需要排查 CustomThread 自己
        if ("java/lang/Thread".equals(owner) && !className.equals("com/sample/asm/CustomThread") && opcode == Opcodes.INVOKESPECIAL && find) {
            find = false;
            mv.visitMethodInsn(opcode, "com/sample/asm/CustomThread", name, desc, itf);
            Log.e("asmcode", "className:%s, method:%s, name:%s", className, methodName, name);
                return;
        }
        super.visitMethodInsn(opcode, owner, name, desc, itf);
    }
復制代碼

在使用 ASM 進行插樁的時候,我們尤其需要注意以下 兩點

  • 1)、當我們使用 ASM 處理字節碼時,需要 逐步小量的修改、驗證,切記不要編寫大量的字節碼并希望它們能夠立即通過驗證并且可以馬上執行。比較穩妥的做法是,每編寫一行字節碼時就考慮一下操作數棧與局部變量表之間的變化情況,確定無誤之后再寫下一行。此外,除了 JVM 的驗證器之外,ASM 還維護了一個單獨的字節碼驗證器,它也會檢查你的字節碼實現是否符合 JVM 規范
  • 2)、注意本地變量表和操作數棧的數據交換以及 try catch blcok 的處理,關于異常處理可以使用 ASM 提供的 CheckClassAdapter,可以在修改完成后驗證一下字節碼是否正常

除了直接使用 ASM 進行插樁之外,如果需求比較簡單,我們可以使用基于 ASM 的字節碼處理工具,例如:lancetHunterHibeaver,此時使用它們的投入產出比會更高。

五、總結

在 ASM Bytecode Outline 工具的幫助下,我們能夠完成很多場景下的 ASM 插樁的需求,但是,當我們使用其處理字節碼的時候還是需要考慮很多種可能出現的情況。如果想要具備這方面的深度思考能力,我們就 必須對每一個操作碼的特征都有較深的了解,如果還不了解的同學可以去看看 《深入探索編譯插樁技術(三、JVM字節碼)。因此,要具備實現一個復雜 ASM 插樁的能力,我們需要對 JVM 字節碼、ASM 字節碼以及 ASM 源碼中的核心工具類的實現 做到了然于心,并且在不斷地實踐與試錯之后,我們才能夠成為一個真正的 ASM 插樁高手

作者:jsonchao
鏈接:https://juejin.cn/post/6844904118700474375
來源:掘金
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容