您當前的位置:首頁 > 舞蹈

Android編譯提速黑科技—Wade Plugin

作者:由 Android小瓜 發表于 舞蹈時間:2021-12-17

作者:潘濤

原文連結:https://mp。weixin。qq。com/s/414nz4T-_KyH42xo_mdj-w

Android編譯提速黑科技—Wade Plugin

隨著得物App業務高速發展,Android專案的程式碼量與元件數量迅速增加,專案編譯時長也明顯升高。今年初增量編譯平均耗時接近3。9分鐘,嚴重影響了開發效率,也促使我們探索各種措施縮短編譯時間提升開發效率。

背景

Android編譯提速黑科技—Wade Plugin

四月初透過一系列常規最佳化,如改造增量註解處理器、 增量Transform、元件化、工程化、最佳化專案配置等等,耗時縮短到2。3分鐘。六月,Wade Plugin 第一版上線,進一步縮短到1。3分鐘。八月, Wade第二個大版本上線,最終將增編耗時降低到了0。8分鐘。本文主要介紹Wade Plugin的技術原理和實現思路。

簡介

Wade Plugin是得物Android自研的Gradle外掛,用於提升編譯速度。常規最佳化手段用盡以後, 專案的增編耗時仍需2。3分鐘, 其中DexArchiveBuilder、MergeProjectDex、 MergeLibDex、MergeExtDex佔了1。7分鐘。不難看出,如果要進一步降低耗時, 應該挖掘DexBuild和DexMerge的最佳化空間。

Android編譯提速黑科技—Wade Plugin

Wade Plugin透過Hook Android原生的編譯流程, 將原生的DexBuildTask替換為WadeDexBuild, 原生的DexMergeTask替換為WadeDexMerge。原生DexBuild平均耗時60秒, WadeDexBuild只需12秒; 原生DexMerge平均耗時42秒, WadeDexMerge只需2秒。

Android編譯提速黑科技—Wade Plugin

WadeDexBuild

原理

呼叫Dx或D8工具完成Class到Dex的轉換這一過程稱為DexConvert, 它佔了DexBuild大部分耗時。原生DexBuild以Jar和Class為粒度執行DexConvert。得物工程中平均1個Jar包含200+個。class, 相當於增量時每改動一個類會觸發200個類執行DexConvert。

Android編譯提速黑科技—Wade Plugin

理想情況是隻有改動的Class參與DexConvert。

最佳化方案

在DexConvert執行前, 解壓縮Jar, 以。class為粒度執行DexConvert。並且只有其中變更的。class參與, 未變更的。class執行結果複用上次編譯快取。具體有四種變更型別對應的快取複用策略: 對於新增的。class, 需要參與DexConvert;改動的。class參與DexConvert;移除的。class不參與DexConvert;未改變的也不參與。

Android編譯提速黑科技—Wade Plugin

其中, 移除。class的情況要特殊處理。 例如Demo。class移除後, 除了相應刪除產物Demo。dex, 還要尋找它的內部類的產物Demo

1.dex, Demo

2。dex等。

主要實現

輸入(Task Inputs)

Android編譯提速黑科技—Wade Plugin

首先要根據Consumer Transform決定參與WadeDexBuild的。class檔案路徑, 消費Transform Inputs的Transform即Consumer Transform。當接入了一個Consumer Transform, 它的輸出路徑參與編譯; 沒有Consumer Transform時, Java Compile、Kotlin Compile的輸出路徑參與編譯; 如有多個Consumer Transforms, 取最後一個Transform的輸出路徑作為DexBuild輸入。

增量編譯觸發條件

觸發條件決定了本次編譯是否走增量邏輯, 以及上次編譯的快取是否可用。WadeDexBuild的增量條件包括五大類共28條(AGP3和AGP4略有不同):

Android編譯提速黑科技—Wade Plugin

Gradle配置

1。AndroidJarClasspath

2。DesugaringClasspathClasses

3。ErrorFormatMode

4。MinSdkVersion

5。Dexer

6。UseGradleWorkers

