軟體研發中,耗費最多的並不是編寫程式碼,而是程式碼編譯和程式碼不斷除錯的過程。對於我們Android來說,隨著專案的不斷迭代,以及業務模組的不斷增加,專案技術棧的增加,專案編譯會越來越慢。隨著業務的擴充套件,相信很多的公司都已經做了模組化/元件化。
背景
建立一個 Project 後可以建立多個 Module,這個 Module 就是所謂的模組。一個簡單的例子,可能在寫程式碼的時候我們會把首頁、訊息、我的模組拆開,每個 tab 所包含的內容就是一個模組,這樣可以減少 module 的程式碼量,但是每個模組之間的肯定是有頁面的跳轉,資料傳遞等,比如 A 模組需要 B 模組的資料,於是我們會在 A 模組的 gradle 檔案內通過 implementation project(':B')
依賴 B 模組,但是 B 模組又需要跳轉到 A 模組的某個頁面,於是 B 模組又依賴了 A 模組。這樣的開發模式依然沒有解耦,改一個bug依然會改動很多模組,並不能解決大型專案的問題。於是就有了元件的概念,我們日常業務需求開發的元件叫做業務元件,如果這個業務需求是可以被普遍複用的,那麼叫做業務基礎元件,譬如圖片載入、網路請求等框架元件我們稱為基礎元件。於是一個典型的元件化架構通常如下圖所示。
實線表示直接依賴關係,虛線表示間接依賴。比如殼工程肯定是要依賴業務基礎元件、業務元件、module_common公共庫的。業務元件依賴業務基礎元件,但並不是直接依賴,而是通過”下沉介面“來實現間接呼叫。業務元件之間的依賴也是間接依賴。最後common元件依賴所有需要的基礎元件,common也屬於基礎元件,它只是統一了基礎元件的版本,同時也提供了給應用提供一些抽象基類,比如BaseActivity、BaseFragment,基礎元件初始化等。
編譯優化
Android編譯流程
Android apk的編譯構建分為四個步驟:
- 程式碼編譯:將原始碼,R檔案,AIDL生成的檔案等 編譯成.class檔案;
- 程式碼合成:通過dex工具將.class檔案和工程依賴的第三方庫檔案生成虛擬機器可執行的.dex檔案,如果使用了MultiDex會產生多個dex檔案;
- 資源打包:apkbuilder工具將.dex檔案,apt編譯後的資原始檔,三方庫中的資原始檔打包生成簽名對齊的apk檔案;
- 簽名和對齊:使用Jarsigner和Zipalign對檔案進行簽名和對齊,生成最終的apk檔案。
以下是gradle編譯一個app module 的task鏈:
gradle clean assembleDebug -x lint check –stacktrace
:app:clean //清理上次編譯的遺留,刪除module下的build資料夾
:app:preDebugBuild //debug版本預編譯
:app:checkDebugManifest //AndroidManifest檢查
:app:prepareDebugDependencies //檢查debug版本的依賴
:app:compileDebugAidl // 編譯debug版本的aidl檔案
:app:compileDebugRenderscript //編譯Renderscript檔案
:app:generateDebugBuildConfig //generated/source資料夾下,生成buildConfig資料夾
:app:generateDebugAssets //生成Assets檔案到generated下的asset資料夾
:app:mergeDebugAssets //在intermediates下生成assets資料夾,將其他module/aar中的assets檔案拷貝過來
:app:generateDebugResValues //生成res value檔案
:app:generateDebugResources //生成Resources檔案
:app:mergeDebugResources //merge(合併)資原始檔
:app:processDebugManifest //將merge後的Manifest檔案放在intermediates/manifests資料夾下
:app:processDebugResources //處理資原始檔,生成R.txt檔案,同時也生成對應的multidex資料夾
:app:generateDebugSources //合成資原始檔在generated資料夾下生成對應的R.java檔案
:app:compileDebugJavaWithJavac //使用javac生成java檔案
:app:compileDebugNdk //ndk編譯
:app:compileDebugSources //編譯資原始檔
:app:transformClassesWithDexForDebug //將.class檔案轉換成.dex檔案
:app:mergeDebugJniLibFolders //合併jni(.so)檔案
:app:transformNative_libsWithMergeJniLibsForDebug //轉換jni檔案
:app:processDebugJavaRes //處理java資源
:app:transformResourcesWithMergeJavaResForDebug //轉換java資原始檔
:app:validateSigningDebug //驗證簽名
:app:packageDebug //打包
:app:assembleDebug //apk編譯完成
開啟InstantRun
Android Studio 2.0 推出了InstantRun,意為瞬間編譯,在編譯開發時減少應用的部署及構建時間。如果需要開啟InstantRun,需要Gradle2.0和minSdkVersion15以上版本。
構建流程:程式碼變更-->編譯-->應用構建-->應用部署-->app重啟-->activity重啟-->完成修改變更
實現即時執行的機制:修改程式碼後,增量構建(產生增量dex),然後通過判斷更新資源的複雜度去選擇執行熱更新,溫更新或者冷更新;
- 熱部署:生效時不需要重啟app,也不需要重啟activity
- 溫部署:重啟activity後才能看到更新
- 冷部署:app需要重啟,但不是重新安裝
InstantRun主要乾了兩件事:
- 使用manifest-merger整合專案的manifest,通過aapt工具將合成的AndroidManifest.xml檔案與res資源編譯到增量apk中;
- 程式碼修改後,通過javac將java檔案編譯成class檔案,然後打包成dex檔案,同樣放置在增量apk中;
gradle編譯優化
我們知道,Android工程是使用gradle進行構建的,所以,優化Android的編譯時間,在gradle方面有很多的措施。
properties配置優化
#開啟並行編譯,僅僅適用於模組化專案(存在多個 Library 庫工程依賴主工程)
org.gradle.parallel=true
# 使用編譯快取
android.emableBuildCache=true
# 開啟構建快取,Gradle 3.5新的快取機制,可以快取所有任務的輸出,
# 不同於buildCache僅僅快取dex的外部libs,它可以複用任何時候的構建快取,設定包括其它分支的構建快取
org.gradle.caching=true
# 構建初始化需要執行許多工,例如java虛擬機器的啟動,載入虛擬機器環境,載入class檔案等等,
# 配置此項可以開啟執行緒守護,並且僅僅第一次編譯時會開啟執行緒(Gradle 3.0版本以後預設支援)
# 保證jvm編譯命令在守護程式中編譯apk,daemon可以大大減少載入jvm和classes的時間
org.gradle.daemon=true
# 最大的優勢在於幫助多 Moudle 的工程提速,在編譯多個 Module 相互依賴的專案時,
# Gradle 會按需選擇進行編譯,即僅僅編譯相關的 Module
org.gradle.configureondemand=true
# 配置編譯時的虛擬機器大小,加大編譯時AndroidStudio使用的記憶體空間
# -Xmx2048m:指定 JVM 最大允許分配的堆記憶體為 2048MB,它會採用按需分配的方式。
#-XX:MaxPermSize=512m:指定 JVM 最大允許分配的非堆記憶體為 512MB,同上堆記憶體一樣也是按需分配的。
org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
過濾gradle task
在執行構建任務時,選擇性的去除並不需要執行的gradle task任務。
tasks.whenTaskAdded(new Action<Task>() {
@Override
void execute(Task task) {
if (task.name.contains("lint") //不掃描潛在bug可以使用該項
|| task.name == "clean"
|| task.name.contains("Aidl") //專案中用到Aidl則不可以捨棄這個任務
|| task.name.contains("mockableAndroidJar")//用不到測試時可以先關閉
|| task.name.contains("UnitTest")//用不到測試時可以先關閉
|| task.name.contains("AndroidTest")//用不到測試時可以先關閉
|| task.name.contains("Ndk") || task.name.contains("Jni")//用不到NDK和jni時關閉
) {
task.enabled = false
}
}
})
使用本地gradle
使用本地的gradle檔案,避免從網路拉取的情況。
其他
將不需要頻繁改動的module從setting.gradle中去掉,直接引用module對應的aar檔案。工程中有多個module時,會先編譯每一個module之後再編譯主工程,儘量少的module依賴肯定會加快編譯速度。另外,如果你使用的是Kotlin+JetPack方式來構建的Android專案,那麼可以嘗試使用KSP:告別KAPT,使用KSP為Android編譯提速