溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

字節(jié)碼技術(shù)在模塊依賴分析中的應(yīng)用

發(fā)布時(shí)間:2020-08-06 22:13:06 來源:ITPUB博客 閱讀:121 作者:amap_tech 欄目:互聯(lián)網(wǎng)科技

背景


近年來,隨著手機(jī)業(yè)務(wù)的快速發(fā)展,為滿足手機(jī)端用戶訴求和業(yè)務(wù)功能的迅速增長,移動(dòng)端的技術(shù)架構(gòu)也從單一的大工程應(yīng)用,逐步向模塊化、組件化方向發(fā)展。以高德地圖為例,Android 端的代碼已突破百萬行級(jí)別,超過100個(gè)模塊參與最終構(gòu)建。

試想一下,如果沒有一套標(biāo)準(zhǔn)的依賴檢測和監(jiān)控工具,用不了多久,模塊的依賴關(guān)系就可能會(huì)亂成一鍋粥。

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

  • 作為模塊 Owner,我首先想知道“誰依賴了我?依賴了哪些接口”。唯有如此才能評(píng)估本模塊改動(dòng)的影響范圍,以及暴露的接口的合理性。

  • 我還想知道“我依賴了誰?調(diào)用了哪些外部接口”,對(duì)所需要的外部能力做到心中有數(shù)。

從全局視角看,一個(gè)健康的依賴結(jié)構(gòu),要防止“下層模塊”直接依賴“上層模塊”,更要杜絕循環(huán)依賴。通過分析全局的依賴關(guān)系,可以快速定位不合理的依賴,提前暴露業(yè)務(wù)問題。

因此,依賴分析是研發(fā)過程中非常重要的一環(huán)。

常見的依賴分析方式


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

  • 分析 Gradle 依賴樹。

  • 掃描代碼中的 import 聲明。

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

我們逐個(gè)來分析這幾個(gè)方案:

1. Gradle 依賴樹

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

字節(jié)碼技術(shù)在模塊依賴分析中的應(yīng)用

不難發(fā)現(xiàn),這種方式有兩個(gè)問題:

  • 聲明即依賴,即使代碼中沒有使用的庫,也會(huì)輸出到結(jié)果中。

  • 只能分析到模塊級(jí)別,無法精確到方法級(jí)別。

2. 掃描 import 聲明

掃描 Java 文件中的 import 語句,可以得到文件(類)之間的調(diào)用關(guān)系。

因?yàn)槟K與文件(類)的對(duì)應(yīng)關(guān)系非常容易得到(掃描目錄)。所以,得到了文件(類)之間的依賴關(guān)系,即是得到了模塊之間文件(類)級(jí)別的依賴關(guān)系。

這個(gè)方案相比 Gradle 依賴掃描提升了結(jié)果維度,可以分析到文件(類)級(jí)別。但是它也存在一些缺點(diǎn):

  • 無法處理 import * 的情況。

  • 掃描“有 import 但未使用對(duì)應(yīng)類”的場景效率太低(需要做源碼字符串查找)。

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

觸發(fā) Android Studio 菜單 「Analyze」 -> 「Analyze Dependencies」,可以得到模塊間方法級(jí)別的依賴關(guān)系數(shù)據(jù)。如圖:

字節(jié)碼技術(shù)在模塊依賴分析中的應(yīng)用

Android Studio 能準(zhǔn)確分析到模塊之間“方法級(jí)別”的引用關(guān)系,支持在 IDE 中跳轉(zhuǎn)查看,也能掃描到對(duì) Android SDK 的引用。

這個(gè)方案比前面兩個(gè)都優(yōu)秀,主要是準(zhǔn)確。但是它也有幾個(gè)問題:

  • 耗時(shí)較長:全面分析 AMap 全源碼,大約需要 10 分鐘。

  • 分析結(jié)果無法為第三方復(fù)用,無法生成可視化的依賴關(guān)系圖。

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

總結(jié)一下上述三種方案:Gralde 依賴基于工程配置,粒度太粗且結(jié)果不準(zhǔn)?!癐mport 掃描方案”能拿到文件級(jí)別依賴但數(shù)據(jù)不全。IDE 掃描雖然結(jié)果精準(zhǔn),但是數(shù)據(jù)復(fù)用困難,不便于工程化。

為什么要使用字節(jié)碼來分析?


字節(jié)碼技術(shù)在模塊依賴分析中的應(yīng)用

參考 Android 構(gòu)建流程圖,所有的 Java 源代碼和 aapt 生成的 R.java 文件,都會(huì)被編譯成 .class 文件,再被編譯為 dex 文件,最終通過 apkbuilder 生成到 apk 文件中。圖中的 .class 文件即是我們所說的 Java 字節(jié)碼,它是對(duì) Java 源碼的二進(jìn)制轉(zhuǎn)義。

