位元組碼技術在模組依賴分析中的應用

amap_tech發表於2019-07-29

背景

近年來,隨著手機業務的快速發展,為滿足手機端使用者訴求和業務功能的迅速增長,移動端的技術架構也從單一的大工程應用,逐步向模組化、元件化方向發展。以高德地圖為例,Android 端的程式碼已突破百萬行級別,超過100個模組參與最終構建。

 

試想一下,如果沒有一套標準的依賴檢測和監控工具,用不了多久,模組的依賴關係就可能會亂成一鍋粥。

 

從模組 Owner 的角度看,為什麼依賴分析這麼重要?

  •  作為模組 Owner,我首先想知道“誰依賴了我?依賴了哪些介面”。唯有如此才能評估本模組改動的影響範圍,以及暴露的介面的合理性。
  • 我還想知道“我依賴了誰?呼叫了哪些外部介面”,對所需要的外部能力做到心中有數。

 

從全域性視角看,一個健康的依賴結構,要防止“下層模組”直接依賴“上層模組”,更要杜絕迴圈依賴。通過分析全域性的依賴關係,可以快速定位不合理的依賴,提前暴露業務問題。

 

因此,依賴分析是研發過程中非常重要的一環。

常見的依賴分析方式

提到 Android 依賴分析,首先浮現在腦海中的可能是以下這些方案:

  • 分析 Gradle 依賴樹。

  • 掃描程式碼中的 `import` 宣告。

  • 使用 Android Studio 自帶的分析功能。

 

我們逐個來分析這幾個方案:

1. Gradle 依賴樹

使用 `./gradlew :<module>:dependencies --configuration releaseCompileClasspath -q` 命令,很容易就可以得到模組的依賴樹,如圖:

不難發現,這種方式有兩個問題:

  • 宣告即依賴,即使程式碼中沒有使用的庫,也會輸出到結果中。

  • 只能分析到模組級別,無法精確到方法級別。

 

2. 掃描 `import` 宣告

掃描 Java 檔案中的 import 語句,可以得到檔案(類)之間的呼叫關係。

 

因為模組與檔案(類)的對應關係非常容易得到(掃描目錄)。所以,得到了檔案(類)之間的依賴關係,即是得到了模組之間檔案(類)級別的依賴關係。 

 

這個方案相比 Gradle 依賴掃描提升了結果維度,可以分析到檔案(類)級別。但是它也存在一些缺點:

  • 無法處理 import * 的情況。

  • 掃描“有 import 但未使用對應類”的場景效率太低(需要做原始碼字串查詢)。

 

3. 使用 IDE 自帶的分析功能

觸發 Android Studio 選單 「Analyze」 -> 「Analyze Dependencies」,可以得到模組間方法級別的依賴關係資料。如圖:

Android Studio 能準確分析到模組之間“方法級別”的引用關係,支援在 IDE 中跳轉檢視,也能掃描到對 Android SDK 的引用。

 

這個方案比前面兩個都優秀,主要是準確。但是它也有幾個問題:

  • 耗時較長:全面分析 AMap 全原始碼,大約需要 10 分鐘。

  • 分析結果無法為第三方複用,無法生成視覺化的依賴關係圖。

  • 分析正向依賴和逆向依賴,需要掃描兩次。

 

總結一下上述三種方案:Gralde 依賴基於工程配置,粒度太粗且結果不準。“Import 掃描方案”能拿到檔案級別依賴但資料不全。IDE 掃描雖然結果精準,但是資料複用困難,不便於工程化。

 

為什麼要使用位元組碼來分析?

參考 Android 構建流程圖,所有的 Java 原始碼和 aapt 生成的 R.java 檔案,都會被編譯成 .class 檔案,再被編譯為 dex 檔案,最終通過 apkbuilder 生成到 apk 檔案中。圖中的 .class 檔案即是我們所說的 Java 位元組碼,它是對 Java 原始碼的二進位制轉義。

 

在 Android 端,常見的位元組碼應用場景包括:

  • 位元組碼插樁:用於實現對 UI 、記憶體、網路等模組的效能監控。

  • 修改 jar 包:針對無原始碼的庫,通過編輯位元組碼來實現一些簡單的邏輯修改。

 

回到本文的主題,為什麼要分析位元組碼,而不是 Java 程式碼或者 dex 檔案?

 

不使用 Java 程式碼是因為有些庫以 jar 或者 aar 的方式提供,我們獲取不到原始碼。不使用 dex 檔案是因為它沒有好用的語法分析工具。所以解析位元組碼幾乎是我們唯一的選擇。

 

