Android ASM 插樁實踐

上一章知道了如何獲取 class 文件,那該如何進行插樁呢?本章告訴你!

什麼是 ASM?

ASM 是一個字節碼操作庫,它可以直接修改已經存在的 class 文件或者生成 class 文件。 ASM 提供了一系列便捷的功能來操作字節碼內容,與其它字節碼的操作框架相比(例如 AspectJ),ASM 更加偏向於底層,直接操作字節碼,在設計上更小、更快,性能上更好,而且幾乎可以修改任意字節碼。

參考網易樂得團隊關於插樁庫的實驗結果:

通過上表,ASM 效率更高。不過效率高的代價就是 ASM 直接操作字節碼,相對於其他庫上手相對困難。

ASM 修改 class

接下來,看下 ASM 是如何修改 class 文件的,先看下 ASM 的核心 API:

  • ClassReader:對具體的 class 文件進行讀取與解析
  • ClassVisitor、AdviceAdapter:可以訪問class文件的各個部分,比如方法、變量、註解等,用於修改 class 文件。
  • ClassWriter:將修改後的class文件通過文件流的方式覆蓋掉原來的 class 文件,從而實現 class 修改;

通過下圖簡單瞭解下 ASM 處理流程:

基礎 Java 字節碼知識

使用 ASM 之前,需要簡單瞭解下基本的 Java 字節碼知識。

什麼是 Java 字節碼?

Java 字節碼(英語:Java bytecode)是Java虛擬機執行的一種指令格式。通俗來講字節碼就是經過 javac 命令編譯之後生成的 class 文件。 class文件包含了 Java 虛擬機指令集和符號表以及若干其他的輔助信息。

可以通過 javap –c xxx.class 終端命令來查看對應的字節碼。例如:

字節碼描述符

從上面生成的 Java 字節碼,可以看到這樣的描述:java/lang/Object.“<init>”:()V,這就是字節碼描述符

描述符的作用是描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。

對於基本數據類型(byte char double float int long short boolean)以及代表無返回值的void類型都用一個大寫字符來表示,對象類型則用字符“L”加對象的全限定名來表示(即把包名所有“.”換成了“/” ),一般對象類型末尾都會加一個“;”來表示全限定名的結束。

舉個例子:

String[] ->  [Ljava/lang/String;

int[][]  ->  [[I;

對於方法則使用 (), 按照參數列表,返回值的順序表示。 例如:

void init()                   ->   ()V
void test(object)             ->   (Ljava/lang/object;)V
String[] getArray(String s)   ->   (Ljava/lang/String;)[Ljava/lang/String;

給出下圖理解下:

虛擬機執行字節碼基礎

爲了控制版面,避免長篇大論的討論具體內容而忽略需要解決的問題的本質,重點討論 Java 運行時的內存佈局:

虛擬機的內存分爲堆內存與棧內存。堆內存所有線程共享,棧內存則線程私有,重點解釋下棧內存。
Java 虛擬機棧描述的是 Java 方法執行的內存模型:每個方法在執行同時會創建一個 棧幀 用於存局部變量表、操作數棧、動態鏈接、方法返回地址等信息。每個方法從調用到執行完畢的過程,就對應着一個棧幀在虛擬機中從入棧到出棧的過程。每一個棧幀都包含了上述信息。一個線程中的方法調用鏈可能會很長,即會有很多棧幀。對於一個當前活動的線程中,只有位於線程棧頂的棧幀纔是有效的,成爲 前棧幀(current stack Frame) ,這個棧幀所關聯的方法成爲 當前方法(current method)

總之我們知道每個線程對應的中會有若干棧幀,每個棧幀對應着相應的方法

棧幀的概念圖:

局部變量表:局部變量表是一組變量存儲空間,用於存儲方法參數(入參)和方法內部定義的局部變量。它的容量以容量槽爲最小單位(slot)。虛擬機通過索引的定位方式使用這個局部變量表,從0開始,在非 static 方法中,0 代表的是“this”,其餘參數從1開始分配。以 Android click 方法爲例

這個方法的局部變量表的容量槽爲:

0 :this
1 :View v

操作數棧:它是一個後入先出的棧結構。當一個方法剛開始執行時,操作數棧是空的,執行過程中,會有各種字節碼執行向操作數中寫入和提取內容,也就是出棧和入棧的過程。

看下面兩個方法,它們對應字節碼指令的解釋:

參考 JVM 指令集合:http://www.wangyuwei.me/2017/01/19/JVM%E6%8C%87%E4%BB%A4%E9%9B%86%E6%95%B4%E7%90%86/

小思考

大家看下面的例子:

可參考下方的助記符說明:

問題:

爲什麼在調用 funB 方法之前要把 this 壓至棧頂(aload_0)呢?

解釋:

如果清楚了前面的內容這個操作就非常好解釋,因爲 funB 方法是作爲 TestClass2 類的內部方法,需要使用當前對象 this 來調用,故需要 aload_0 指令將 this 壓至棧頂。


經過上面的一系列操作,我想大家應該。。。


ASM Bytecode Outline 插件

上面的字節碼知識只是爲了幫助大家理解後面插樁代碼的含義,真正使用時推薦使用這個神器,一款 IDEA 的插件 ASM Bytecode Outline。它可以自動給你生成對應的 ASM 操作代碼。

使用方法:

Bytecode Tab 會生成對應的字節碼文件,ASMMified 會生成對應的 ASM 操作代碼:


純 Java 的插樁示例

通過前面一系列的準備,我們終於可以嘗試去寫一個插樁示例了。示例很簡單,就是在 insertFun 方法前和後分別插入一段回調代碼,並把 this,入參帶過去。

爲了驗證插入的正確性,在回調方法中打印一些日誌:

寫一個 ASMJava 類,讀取 TestClass,通過 TestClassVisitor 修改後寫入 out/TestClass.class 中。

參考鏈接:https://www.jianshu.com/p/abd1b1b8d3f3

在寫 TestClassVisitor 前,需要用到前面提到的工具 ASM Bytecode Outline ,將要需要插入的 ASM 代碼 copy 出來:

寫一個 TestClassVisitor 繼承自 ClassVisitor,實現其 visitMethod 方法,並將剛纔的代碼 copy 過來:

TestMethodVisitor 類只需繼承自 AdviceAdapter 即可。會有一些重要的可複寫方法,有興趣的同學可以打 log 試下。

經過上面的處理我們來看下成果,找到這個輸出的 class,結果如我們所料,大功告成:

Android 插樁示例

基於前面的內容,接下來實現一個對 OnClick 自動監聽的小示例:

在 第二章 CustomTransform 的基礎上,進行修改,獲取到對應的 class,進行修改 class 字節碼,開始前不用忘記在插件模塊的 build.gradle 文件中添加 asm 依賴:

implementation 'org.ow2.asm:asm:5.0.3'
implementation 'org.ow2.asm:asm-commons:5.0.3'

這裏貼出主要的核心代碼,其他代碼篇幅限制,在 Demo 中查看:

效果展示:

Demo 工程

IDEA Java 工程:https://github.com/changer0/JavaASMDemo

Android 工程:https://github.com/changer0/ASMInjectDemo

以上就是本節內容,歡迎大家關注👇👇👇

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章