7。InBufferSize

8。Debuggable

9。Java8LangSupportType

10。ProjectVariant

11。NumberOfBuckets

12。DxNoOptimizeFlagPresent

Wade配置

13。WadeExtension。scope

14。WadeExtension。duplicateClass

15。WadeExtension。dexBucketSize

16。WadeExtension。jarBucketSize

Wade快取

17。ProjectWorkspaceDir

18。SubProjectWorkspaceDir

19。ExternalLibWorkspaceDir

20。MixedScopeWorkspaceDir

輸入檔案

21。ProjectClasses

22。SubProjectClasses

23。ExternalLibClasses

24。MixedScopeClasses

產物檔案

25。ProjectOutputDex

26。SubProjectOutputDex

27。ExternalLibOutputDex

28。MixedScopeOutputDex

其中Gradle配置相關的條件和原生的觸發條件相似。觸發全量編譯的情況, 例如Gradle配置中的Dexer由D8 Dexer改為DX Dexer, 上次編譯快取肯定無法複用, 需要重新完整編譯。觸發增量編譯的情況, 例如修改了一個Kotlin類, 導致輸入檔案中的MixedScopeClasses有變化, 此時編譯快取應可複用, 則觸發增量編譯。

Dex Convert

WadeDexBuild關鍵步驟是將原生Dex Convert由Jar為粒度轉換為Class為粒度執行。首先解壓縮Jar, 解壓後的。class寫入快取目錄, 再將參與上次編譯的Class與參與本次編譯的Class檔案逐個對比, 只有新增和變更的Class參與Dex Convert, 移除和未改變的直接刪除或沿用對應快取。

Android編譯提速黑科技—Wade Plugin

效能最佳化

Android編譯提速黑科技—Wade Plugin

Dex Convert粒度由Jar轉換為Class後耗時明顯降低。但專案中共有423個Jar, 解壓後83000+個Class, 導致Dex Convert前解壓縮和檔案對比兩個步驟非常耗時。對這兩步的最佳化主要有三方面。

ForkJoinPool

用ForkJoinPool替代傳統的ExecutorService做併發, 因為它的Work Steeling演算法特別適合小檔案, 任務數特別多的場景, 能夠最大化利用CPU空閒時間。

mmap

檔案對比是I/O密集型任務, 普通檔案流的讀寫速度較慢。Wade Plugin所有I/O操作都用mmap實現, 包括讀、寫、複製等。檔案流替換為mmap對整體速度提升有很明顯的效果。

CRC-32代替MD5

對比兩檔案是否相同的常規做法是先比較檔案長度, 再校驗檔案MD5是否一致。由於Class數量太多, 計算MD5的耗時非常可觀。用CRC-32演算法計算檔案Hash, 作為Checksum來代替MD5能減少檔案對比的時間。

CRC-32計算的Checksum可靠性不如MD5, 理論上會有Hash碰撞, 導致修改Class修改後被誤判為未修改, 接著使用快取而非最新檔案參與編譯, 反映到產物APK上意味著這次修改無效。但是實際發生機率極低, 整體來看值得犧牲理論上的正確性來保證每次編譯的效率。

最佳化效果

最佳化後解壓縮、寫快取平均耗時5700ms, 檔案對比耗時得益於CRC-32演算法只需10ms, DexBuild整體耗時從原生的60秒降低到12秒。

Android編譯提速黑科技—Wade Plugin

WadeDexMerge

最佳化方案

