Android 編譯插樁操縱字節碼

學習記錄 本文摘自 拉鉤教育 Android 工程師進階 34 講

編譯插樁是什么

顧名思義,所謂編譯插樁就是在代碼編譯期間修改已有的代碼或者生成新代碼。實際上,我們項目中經常用到的 Dagger、ButterKnife 甚至是 Kotlin 語言,它們都用到了編譯插樁的技術。

理解編譯插樁之前,需要先回顧一下 Android 項目中 .java 文件的編譯過程:

java 文件的編譯過程

從上圖可以看出,我們可以在 1、2 兩處對代碼進行改造。

  1. 在 .java 文件編譯成 .class 文件時,APT、AndroidAnnotation 等就是在此處觸發代碼生成。

  2. 在 .class 文件進一步優化成 .dex 文件時,也就是直接操作字節碼文件。這種方式功能更加強大,應用場景也更多。但是門檻比較高,需要對字節碼有一定的理解。

本文主要介紹第 2 種實現方式,用一張圖來描述如下過程,其中紅色虛框包含了要講的所有內容。

ASM字節碼

一般情況下,我們經常會使用編譯插樁實現如下幾種功能:

  • 日志埋點;

  • 性能監控;

  • 動態權限控制;

  • 業務邏輯跳轉時,校驗是否已經登錄;

  • 甚至是代碼調試等。

插樁工具介紹

目前市面上主要流行兩種實現編譯插樁的方式:

AspectJ

AspectJ 是老牌 AOP(Aspect-Oriented Programming)框架,如果你做過 J2EE 開發可能對這個框架更加熟悉,經常會拿這個框架跟 Spring AOP 進行比較。其主要優勢是成熟穩定,使用者也不需要對字節碼文件有深入的理解。

ASM

目前另一種編譯插樁的方式 ASM 越來越受到廣大工程師的喜愛。通過 ASM 可以修改現有的字節碼文件,也可以動態生成字節碼文件,并且它是一款完全以字節碼層面來操縱字節碼并分析字節碼的框架。

舉個例子,在 Java 中如果實現兩個數相加操作,可以如下實現:

public int add() {
    int a = 10;
    int b = 20;
    return a+b;
}

但是如果使用 ASM 直接編寫字節碼指令,則有可能是如下幾個字節碼指令:

asm

雖然上面的代碼看起來很恐怖,但是沒必要太過擔心,因為有各種工具幫我們生成這些字節碼指令。

需求

記錄每一個頁面的打開和關閉事件,并通過各種 DataTracking 的框架上傳到服務器,用來日后做數據分析。

實現思路

過程主要包含兩步:

遍歷項目中所有的 .class 文件

如何找到項目中編譯生成的所有 .class 文件,是我們需要解決的第一個問題。眾所周知,Android Studio 使用 Gradle 編譯項目中的 .java 文件,并且從 Gradle1.5.0 之后,我們可以自己定義 Transform,來獲取所有 .class 文件引用。但是 Transform 的使用需要依賴 Gradle Plugin。因此我們第一步需要創建一個單獨的 Gradle Plugin,并在 Gradle Plugin 中使用自定義 Transform 找出所有的 .class 文件

遍歷到目標 .class 文件 (Activity)之后,通過 ASM 動態注入需要被插入的字節碼

如果第一步進行順利,我們可以找出所有的 .class 文件。接下來就需要過濾出目標 Activity 文件,并在目標 Activity 文件的 onCreate 方法中,通過 ASM 插入相應的 log 日志字節碼。

具體實現

創建 ASMLifeCycleDemo 項目

創建主項目 ASMLifeCycleDemo,當前項目中只有一個 MainActivity,如下:

ASMLifeCycleDemo1

創建自定義 Gradle 插件

首先在 ASMLifeCycleDemo 項目中創建一個新的 module,并選擇 Android Library 類型,命名為 asm_lifecycle_plugin。

將 asm_lifecycle_plugin module 中除了 build.gradle 和 main 文件夾之外的所有內容都刪除。然后在 main 目錄下分別創建 groovy 和 java 目錄,結構如下:

ASMLifeCycleDemo2

因為 Gradle 插件是使用 groovy 語言編寫的,所以需要新建一個 groovy 目錄,用來存放插件相關的.groovy類。 但 ASM 是 java 層面的框架,所以在 java 目錄里存放 ASM 相關的類。

