Android 進階——代碼插樁必知必會之初識ASM7字節碼操作庫完全攻略(一)

引言

對於我們Java 程序員來說,或許對於Java源文件,再熟悉不過了,畢竟整天都是與之打交道,但是是否是真的已經很熟悉了,鎮的瞭解了嗎?我想大家都知道Java 源文件經過編譯之後轉爲.class字節碼文件,但是或許很多人不知道其實.class字節碼文件也是可以被編輯的,學習字節碼操作相關的,需要具備一些JVM 設計規範和字節碼文件的相關知識,接下來就進入正文開始ASM 字節碼操作庫的小結,代碼操作庫有很多種,這裏總結下最常用的ASM,代碼插樁需要掌握一系列的知識:

一、ASM庫概述

ASM 是一個Java 字節碼(.class)操控框架,它可以用來動態生成類的字節碼或者改變現有類的字節碼。藉由ASM可以直接創建或修改字節碼文件,也就能在類被加載到JVM執行之前動態改變原有的類行爲。其目的是生成、轉換和分析字節數組來表示的已編譯 Java 類。因爲無論是在磁盤的存儲形式還是JVM的加載皆採用這種字節數組形式,Java 字節碼文件按照Java 虛擬機規範中的格式進行組織並存儲。爲此,ASM把Java class 抽象爲一棵樹,使用 “Visitor” 模式遍歷整個二進制結構,基於事件驅動的處理方式使得用戶只需要關注於對其編程有意義的部分,而不必深入Java 類文件格式的所有細節。ASM 從字節碼文件中讀取所有相關信息(包括類名稱、方法、屬性、數值常數、 字符串、 Java 標識符、 Java類型、 Java 類結構元素以及 Java 字節碼指令等)並提供了字節碼級別的接口來讀寫和轉換這些字節數組且在訪問到對應的信息時提供對應的回調方法。通俗來說就是 ASM提供了一系列的API,能夠讓我們通過Java 字節碼指令去處理Java字節碼 (因爲字節碼文件就是主要就是由16進制形式的字節碼指令組成的),欲瞭解更多請自己參閱官網

ASM 的名字沒有任何含義,就是任性引用了C庫中的一些可以操作彙編語言函數名稱中的asm,而且ASM 的使用範圍僅限於
對類(.class)文件的讀、寫、轉換和分析(即加載前的過程進行干預),至於類的加載過程就超出了其能力範圍。

在這裏插入圖片描述

二、ASM庫的架構模型概述

ASM 庫提供了兩個用於生成和轉換已編譯類的 API:核心 API(,以基於事件的形式來表示類,)和樹 API,以基於對象的形式來表示類

1、核心API概述

核心 API基於事件的形式來表示類,把類抽象爲一系列事件,每個事件表示類的一個元素(比如它的一個標頭、一個字段、一個方法聲明、一條指令等等)。基於事件的 API 定義了一組可能事件,以及這些事件必須遵循的發生順序,還提供了一個類分析器,爲每個被分析元素生成一個事件,還提供一個類寫入器,由這些事件的序列生成經過編譯的類。其組織結構是圍繞事件生成器(類分析器)、事件使用器(類寫入器)和各種預定義的事件篩選器進行的,在這一結構中可以添加用戶定義的生成器、使用器和篩選器,將事件生成器(負責執行生成或轉換過程)、篩選器和使用器組件組裝爲可能很複雜的體系結構,
‰ 然後啓動事件生成器,以執行生成或轉換過程。

2、樹 API概述

