Android 編譯優化

xiangzhihong發表於2022-03-16

軟體研發中,耗費最多的並不是編寫程式碼,而是程式碼編譯和程式碼不斷除錯的過程。對於我們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的編譯構建分為四個步驟:

  1. 程式碼編譯:將原始碼,R檔案,AIDL生成的檔案等 編譯成.class檔案;
  2. 程式碼合成:通過dex工具將.class檔案和工程依賴的第三方庫檔案生成虛擬機器可執行的.dex檔案,如果使用了MultiDex會產生多個dex檔案;
  3. 資源打包:apkbuilder工具將.dex檔案,apt編譯後的資原始檔,三方庫中的資原始檔打包生成簽名對齊的apk檔案;
  4. 簽名和對齊:使用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主要乾了兩件事:

  1. 使用manifest-merger整合專案的manifest,通過aapt工具將合成的AndroidManifest.xml檔案與res資源編譯到增量apk中;
  2. 程式碼修改後,通過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編譯提速

相關文章