深刻探索編譯插樁技術(4、ASM 探祕)

前言

成爲一名優秀的Android開發,須要一份完備的 知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

《深刻探索編譯插樁技術(2、AspectJ)》 一文中咱們深刻學習了 AspectJ 在 Android 下的使用。能夠看到 AspectJ 很是強大,可是它也只能實現 50% 的字節碼操做場景,若是想要實現 100% 的字節碼操做場景,那麼就不得不使用 ASM。html

此外,AspectJ 有着一系列弊端: 因爲其基於規則,因此其切入點相對固定,對於字節碼文件的操做自由度以及開發的掌控度就大打折扣。而且,他會額外生成一些包裝代碼,對性能以及包大小有必定影響java

而 ASM 基本上能夠實現任何對字節碼的操做,也就是自由度和開發的掌控度很高。它提供了 訪問者模式來訪問字節碼文件,而且只注入咱們想要注入的代碼android

ASM 最初起源於一個博士的研究項目,在 2002 年開源,並從 5.x 版本便開始支持 Java 8。而且,ASM 是諸多 JVM 語言欽定的字節碼生成庫,它在效率與性能方面的優點要遠超其它的字節碼操做庫如 javassist、AspectJgit

思惟導圖大綱

目錄

1、ASM 的優點和逆勢

使用 ASM 操做字節碼的優點與逆勢都 比較明顯,其分別以下所示。github

一、ASM 的優點

  • 1)、內存佔用很小
  • 2)、運行速度很是快
  • 3)、操做靈活:對於字節碼的操做很是地靈活,能夠進行插入、刪除、修改等操做
  • 4)、想象空間大,可以借用它提高生產力
  • 5)、豐富的文檔與衆多社區的支持

二、ASM 的逆勢

上手難度較大,須要對 Java 字節碼有比較充分的瞭解web

對於 ASM 而言,它提供了 兩種模型:對象模型和事件模型算法

下面,咱們就先來說講 ASM 的對象模型。編程

2、ASM 的對象模型(ASM Tree API)

對象模型的 本質 是一個 被封裝事後的事件模型,它 使用了樹狀圖的形式來描述一個類,其中包含多個節點,例如方法節點、字段節點等等,而每一個節點又有子節點,例如方法節中有操做碼子節點 等等。下面咱們先來了解下由這種樹狀圖模式實現的對象模型的利弊。json

一、優勢

  • 1)、適宜處理簡單類的修改
  • 2)、學習成本較低
  • 3)、代碼量較少

二、缺點

  • 1)、處理大量信息會使代碼變得複雜
  • 2)、代碼難以複用

在對象模型下的 ASM 有 兩類操做緯度,分別以下所示:bootstrap

  • 1)、獲取節點獲取指定類、字段、方法節點
  • 2)、操控操做碼(針對方法節點)獲取操做碼位置、替換、刪除、插入操做碼、輸出字節碼

下面咱們就來分別來了解下 ASM 的這兩類操做。

三、獲取節點

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) {
    // 一、判斷方法名是否匹配目標方法
    if(methodNode.name.equals("getName")) {
        // 二、進行操做
    }
}
複製代碼

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 本地(局部)變量節點列表

四、操控操做碼

在操控字節碼以前,咱們必須先了解下 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:局部變量

下面,咱們就開始來說解下字節碼操控有哪幾種常見的方式。

一、獲取操做碼的位置

獲取指定操做碼位置的代碼以下所示:

for(AbstractInsnNode ainNode : methodNode.instructions.toArray()) {
    if(ainNode.getOpcode() == Opcodes.SIPUSH && ((IntInsnNode)ainNode).operand == 16) {
        ....//進行操做
    }
}
複製代碼

因爲通常狀況下咱們都沒法肯定操做碼在列表中的具體位置,所以 一般會經過遍歷的方式去判斷其關鍵特徵,以此來定位指定的操做碼,上述代碼就能定位到一個 SIPUSH 16 的字節碼,須要注意的是,有時一個方法中會有多個相同的指令,這是咱們須要靠判斷先後字節碼識別其特徵來定位,也能夠記下其命中次數而後設定在某一次進行操做,通常狀況下咱們都是使用的第二種

二、替換指定的操做碼

替換指定的操做碼的代碼以下所示:

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

三、刪除指定的操做碼

methodNode.instructions.remove(xxx);
複製代碼

xxx 表示的是要刪除的操做碼實例,咱們直接調用用 InsnList 的 remove 方法將它移除掉便可。

四、插入指定的操做碼

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
複製代碼

替換爲