樹 API基於對象模型來表示類, 把類抽象爲對象樹, 每個對象表示類的一部分( 比如類本身、一個字段、一個方法、一條指令等等,每個對象都有一些引用,指向表示其組成部分的對象)。基於對象的 API 提供了一種方法,可以將表示一個類的事件序列轉換爲表示同一個類的對象樹,也可以反過來,將對象樹表示爲等價的事件序列。換言之,基於對象的 API 構建在基於事件的API 之上,用於操作類樹的類生成器或轉換器組件是可以組成鏈的,它們之間的鏈接代表着轉換的順序,使用 “Visitor” 模式遍歷整個二進制結構,基於事件驅動的處理方式使得用戶只需要關注於對其編程有意義的部分,而不必深入Java 類文件格式的所有細節。如下所示的複雜體系結構:
在這裏插入圖片描述
其中的箭頭表示在類分析器、寫入器或轉換器之間進行的基於事件或基於對象的通信, 在整個鏈中的任何位置, 都可能會在基於事件與基於對象的表示之間進行轉換。

三、ASM庫核心組件和接口類

ASM通過樹這種數據結構來抽象複雜的字節碼結構並利用 Push 模型來對樹進行遍歷,在遍歷過程中對字節碼進行修改。所謂的 Push 模型類似於簡單的訪問者(Visitor) 設計模式,因爲字節碼結構是固定的,所以不需要專門抽象出一種 Vistable 接口,而只需要提供 Visitor 接口來遍歷一些複雜的數據結構,其中Visitor 相當於用戶派出的代表,深入到算法內部,由算法安排訪問行程,而Visitor 代表可以更換,但對算法流程無法干涉,因此是被動的。

1、ClassVisitor

ASM用於生成和變轉字節碼文件的API是基於 ClassVisitor 抽象類的,該類中的每個方法都對應於同名的類文件結構部分。對於字節碼中簡單的部分只需調用一個方法就能完成對應部分的字節碼構建並返回 void;有些複雜部分的內容(例如visitAnnotation、 visitField 和 visitMethod 方法,它們分別返AnnotationVisitor、 FieldVisitor 和 MethodVisitor)則用一個初始方法調用來訪問並返回一個輔助的訪問者類。ClassVisitor作爲Java類的訪問者角色,封裝了在讀取Class字節碼時會觸發的一系列事件,如類頭解析完成、註解解析、字段解析、方法解析等並按照以下順序調用: visit() >visitSource() >visitModule() >visitNestHost() >visitOuterClass() > visitAnnotation() > visitTypeAnnotation() > visitAttribute()> visitNestMember() > visitInnerClass() > visitField()> visitMethod()> visitEnd(),當分析到方法時就會回調visitMethod方法,進入到方法體內部時也會回調對應的方法。簡而言之,ClassVisitor (包含其子類)在ASM 訪問到對應類的元素時回自動回調對應的方法

類/接口 說明
AnnotationVisitor 定義在解析註解時會觸發的一系列的事件,解析到一個基本值類型的註解、enum值類型的註解、Array值類型的註解、註解值類型的註解時,會調用對應的方法,下同。
FieldVisitor 定義在解析字段時觸發的事件,如解析到字段上的註解、解析到字段相關的屬性等。
MethodVisitor 定義在解析方法時觸發的事件,如方法上的註解、屬性、代碼等。
SignatureVisitor 定義在解析Signature時會觸發的事件,如正常的Type參數、類或接口的邊界等。

各個 ClassVisitor通過責任鏈 (Chain-of-responsibility) 模式,可以非常簡單的封裝對字節碼的各種修改,而無須關注字節碼的內部字節偏移,通過這些對應的Visitor可以訪問字節碼文件中對應的組成部分,而且ClassVisitor會自己去控制這些過程,用戶要做的只是覆寫相應的 visit 方法,比如 visitMethod會返回一個實現 MethordVisitor接口的實例,visitField會返回一個實現 FieldVisitor接口的實例,完成子過程後控制返回到父過程,繼續訪問下一節點。