DexMerge透過合併。dex檔案來降低APK內Dex檔案數量和體積, 提升安裝速度和首次執行速度。原生DexMerge的缺點是不支援增量編譯, 耗時和Dex檔案數量成正比, 得物專案的DexMerge耗時在30~60秒之間。對於程式碼量少, 類總數不多的專案可以不執行DexMerge。AGP本身也有自動跳過DexMergingTask的邏輯, 當MinSDKVersion>23時, Dex數量小於500個不會執行DexMerge, MinSDKVersion<23時, Dex數量小於50個則自動跳過DexMerge。Hook DexMergingTask可以做到忽略AGP的Dex數量閾值強行跳過DexMerge。但對於Dex數非常多的工程, 強行跳過DexMerge的副作用明顯, 在得物App上強行跳過會導致包體積增加40M左右、安裝APK耗時增加15秒、首次啟動耗時增加約10秒。WadeDexMerge支援了強行跳過DexMerge與增量Merge兩種策略, 預設使用增量Merge。跳過DexMerge的實現比較簡單, 只需注意隨後的PackageTask只識別。dex, 而不能識別。jar, 要先處理DexBuild產物中的。jar檔案, 再和。dex產物一起複製到PackageTask的inputDir即可, 其中inputDir可以透過反射PackageAndroidArtifact。getDexFolders()獲得。這裡主要介紹WadeDexMerge增量編譯的實現。

主要實現

Android編譯提速黑科技—Wade Plugin

DexMerge輸入檔案有。jar和。dex, 輸出。dex檔案。增量實現的核心是對輸入檔案作分桶, 只對變更的桶Merge, 其他桶複用快取。

Android編譯提速黑科技—Wade Plugin

假設本次編譯只有Bucket0中一個檔案發生變更, 其他Bucket均無變化, 那麼只需對Bucket0做Merge。分桶後, 需要找出本次編譯相比於上次編譯變更了哪些檔案以及它們的變更型別。這個場景類似於經典演算法題“如何找出兩個陣列中不相同的元素?”,因此可以用快慢指標來計算檔案變更。

Android編譯提速黑科技—Wade Plugin

如圖,慢指標指向上次編譯的檔案陣列, 快指標指向本次編譯的檔案陣列, 對比兩個指標的檔案, 如果相同則快指標指向下一個檔案, 直到找到不同, 此時慢指標指向下一個檔案, 再開始下一輪對比。虛擬碼如下:

long

fast

=

0

long

slow

=

0

while

slow

<

prev

size

())

{

long

temp

=

fast

while

temp

<

curr

size

())

{

if

prev

slow

==

curr

temp

])

{

break

}

temp

++

}

if

temp

!=

curr

size

())

{

fast

=

temp

boolean

isModified

=

isModified

prev

slow

],

curr

fast

],

reuseScope

if

isModified

{

//found difference

fileChanges

add

new

DefaultFileChange

prev

slow

],

ChangeType

MODIFIED

))

}

}

else

{

//not found

fileChanges

add

new

DefaultFileChange

prev

slow

],

ChangeType

REMOVED

))

}

slow

++

}

桶總數和桶內檔案數

Bucket

Size

直接影響到增量效果

理論上

分桶越多越好

如果有100個Bucket

相當於增量只需1

/

100的全量Merge時間

但Bucket越多意味著APK內

dex越多

又會影響到包體積

安裝時間和首次啟動耗時

經過多次試驗

Bucket總數在50

100個時綜合效果最好

Merge耗時降低明顯

副作用也不大

目前得物工程中共有66個Bucket

其中Jar型別23個

Dex型別43個

高可用

在高可用建設方面, 主要透過資料統計、建立編譯情況監控、編譯指標週報及時獲取大盤情況和發現問題; 相容不同AGP和Gradle版本以提高外掛的相容性; 持續監控編譯異常並迭代修復問題提高穩定性。

七大指標

Android編譯提速黑科技—Wade Plugin

七個指標反映團隊的編譯總體情況:

增量編譯耗時

平均編譯耗時

全量編譯耗時

增量編譯耗時50分位值

增量佔比

編譯成功率

人均編譯總時長

指標的計算依賴埋點資料上報, 埋點中部分欄位的值較難獲取。例如本次編譯的JavaCompileTask是否為增量, 需透過對AGP和Gradle插樁實現, 有三處Hook點可以切入。

Android編譯提速黑科技—Wade Plugin

Wade早期版本使用方案一, 實際使用發現Hook Gradle的類相容性較差。目前使用方案二, Hook AGP的