在 Android 端,常見的字節(jié)碼應(yīng)用場景包括:

  • 字節(jié)碼插樁:用于實(shí)現(xiàn)對(duì) UI 、內(nèi)存、網(wǎng)絡(luò)等模塊的性能監(jiān)控。

  • 修改 jar 包:針對(duì)無源碼的庫,通過編輯字節(jié)碼來實(shí)現(xiàn)一些簡單的邏輯修改。

回到本文的主題,為什么要分析字節(jié)碼,而不是 Java 代碼或者 dex 文件?

不使用 Java 代碼是因?yàn)橛行煲?jar 或者 aar 的方式提供,我們獲取不到源碼。不使用 dex 文件是因?yàn)樗鼪]有好用的語法分析工具。所以解析字節(jié)碼幾乎是我們唯一的選擇。

如何使用字節(jié)碼分析依賴關(guān)系?


要得到模塊之間的依賴關(guān)系,其實(shí)就是要得到“模塊間類與類”之間的依賴關(guān)系。而要確定類之間的關(guān)系,分析類字節(jié)碼的語句即可。

1. 在什么時(shí)機(jī)來分析?

了解 Android 構(gòu)建流程的同學(xué),應(yīng)該對(duì) transform 這個(gè)任務(wù)不陌生。它是 Android Gradle 插件提供的一個(gè)字節(jié)碼 Hook 入口。

在 transform 這個(gè)任務(wù)中,所有的字節(jié)碼文件(包括三方庫) 以 Input 的格式輸入。

以JarInput 為例,分析其 file 字段,可得到模塊的名稱。解析 file 文件,即可得到此模塊所有的字節(jié)碼文件。

字節(jié)碼技術(shù)在模塊依賴分析中的應(yīng)用

有了模塊名稱和對(duì)應(yīng)路徑下的 class 文件,就建立了模塊與類的對(duì)應(yīng)關(guān)系,這是我們拿到的第一個(gè)關(guān)鍵數(shù)據(jù)。

2. 使用什么工具分析?

解析 Java 字節(jié)碼的工具,最常用的包括 Javassit,ASM,CGLib。ASM 是一個(gè)輕量級(jí)的類庫,性能較好,但需要直接操作 JVM 指令。CGLib 是對(duì) ASM 的封裝,提供了更高級(jí)的接口。

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

3. 具體方案是怎樣的?

先看一個(gè)簡單的示例,如何分析下面這段代碼的調(diào)用關(guān)系:


1: package com.account;
2: import com.account.B;
3: public class A {
4:     void methodA() {
5:         B b = new B(); // 初始化了 Class B 的實(shí)例 b
6:         b.methodB();   // 調(diào)用了 b 的 methodB 方法
7:     }
8: }

第1步:初始化環(huán)境,加載字節(jié)碼 A.class,注冊語句分析器。


// 初始化 ClassPool,將字節(jié)碼文件目錄注冊到 Pool 中。
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath('<class文件所在目錄>')
// 加載類A
CtClass cls = pool.get("com.account.A");
// 注冊表達(dá)式分析器到類A
MyExprEditor editor = new MyExprEditor(ctCls)
ctCls.instrument(editor)

第2步:自定義表達(dá)式解析器,分析類A(以解析語句調(diào)用為例)。


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

ExprEditor 的 edit(MethodCall m) 回調(diào)能攔截 Class A 中所有的方法調(diào)用(MethodCall)。

除了本例中對(duì) MethodCall 的解析,它還支持解析 new,new Array,ConstructorCall,F(xiàn)ieldAccess,InstanceOf,強(qiáng)制類型轉(zhuǎn)換,try-catch 語句。

解析完 Class A,我們得到了 A 對(duì) B 的依賴信息 :


Class1 Class2 Expr method1 method2 lineNo
com.account.A com.account.B NewExpr methodA 5
com.account.A com.account.B methodCall methodA methodB 6

簡單解釋如下:

類 com.account.A 的第5行(methodA方法內(nèi)),調(diào)用了 com.account.B 的構(gòu)造函數(shù);

類 com.account.A 的第6行(methodA方法內(nèi)),調(diào)用了 com.account.B 的 methodB 函數(shù);

這便是“類和類之間方法級(jí)”的依賴數(shù)據(jù)。結(jié)合第1步得到的“模塊和類”的對(duì)應(yīng)關(guān)系,最終我們便獲得了“模塊間方法級(jí)的依賴數(shù)據(jù)”。

基于這些基礎(chǔ)數(shù)據(jù),我們還可以自定義依賴檢測規(guī)則、生成全局的模塊依賴關(guān)系圖等,本文就不展開了。

小結(jié)


本文主要介紹了模塊依賴分析在研發(fā)過程中的重要性,分析了 Android 常見的依賴分析方案,從 Gradle 依賴樹分析, Import 掃描,使用 IDE 分析,到最后的字節(jié)碼解析,方案逐步遞進(jìn)。越是接近源頭的解法,才是越根本的解法。

字節(jié)碼技術(shù)在模塊依賴分析中的應(yīng)用

關(guān)注高德技術(shù),找到更多出行技術(shù)領(lǐng)域?qū)I(yè)內(nèi)容
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI