在 《深刻探索編譯插樁技術(2、AspectJ)》 一文中咱們深刻學習了 AspectJ 在 Android 下的使用。能夠看到 AspectJ 很是強大,可是它也只能實現 50% 的字節碼操做場景,若是想要實現 100% 的字節碼操做場景,那麼就不得不使用 ASM。html
此外,AspectJ 有着一系列弊端: 因爲其基於規則,因此其切入點相對固定,對於字節碼文件的操做自由度以及開發的掌控度就大打折扣。而且,他會額外生成一些包裝代碼,對性能以及包大小有必定影響。java
而 ASM 基本上能夠實現任何對字節碼的操做,也就是自由度和開發的掌控度很高。它提供了 訪問者模式來訪問字節碼文件,而且只注入咱們想要注入的代碼。android
ASM 最初起源於一個博士的研究項目,在 2002 年開源,並從 5.x 版本便開始支持 Java 8。而且,ASM 是諸多 JVM 語言欽定的字節碼生成庫,它在效率與性能方面的優點要遠超其它的字節碼操做庫如 javassist、AspectJ。git
使用 ASM 操做字節碼的優點與逆勢都 比較明顯,其分別以下所示。github
上手難度較大,須要對 Java 字節碼有比較充分的瞭解。web
對於 ASM 而言,它提供了 兩種模型:對象模型和事件模型。算法
下面,咱們就先來說講 ASM 的對象模型。編程
對象模型的 本質 是一個 被封裝事後的事件模型,它 使用了樹狀圖的形式來描述一個類,其中包含多個節點,例如方法節點、字段節點等等,而每一個節點又有子節點,例如方法節中有操做碼子節點 等等。下面咱們先來了解下由這種樹狀圖模式實現的對象模型的利弊。json
在對象模型下的 ASM 有 兩類操做緯度,分別以下所示:bootstrap
獲取節點
:獲取指定類、字段、方法節點。操控操做碼(針對方法節點)
:獲取操做碼位置、替換、刪除、插入操做碼、輸出字節碼。下面咱們就來分別來了解下 ASM 的這兩類操做。
獲取一個類節點的代碼以下所示:
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 | 類的方法列表 |
獲取一個字段節點的代碼以下所示:
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 |
接下來,咱們看看如何獲取一個方法節點。
獲取指定的方法節點的代碼以下所示:
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 主要提供了 四類 方法用於插入字節碼,以下所示:
add(AbstractInsnNode insn)
: 將一個操做碼添加到 InsnList 的末尾。insert(AbstractInsnNode insn)
: 將一個操做碼插入到這個 InsnList 的開頭。insert(AbstractInsnNode insnNode,AbstractInsnNode insn)
: 將一個操做碼插入到另外一個操做碼的下面。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 的事件模型。
對象模型是由事件模型封裝而成,所以事件模型的上手難度會更大一些。
對於事件模型來講,它 採用了設計模式中的訪問者模式。它的出現是爲了更好地解決這樣一種需求:有 A 個元素和 N 種算法,每一個算法都能做用於任意一個元素,而且在不一樣的元素上有不一樣的運行方式。
在訪問者模式出現以前,咱們一般會在每個元素對應的類中添加 N 個方法,而後在每個方法中去實現一個算法,可是,這樣的作法容易致使代碼耦合性太高,而且可維護性差。
所以,訪問者模式應運而生,咱們能夠 創建 N 個訪問者,而且每個訪問者擁有一個算法及其內部的 A 種運行方式。當咱們須要調用一個算法時,就讓相應的訪問者去訪問元素,而後讓訪問者根據被訪問對象選擇相應的算法。
須要注意的是,訪問者並無直接去操做元素,而是先讓元素類調用 accept 方法接收訪問者,而後,訪問者在元素類的內部方法中開始調用 visit 方法訪問當前的元素類。這樣,訪問者便能直接訪問元素類中的內部私有成員,其優點在於 避免了暴露沒必要要的內部細節。
要理解 ASM 的事件模型,咱們就須要對其中的 兩個重要成員的工做原理 有較深的瞭解。它們即是 類訪問者 ClassVisitor 與 類讀取(解析)者 ClassReader。
從字節碼的視角中,一個 Java 類由不少組件凝聚而成,而這之中便包括超類、接口、屬性、域和方法等等。當咱們在使用 ASM 進行操控時,能夠將它們視爲一個個與之對應的事件。所以 ASM 提供了一個 類訪問者 ClassVisitor,以經過它來訪問當前類的各個組件,當解析器 ClassReader 依次遇到上述的各個組件時,ClassVisitor 上對應的 visitor 事件處理器方法均會被一一調用。
與類類似,方法也是由多個組件凝聚而成的,其對應着方法屬性、註解及編譯後的代碼(Class 字節碼)。ASM 的 MethodVisitor 提供了一種 hook(鉤子)機制,以便可以訪問方法中的每個操做碼,這樣咱們便可以對字節碼文件進行細粒度地修改。
下面,咱們便來一一分析下它們。
一般咱們在使用 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),它究竟是什麼呢?
它是 Java 6 之後引入的一種驗證機制,用於 檢驗 Java 字節碼的正確性。它的工做方式是 記錄每個關鍵步驟完成後其方法中操做數棧的理論狀態,而後,在實際運行的時候,ASM 會將其實際狀態和理論狀態對比,若是狀態不一致則代表出現了錯誤。
但棧映射幀的實現並不簡單,所以經過調用 classReader 實例的 accept 方法咱們即可以讓 ASM 自動去計算棧映射幀,儘管這 會增長 50% 的額外運算。此外,可能會有小几率的狀況遇到 棧映射幀驗證失敗 的狀況,例如:VerifyError: Inconsistent stackmap frames at branch target
這個錯誤。
最多見的緣由可能就是因爲 字節碼寫錯形成的,此時,咱們應該去檢查對應的字節碼實現代碼。此外,也多是 JDK 版本的支持問題或是 ASM 自身的缺陷,可是,這種狀況幾乎不會發生。
如今,讓咱們再回到上述註釋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 標識已訪問結束。
這裏,咱們先來看看 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 方法結束了字段信息的訪問。
下面,咱們在看看 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 的關鍵組件,以下所示:
ClassReader
:用於讀取已經編譯好的 .class 文件。ClassWriter
:用於從新構建編譯後的類,如修改類名、屬性以及方法,也能夠生成新的類的字節碼文件。各類 Visitor 類
:如上所述,Core API 根據字節碼從上到下依次處理,對於字節碼文件中不一樣的區域有不一樣的 Visitor,好比用於訪問方法的 MethodVisitor、用於訪問類變量的 FieldVisitor、用於訪問註解的 AnnotationVisitor 等等。爲了實現 AOP,其重點是要靈活運用 MethodVisitor。在開始使用 ASM Core API 以前,咱們須要先了解一下 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 編譯插樁統計方法耗時主要能夠細分爲以下三個步驟:
剛開始的時候,咱們能夠在 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 出去。
首先,咱們先將 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 進行插樁的時候,咱們尤爲須要注意如下 兩點:
try catch blcok
的處理,關於異常處理可使用 ASM 提供的 CheckClassAdapter,能夠在修改完成後驗證一下字節碼是否正常。除了直接使用 ASM 進行插樁以外,若是需求比較簡單,咱們可使用基於 ASM 的字節碼處理工具,例如:lancet、Hunter 和 Hibeaver,此時使用它們的投入產出比會更高。
在 ASM Bytecode Outline 工具的幫助下,咱們可以完成不少場景下的 ASM 插樁的需求,可是,當咱們使用其處理字節碼的時候仍是須要考慮不少種可能出現的狀況。若是想要具有這方面的深度思考能力,咱們就 必須對每個操做碼的特徵都有較深的瞭解,若是還不瞭解的同窗能夠去看看 《深刻探索編譯插樁技術(3、JVM字節碼)。所以,要具有實現一個複雜 ASM 插樁的能力,咱們須要對 JVM 字節碼、ASM 字節碼以及 ASM 源碼中的核心工具類的實現 作到了然於心,而且在不斷地實踐與試錯以後,咱們纔可以成爲一個真正的 ASM 插樁高手。
一、ASM官方文檔
二、極客時間之Android開發高手課 編譯插樁的三種方法:AspectJ、ASM、ReDex
三、極客時間之Android開發高手課 練習Sample跑起來 | ASM插樁強化練習
四、AndroidAdvanceWithGeektime / Chapter07
九、AndroidAdvanceWithGeektime / Chapter-ASM
十、IntelliJ 插件 - ASM Bytecode Outline
十一、ASM封裝庫:lancet、Hunter 和 Hibeaver
十二、基於 Javassist 的字節碼處理工具:DroidAssist
1三、除了編譯期間修改 class 的方式,其實在運行期間咱們也能夠生成代碼,例如如今比較流行的運行時代碼生成庫 byte-buddy、byte-buddy 中文文檔
歡迎關注個人微信:
bcce5360
因爲微信羣已超過 200 人,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~