部分方法 說明
void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 訪問class的頭部信息時,version爲class版本(編譯版本),access爲訪問修飾符,name爲類名稱,signature爲class的簽名,可能是null,superName爲超類名稱,interfaces爲接口的名稱
AnnotationVisitor visitAnnotation(String descriptor, boolean visible) 訪問class的註解信息時,descriptor爲簽名描述信息,visible爲是否運行時可見
void visitAttribute(Attribute attribute) 訪問該類的非標準屬性。
void visitInnerClass(String name, String outerName, String innerName, int access) 訪問class中內部類的信息,而且這個內部類不一定是被訪問類的成員(有可能是一段方法中的匿名內部類或者聲明在一個方法中的類等等)。name爲內部類的名稱,outerName爲內部類所在類的名稱,innerName爲內部類的名稱
void visitOuterClass(String owner, String name, String descriptor) 訪問該類的外部類,僅當類具有封閉類時,才必須調用此方法。owner爲擁有該類的class名稱,name爲包含該類的方法的名稱,如果該類未包含在其封閉類的方法中,則返回null,descriptor爲簽名描述信息
void visitEnd() 結束訪問class時
FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) 訪問class中字段的信息,返回一個FieldVisitor用於操作字段相關的信息,access爲訪問修飾符,name爲類名稱,signature爲class的簽名,可能是null,descriptor爲描述信息
MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) 訪問class中方法的信息,返回一個MethodVisitor用於操作字段相關的信息,access爲訪問修飾符,name爲方法名稱,signature爲方法的簽名,可能是null,descriptor爲描述信息,exceptions爲異常
ModuleVisitor visitModule(String name, int access, String version) 訪問對應的模塊。
AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) 訪問類簽名中類型的註釋。

2、ClassWriter

ClassWriter類是ASM中主要用於生成一個類的字節碼文件的類繼承了ClassVisitor抽象類,通過toByteArray()方法返回生成的字節碼的字節流,將字節流寫回文件即可生產調整後的 class 文件(所有 visit 事件的時間上先後調用,最終轉換成字節碼的空間位置前後的調整)。可以通過ClassWriter中的visitXxxx方法返回對應各部分的訪問者去創建字節碼的文件中的各個部分,傳入對應的字節碼指令也是通過visitXxxxxx方法,如果要創建則傳入對應的創建類型的字節碼指令

2.1、ClassWriter 核心方法

部分方法 說明
public ClassWriter(final int flags) 構造ClassWriter對象,flag取值爲0、1、2(0時表示需要手動計算最大操作數棧、局部變量表、楨變化;ClassWriter.COMPUTE_MAXS表示自動計算局部變量表和操作數棧,但是必須要調用visitMaxs,方法參數會被忽略。楨變化需要手動計算ClassWriter.COMPUTE_FRAMES表示全自動計算,但是必須要調用visitMaxs,方法參數會被忽略。但ClassWriter.COMPUTE_MAXS比0慢10%,比COMPUTE_FRAMES慢一倍。)
public final void visit(final int version, final int access,final String name,final String signature,final String superName,final String[] interfaces) 構造class文件的頭部信息,version爲指定的JDK版本(取值爲Opcodes定義的常量),access爲類的修飾符(同version),name爲類的名稱,signature與泛型相關的,若傳入null則表示該字段不是泛型的簽名,superName爲要繼承父類的全限定名,Interfaces爲要實現的接口全限定名
public final FieldVisitor visitField(final int access,final String name,final String descriptor,final String signature,final Object value) 構造class文件的成員屬性,name爲成員屬性名,descriptor爲屬性的類型簽名,value爲屬性的值,只適用於靜態字段,若當前要生成的字段不是靜態的則傳入null
public final MethodVisitor visitMethod(final int access,final String name,final String descriptor,final String signature,final String[] exceptions) **構造class文件的方法“簽名”**並返回都構造方法體的對象(即方法的修飾符、方法名、返回值及全限定的參數)
public final AnnotationVisitor visitAnnotation(final String descriptor, final boolean visible) 構造class 的註解對象,descriptor爲註解的描述名,visible爲運行時是否可見
public byte[] toByteArray() 返回生成的字節碼的字節流,將字節流寫回文件即可生產調整後的 class 文件

2.2、AnnotationWriter、FieldWriter、MethodWriter、SignatureWriter