com。android。build。gradle。tasks。JavaCompileCreationAction

類, 注入

WadeJavaCompile

類代替原生的

org。gradle。api。tasks。compile。JavaCompile

類。

WadeJavacCompile

JavaCompile

的包裝類, 重寫

compile()

取到Javac的增量標識

inputs。isIncremental

。 虛擬碼如下:

public

class

WadeJavaCompile

extends

JavaCompile

{

。。。

private

static

File

mFile

@Override

protected

void

compile

IncrementalTaskInputs

inputs

{

。。。

boolean

isIncremental

=

inputs

isIncremental

();

try

{

FileUtils

writeStringToFile

mFile

“isIncremental:”

+

isIncremental

+

“\n”

true

);

}

catch

IOException

e

{

。。。

}

super

compile

inputs

);

}

。。。

}

對AGP原生類的Hook過程大致可分為3步, 獲取Gradle的

VisitableURLClassLoader

, 用ASM或Javassist編輯目標類的位元組碼, 反射呼叫

ClassLoader。defineClass()

載入編輯後的位元組碼。

Gradle程序和Gradle Daemon程序一般常駐後臺, Android Studio開啟後第一次編譯會觸發載入AGP類的位元組碼, 之後再編譯都不會觸發類載入, 所以只有一次Hook機會, 必須保證Hook的位元組碼比AGP“搶先”載入到

VisitableURLClassLoader

。因此, Wade外掛接入要求在Root Project中

apply wade plugin

, 以確保Hook程式碼能在App Project的

apply android plugin

之前執行。

相容性

主要相容了AGP3和AGP4、Gradle5和Gradle6兩套版本。

Android編譯提速黑科技—Wade Plugin

外掛中的關鍵步驟如增量編譯觸發條件、反射獲取Consumer Transform、WadeDexMergeTask等都針對不同版本分別做了適配。

穩定性

實際使用過程中遇到了各種疑難雜症, 這裡列出前10個常見異常。

java。io。IOException: The input doesn‘t contain any classes。 Did you specify the proper ’-injars‘ options?

java。io。FileNotFoundException: /Users/panes/app/build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/R。jar (No such file or directory)

Caused by: com。android。tools。r8。utils。b: Error:YeezyCompleteListener。class, Type

http://

com。xxx

is defined multiple times

Caused by: org。gradle。api。UncheckedIOException:java。util。zip。ZipException: error in opening zip file

Caused by: com。android。tools。r8。utils。b: Error: Class content provided for type descriptor xxx。r actually defines class com。xxx。R

A failure occurred while executing com。android。build。gradle。internal。tasks。Workers$ActionFacade

com。android。builder。dexing。DexArchiveMergerException: Error while merging dex archives: Type com。xxx。R is defined multiple times

base。apk code is missing

Archive is not readable : /Users/panes/android/app/build/intermediates/mixed_scope_dex_archive/developerDebug/out/c6795cc73f81ff9c1c0b5d0adb06b1b4161c540cbf761ba11415aae4856b11b4_4。jar

Could not determine dependencies of app:wadeInputChangesInspect

經過近30個版本的迭代, 這些問題都已解決。最近版本v2。6。4上線至今經歷6800次編譯, 異常次數4次。

基準測試

Android編譯提速黑科技—Wade Plugin

Benchmark跑分顯示, 10次增量編譯(只改動一行程式碼)的平均耗時14。4秒, 10次無量編譯(程式碼不變)平均耗時6。2秒。跑分時清理後臺任務、關閉了其他佔用資源的程序, 但實際編譯環境比理想環境複雜得多, 基準測試只用於驗證理論是否有效。

總結

Wade Plugin開發過程中困難重重, 重寫Android原生的編譯流程做到既大幅提升速度又保證穩定可靠並非易事。其中還有更多細節未介紹到, 如增編時識別熱點程式碼、複用檔案變更計算結果、Hook PackageTask做Apk內檔案兜底防止出包異常。同時也期待後續版本能有更多提升。

標簽: 編譯  耗時  檔案  增量  Class