然后,在 groovy 中創建目錄 danny.jiang.plugin,并在此目錄中創建類 LifeCyclePlugin.groovy 文件。在 LifeCyclePlugin 中重寫 apply 方法,實現插件邏輯,因為是 demo 演示,所以我只是簡單的打印 log 日志。

ASMLifeCycleDemo3

可以看出 LifeCyclePlugin 實現了 gradle api 中的 Plugin 接口。當我們在 app module 的 build.gradle 文件中使用此插件時,其 LifeCyclePlugin 的 apply 方法將會被自動調用。

接下來,將 asm_lifecycle_plugin module 的 build.gradle 中的內容全部刪掉,改為如下內容:

ASMLifeCycleDemo4

groupversion 都需要在 app module 引用此插件時使用。

所有的插件都需要被部署到 maven 庫中,我們可以選擇部署到遠程或者本地。這里只是演示,所以只是將插件部署到本地目錄中。具體地址通過 repository 屬性配置,如圖所示我將其配置在項目根目錄下的 asm_lifecycle_repo 目錄下。

最后一步,創建 properties 文件

在 plugin/src/main 目錄下新建目錄 resources/META-INF/gradle-plugins,然后在此目錄下新建一個文件:danny.asm.lifecycle.properties,其中文件名 danny.asm.lifecycle 就是我們自定義插件的名稱,稍后我們在 app module 中會使用到此名稱。

在 .properties 文件中,需要指定我們自定義的插件類名 LifeCyclePlugin,如下所示:

ASMLifeCycleDemo5

至此,自定義 Gradle 插件就已經寫完,現在可以在 Android Studio 的右邊欄找到 Gradle 中點擊 uploadArchives,執行 plugin 的部署任務:

ASMLifeCycleDemo6

可以看到,構建成功之后,在 Project 的根目錄下將會出現一個 repo 目錄,里面存放的就是我們的插件目標文件。

測試 asm_lifecycle_plugin

為了測試自定義的 Gradle 插件是否可用,可以在 app module 中的 build.gradle 中引用此插件。

ASMLifeCycleDemo7

圖中 ① 處就是在自定義 Gradle 插件中 properties 的文件名 (danny.asm.lifecycle)。

圖中 ② 處 dependencies 中的 classpath 是 group 值 + module 名 + version。

然后在命令行中使用 gradlew 執行構建命令,如果打印出我們自定義插件里的 log,則說明自定義 Gradle 插件可以使用:

ASMLifeCycleDemo8

自定義 Transform,實現遍歷 .class 文件

自定義 Gradle 插件已經寫好,接下來就需要實現遍歷所有 .class 的邏輯。這部分功能主要依賴 Transform API。

什么是 Transform ?

Transform 可以被看作是 Gradle 在編譯項目時的一個 task,在 .class 文件轉換成 .dex 的流程中會執行這些 task,對所有的 .class 文件(可包括第三方庫的 .class)進行轉換,轉換的邏輯定義在 Transform 的 transform 方法中。實際上平時我們在 build.gradle 中常用的功能都是通過 Transform 實現的,比如混淆(proguard)、分包(multi-dex)、jar 包合并(jarMerge)。

自定義 Transform

在 danny.jiang.plugin 目錄中,新建 LifeCycleTransform.groovy,并繼承 Transform 類。

ASMLifeCycleDemo9

可以看到,LifeCycleTransform 需要實現抽象類 Transform 中的抽象方法,具體有如下幾個方法需要實現:

ASMLifeCycleDemo10

解釋說明:Transform 主要作用是檢索項目編譯過程中的所有文件。通過這幾個方法,我們可以對自定義 Transform 設置一些遍歷規則,具體如下:

getName:

  • 設置我們自定義的 Transform 對應的 Task 名稱。Gradle 在編譯的時候,會將這個名稱顯示在控制臺上。比如:Task :app:transformClassesWithXXXForDebug。

getInputType:

  • 在項目中會有各種各樣格式的文件,通過 getInputType 可以設置 LifeCycleTransform 接收的文件類型,此方法返回的類型是 Set<QualifiedContent.ContentType> 集合。

ContentType 有以下 2 種取值。