類/接口 說明
AnnotationWriter 實現了AnnotationVisitor,用於創建註解相關字節碼指令來創建註解部分字節碼,下類似。
FieldWriter 實現了FieldVisitor,用於創建字段相關字節碼。
MethodWriter 實現了MethodVisitor,用於創建方法相關字節碼。
SignatureWriter 實現了SignatureVisitor,用於創建泛型相關字節碼。
AnnotationWriter 實現了AnnotationVisitor,用於創建註解相關字節碼。

3、FieldVisitor 、MethodVisitor 、AnnotationVisitor 等

FieldVisitor 、MethodVisitor 、AnnotationVisitor 雖然名稱形式有點類似,但是三者在繼承關係除了都是繼承自Object的抽象類之外並無其他類之間的關係,正如名稱所示,它們都是ASM 提供出來供我們傳入對應字節碼指令來生成對應部分的方法,比如說MethodVisitor 用於創建方法體,不同類型的字節碼指令對應不同的方法。

4、ClassReader和SignatureReader

ClassReader類可以從字節數組直接或 class 文件間接的獲得字節碼數據(因爲它是按照Java虛擬機規範中定義的方式來解析class文件中的內容),調用accept方法時開始分析字節碼並構建出字節碼文件在內存中的抽象的結構樹,是字節碼的讀取與分析引擎,它採用類似SAX的事件讀取機制,每當有事件發生時,調用註冊的ClassVisitor、AnnotationVisitor、FieldVisitor、MethodVisitor做相應的處理(當然也可以不通過 ClassReader類,自行手工控制這個流程,只要確保各個 visit 事件被先後正確的調用,最後就能生成可以被正確加載的字節碼),調用accep方法之後解析字節碼中常量池之後的所有元素並構造成Attribute鏈,如果attribute名稱符合在accept中attribute數組中指定的attribute名,則替換傳入的attribute數組對應的項,其中 accept(final ClassVisitor classVisitor, final int parsingOptions)方法中的parsingOptions參數代表用於解析class的選項,有以下取值:

  • ClassReader.SKIP_CODE——過代碼屬性的標誌

  • ClassReader.SKIP_FRAMES——跳過StackMap和StackMapTable屬性的標誌,跳過MethodVisitor.visitFrame方法,對於我們開發者來說最好選這個。

  • ClassReader.SKIP_DEBUG——跳過SourceFile,SourceDebugExtension,LocalVariableTable,LocalVariableTypeTable和LineNumberTable屬性的標誌,跳過ClassVisitor.visitSource, MethodVisitor.visitLocalVariable, MethodVisitor.visitLineNumber方法。

  • ClassReader.EXPAND_FRAMES——用於展開堆棧映射幀的標誌,這會大大降低性能,不過建議使用這個標誌。

簡而言之,ClassReader字節碼分析器就是讀取並解析class文件中的內容,在遇到合適的字段時調用ClassVisitor中相對應的方法。而SignatureReader類負責對類定義、字段定義、方法定義、本地變量定義的簽名的解析,當範型被引入時,用於存儲範型定義時的元數據(因爲元數據在運行時會被擦除)。剩下的還有對字節碼中屬性的類進行抽象的Attribute;用於存儲字節碼二進制存儲的容器ByteVector字節碼指令的一些常量定義的Opcodes接口以及類型相關的常量定義以及一些基於其上的操作的Type類。

四、查看ASM 需要傳入的字節碼指令

ASM 需要傳入的字節碼指令有兩種方法:

  • 可以藉助ASM Bytecode Outline插件通過.java文件轉爲使用ASM 時所用到的字節碼指令,其中第一欄Bytecode直接調入ASM提供的工具類傳入的原始的字節碼,第二欄ASMified爲使用ASM 生成class的代碼,第三欄Groovified爲使用Groovy 語言生成class的代碼。
    在這裏插入圖片描述
  • 使用javap反編譯.class文件查看

javap -c是用於把.class文件反編譯爲字節碼,可以不用帶上後綴,javap -verbose 可輸出更多完整的信息。

在這裏插入圖片描述
PS:ASM的基本操作見下篇。

發佈了242 篇原創文章 · 獲贊 136 · 訪問量 54萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章