ALOAD 1
ACONSTNULL
INVOKEVIRTUAL java/awt/image/BufferedImage.getWidth(Ljava/awt/image/ImageObserver;)I
複製代碼

當咱們操控完指定的類節點以後,就可使用 ASM 的 ClassWriter 類來輸出字節碼,代碼以下所示:

// 一、讓 ClassWriter 自行計算最大棧深度和棧映射幀等信息
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTEFRAMES);
classNode.accept(classWriter);
return classWriter.toByteArray();
複製代碼

關於 ClassWriter 的具體用法,咱們會在 ASM Core API 這部分來進行逐步講解。下面👇,咱們就先來看看 ASM 的事件模型。

3、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(鉤子)機制,以便可以訪問方法中的每個操做碼,這樣咱們便可以對字節碼文件進行細粒度地修改

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

一、類訪問者 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 自身的缺陷,可是,這種狀況幾乎不會發生。

二、類讀取(解析)者 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;

    // 一、讀取類的描述信息,例如 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;
    }

    // 二、讀取類的屬性信息,例如簽名 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);
    }

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

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

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

    // 六、訪問類註解和類型註解
    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));
        }
    }

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

    // 八、訪問內部類
    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;
        }
    }

    // 九、訪問字段和方法
    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) {
    // 一、讀取字段的描述信息
    char[] c = context.buffer;
    int access = readUnsignedShort(u);
    String name = readUTF8(u + 2, c);
    String desc = readUTF8(u + 4, c);
    u += 6;

    // 二、讀取字段的屬性
    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;

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

    // 四、訪問字段的註解和類型註解
    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));
        }
    }

    // 五、訪問字段的屬性
    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) {
    // 一、讀取方法描述信息
    char[] c = context.buffer;
    context.access = readUnsignedShort(u);
    context.name = readUTF8(u + 2, c);
    context.desc = readUTF8(u + 4, c);
    u += 6;

    // 二、讀取方法屬性信息
    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;

    // 三、訪問方法描述信息
    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 writer's 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;
            }
        }
    }

    // 四、訪問方法參數信息
    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));
        }
    }

    // 五、訪問方法的註解信息
    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);
    }

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

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

    // 八、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 的一張流程圖用於回顧梳理,以下所示:

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

三、小結

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

4、綜合實戰訓練

在開始使用 ASM Core API 以前,咱們須要先了解一下 ASM Bytecode Outline 工具的使用。

一、使用 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 編譯插樁統計方法耗時

使用 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 出去

三、全局替換項目中全部的 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,此時使用它們的投入產出比會更高。

5、總結

在 ASM Bytecode Outline 工具的幫助下,咱們可以完成不少場景下的 ASM 插樁的需求,可是,當咱們使用其處理字節碼的時候仍是須要考慮不少種可能出現的狀況。若是想要具有這方面的深度思考能力,咱們就 必須對每個操做碼的特徵都有較深的瞭解,若是還不瞭解的同窗能夠去看看 《深刻探索編譯插樁技術(3、JVM字節碼)。所以,要具有實現一個複雜 ASM 插樁的能力,咱們須要對 JVM 字節碼、ASM 字節碼以及 ASM 源碼中的核心工具類的實現 作到了然於心,而且在不斷地實踐與試錯以後,咱們纔可以成爲一個真正的 ASM 插樁高手

參考連接:


一、ASM官方文檔

二、《ASM 3.0 指南翻譯》PDF

二、極客時間之Android開發高手課 編譯插樁的三種方法:AspectJ、ASM、ReDex

三、極客時間之Android開發高手課 練習Sample跑起來 | ASM插樁強化練習

四、AndroidAdvanceWithGeektime / Chapter07

五、Java字節碼(Bytecode)與ASM簡單說明

六、字節碼加強技術探索

七、字節碼操縱技術探祕

八、一塊兒玩轉Android項目中的字節碼

九、AndroidAdvanceWithGeektime / Chapter-ASM

十、IntelliJ 插件 - ASM Bytecode Outline

十一、ASM封裝庫:lancetHunterHibeaver

十二、基於 Javassist 的字節碼處理工具:DroidAssist

1三、除了編譯期間修改 class 的方式,其實在運行期間咱們也能夠生成代碼,例如如今比較流行的運行時代碼生成庫 byte-buddybyte-buddy 中文文檔

Contanct Me

● 微信:

歡迎關注個人微信:bcce5360

● 微信羣:

因爲微信羣已超過 200 人,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~

About me

很感謝您閱讀這篇文章,但願您能將它分享給您的朋友或技術羣,這對我意義重大。

但願咱們能成爲朋友,在 Github掘金上一塊兒分享知識。

相關文章
相關標籤/搜索