ASMLifeCycleDemo11
  1. CLASSES:代表只檢索 .class 文件;

  2. RESOURCES:代表檢索 java 標準資源文件。

getScopes()

這個方法規定自定義 Transform 檢索的范圍,具體有以下幾種取值:

img

isIncremental() 表示當前 Transform 是否支持增量編譯,我們不需要增量編譯,所以直接返回 false 即可。

transform()

在 自定義Transform 中最重要的方法就是 transform()。在這個方法中,可以獲取到兩個數據的流向。

  • inputs:inputs 中是傳過來的輸入流,其中有兩種格式,一種是 jar 包格式,一種是 directory(目錄格式)。

  • outputProvider:outputProvider 獲取到輸出目錄,最后將修改的文件復制到輸出目錄,這一步必須做,否則編譯會報錯。

我們可以實現一個簡易 LifeCycleTransform,功能是打印出所有 .class 文件。代碼如下:

ASMLifeCycleDemo12

解釋說明:

  1. 自定義的 Transform 名稱為 LifeCycleTransform;

  2. 檢索項目中 .class 類型的目錄或者文件;

  3. 設置當前 Transform 檢索范圍為當前項目;

  4. 設置過濾文件為 .class 文件(去除文件夾類型),并打印文件名稱。

將自定義的 LifeCycleTransform 注冊到 Gradle 插件中

在 LifeCyclePlugin 中添加如下代碼:

img

再次在命令行中執行 build 命令,可以看到 LifeCycleTransform 檢索出的所有 .class 文件。

img

從圖中可以看出,Gradle 編譯時多了一個我們自定義的 LifeCycleTransform 類型的任務,并且將所有 .class 文件名打印出來,其中包含了我們需要的目標文件 MainActivity.class。

使用 ASM,插入字節碼到 Activity 文件

ASM 是一套開源框架,其中幾個常用的 API 如下:

  • ClassReader:負責解析 .class 文件中的字節碼,并將所有字節碼傳遞給 ClassWriter。

  • ClassVisitor:負責訪問 .class 文件中各個元素,還記得上一課時我們介紹的 .class 文件結構嗎?ClassVisitor 就是用來解析這些文件結構的,當解析到某些特定結構時(比如類變量、方法),它會自動調用內部相應的 FieldVisitor 或者 MethodVisitor 的方法,進一步解析或者修改 .class 文件內容。

  • ClassWriter:繼承自 ClassVisitor,它是生成字節碼的工具類,負責將修改后的字節碼輸出為 byte 數組。

添加 ASM 依賴

在 asm_lifecycle_plugin 的 build.gradle 中,添加對 ASM 的依賴,如下:

img

創建自定義 ASM Visitor 類

在 asm_lifecycle_plugin module 中的 src/main/java 目錄下創建包 danny.jiang.asm,并分別創建 LifecycleClassVisitor.java 和 LifecycleMethodVisitor.java。代碼如下:

LifecycleClassVisitor.java

紅框中,在 visitMethod 方法中,過濾出繼承自 AppCompatActivity 的文件,并在 LifeCycleMethodVisitor.java 中對 onCreate 進行改造。

LifeCycleMethodVisitor.java

圖中紅框內是真正執行插入字節碼的邏輯。可以看出 ASM 都是直接以字節碼指令的方式進行操作的,所以如果想使用 ASM,需要程序員對字節碼有一定的理解。如果對字節碼不是很了解,也可以借助三方工具 ASM Bytecode Outline 來生成想要的字節碼。

修改 LifeCycleTransform 的 transform 方法,使用 ASM

各種 Visitor 都定義好之后,我們就可以修改 LifeCycleTransform 的 transform 方法,并將需要插樁的字節碼插入到 MainActivity.class 文件中:

LifeCycleTransform

重新部署自定義 Gradle 插件,并運行主項目

上面幾步如果一切執行順利,那接下來就可以在點擊 uploadArchives 重新部署 LifeCyclePlugin。

注意:重新部署時,需要先在 app module 的 build.gradle 中將插件依賴注釋,否則報錯

部署成功之后,重新在 app 中依賴自定義插件并運行主項目,當 MainActivity 被打開時,會在 logcat 中看到如下日志:

Log

備注

本文摘自 拉鉤教育 Android 工程師進階 34 講

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容