如何使用位元組碼分析依賴關係?

要得到模組之間的依賴關係,其實就是要得到“模組間類與類”之間的依賴關係。而要確定類之間的關係,分析類位元組碼的語句即可。

 

1. 在什麼時機來分析?

瞭解 Android 構建流程的同學,應該對 transform 這個任務不陌生。它是 Android Gradle 外掛提供的一個位元組碼 Hook 入口。

 

在 transform 這個任務中,所有的位元組碼檔案(包括三方庫) 以 Input 的格式輸入。

 

以JarInput 為例,分析其 file 欄位,可得到模組的名稱。解析 file 檔案,即可得到此模組所有的位元組碼檔案。

有了模組名稱和對應路徑下的 class 檔案,就建立了模組與類的對應關係,這是我們拿到的第一個關鍵資料。

 

2. 使用什麼工具分析?

解析 Java 位元組碼的工具,最常用的包括 Javassit,ASM,CGLib。ASM 是一個輕量級的類庫,效能較好,但需要直接操作 JVM 指令。CGLib 是對  ASM 的封裝,提供了更高階的介面。

 

相比而言,Javassist 要簡單的多,它基於 Java 的 API ,無需操作 JVM 指令,但其效能要差一些(因為 Javassit 增加了一層抽象)。在工程原型階段,為了快速驗證結果,我們優先選擇了 Javassit 。

 

3. 具體方案是怎樣的?

先看一個簡單的示例,如何分析下面這段程式碼的呼叫關係:

1: package com.account;
2: import com.account.B;
3: public class A {
4:     void methodA() {
5:         B b = new B(); // 初始化了 Class B 的例項 b
6:         b.methodB();   // 呼叫了 b 的 methodB 方法
7:     }
8: }

 

第1步:初始化環境,載入位元組碼 A.class,註冊語句分析器。

// 初始化 ClassPool,將位元組碼檔案目錄註冊到 Pool 中。
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath('<class檔案所在目錄>')
// 載入類A
CtClass cls = pool.get("com.account.A");
// 登錄檔達式分析器到類A
MyExprEditor editor = new MyExprEditor(ctCls)
ctCls.instrument(editor)

第2步:自定義表示式解析器,分析類A(以解析語句呼叫為例)。

class MyExprEditor extends ExprEditor {
    @Override
    void edit(MethodCall m) {
        // 語句所在類的名稱
        def clsAName = ctCls.name
        // 語句在哪個方法被呼叫
        def where = m.where().methodInfo.getName()
        // 語句在哪一行被呼叫
        def line = m.lineNumber
        // 被呼叫類的名稱
        def clsBName = m.className
        // 被呼叫的方法
        def methodBName = m.methodName
    }
    // 省略其它解析函式 ...
}

ExprEditor 的 edit(MethodCall m) 回撥能攔截 Class A 中所有的方法呼叫(MethodCall)。

 

除了本例中對 MethodCall 的解析,它還支援解析 new,new Array,ConstructorCall,FieldAccess,InstanceOf,強制型別轉換,try-catch 語句。

 

解析完 Class A,我們得到了 A 對 B 的依賴資訊 :

---------------------------------------------------------------------------
| Class1        | Class2        | Expr       | method1 | method2 | lineNo |
| ------------- | ------------- | ---------- | ------- | ------- | ------ |
| com.account.A | com.account.B | NewExpr    | methodA | <init>  | 5      |
| com.account.A | com.account.B | methodCall | methodA | methodB | 6      |
------------------- -------------------------------------------------------
簡單解釋如下:
類 com.account.A 的第5行(methodA方法內),呼叫了 com.account.B 的建構函式;
類 com.account.A 的第6行(methodA方法內),呼叫了 com.account.B 的 methodB 函式;
 

這便是“類和類之間方法級”的依賴資料。結合第1步得到的“模組和類”的對應關係,最終我們便獲得了“模組間方法級的依賴資料”。

 

基於這些基礎資料,我們還可以自定義依賴檢測規則、生成全域性的模組依賴關係圖等,本文就不展開了。

小結

本文主要介紹了模組依賴分析在研發過程中的重要性,分析了 Android 常見的依賴分析方案,從 Gradle 依賴樹分析, Import 掃描,使用 IDE 分析,到最後的位元組碼解析,方案逐步遞進。越是接近源頭的解法,才是越根本的解法。

關注高德技術,找到更多出行技術領域專業內容

 

相關文章