Android 優化 APP 構建速度的 17 條建議

依然範特稀西發表於2017-03-22

較長的構建時間將會減緩專案的開發進度,特別是對於大型的專案,app的構建時間長則十幾分鍾,短則幾分鐘,長的構建時間已經成了開發瓶頸,本篇文章根據Google官方文件,加上自己的一些理解提供一些提升app構建速度的優化建議。

1. 為開發環境建立一個變體

有許多配置是你在準備app的release 版本的時候需要,但是當你開發app的時候是不需要的,開啟不必要的構建程式會使你的增量構建或者clean構建變得很慢,因此需要構建一個只保留開發時需要配置的變體,如下例子建立了一個devprod變體(prod 為release 版本的配置)。

android {
  ...
  defaultConfig {...}
  buildTypes {...}
  productFlavors {
    // When building a variant that uses this flavor, the following configurations
    // override those in the defaultConfig block.
    dev {
      // To avoid using legacy multidex, set minSdkVersion to 21 or higher.
      minSdkVersion 21
      versionNameSuffix "-dev"
      applicationIdSuffix '.dev'
    }

    prod {
      // If you've configured the defaultConfig block for the release version of
      // your app, you can leave this block empty and Gradle uses configurations in
      // the defaultConfig block instead. You still need to create this flavor.
      // Otherwise, all variants use the "dev" flavor configurations.
    }
  }
}

2 . 避免編譯不必要的資源

避免編譯和包含你沒有測試的資源(比如新增的一個本地的語言和螢幕密度資源),你可以只在你的’dev’ flavor下指定一種語言和一個螢幕密度,如下:

android {
  ...
  productFlavors {
    dev {
      ...
      // The following configuration limits the "dev" flavor to using
      // English stringresources and xxhdpi screen-density resources.
      resConfigs "en", "xxhdpi"
    }
    ...
  }
}

上面的配置將會限制dev 變體只使用 english string 資源和 xxhdpi 螢幕密度資源。

3 . 配置debug 構建的Crushlytics為不可用狀態

在debug 構建狀態下,如果你不需要執行崩潰上報,你可以將這個外掛設定為不可用狀態來提升你的構建速度,如下:

android {
  ...
  buildTypes {
    debug {
      ext.enableCrashlytics = false
    }
}

上面只是舉個例子,Crushlytics 為崩潰上報分析工具,在開發的時候我們可能不需要,因此不需要開啟,在我們實際開發中,像崩潰上報SDK,資料統計SDK等(如 友盟統計、GrowingIO、百度統計)在開發階段都設定為不可用,來提升構建速度。

4 . 用靜態的構建配置值來構建你的Debug版

一般地,在你的debug 構建時,為manifest檔案或者資原始檔配置使用靜態/硬編碼的值。如果你的manifest或者資原始檔的值每次構建都需要動態更新,那麼Instant Run 無法執行程式碼交換-它必須重新構建和安裝新的APK。

例如,使用動態的version codes ,version names ,resources或者其他更改manifest檔案的構建邏輯,每次你想執行一個修改都會構建全部APK,即使實際的修改可能僅僅只需要熱交換。如果這些構建配置是需要動態配置的,那麼將它們從你的release 構建變體中分離出來,並且在你的debug 構建中保留它們的靜態值。像下面build.gradle 檔案顯示的這樣:

int MILLIS_IN_MINUTE = 1000 * 60
int minutesSinceEpoch = System.currentTimeMillis() / MILLIS_IN_MINUTE

android {
    ...
    defaultConfig {
        // Making either of these two values dynamic in the defaultConfig will
        // require a full APK build and reinstallation because the AndroidManifest.xml
        // must be updated (which is not supported by Instant Run).
        versionCode 1
        versionName "1.0"
        ...
    }

    // The defaultConfig values above are fixed, so your incremental builds don't
    // need to rebuild the manifest (and therefore the whole APK, slowing build times).
    // But for release builds, it's okay. So the following script iterates through
    // all the known variants, finds those that are "release" build types, and
    // changes those properties to something dynamic.
    applicationVariants.all { variant ->
        if (variant.buildType.name == "release") {
            variant.mergedFlavor.versionCode = minutesSinceEpoch;
            variant.mergedFlavor.versionName = minutesSinceEpoch + "-" + variant.flavorName;
        }
    }
}

5 . 用靜態的版本依賴

當你在build.gradle檔案中宣告依賴的時候,你應該避免在版本號結束的地方使用+號,比如:com.android.tools.build:gradle:2.+ 因為Gradle的檢查更新,用動態的版本號會導致未知的版本更新、使解決版本的差異變得困難和更慢的構建。你應該使用靜態或者硬編碼版本號來代替。如:com.android.tools.build:gradle:2.2.2 。

6 . 使 on demand 配置為enable 狀態

為了讓Gradle能夠確切的知道該如何構建你的APP,在每次構建之前,構建系統配置工程的所有modules和其他依賴(即使你只想構建或者測試一個modules),這使得大型的多module 工程的構建速度變得很慢。告訴Gradle僅僅配置你想要構建的Modules,用如下步驟使 on demand 配置可用

(1) 在選單欄上選擇 File -> Settings(如果是Mac上 ,選擇 Android Studio ->Preferences)

(2) 導航到 Build,Execution,Deployment -> Compiler

(3) check Configure on demand 核取方塊

(4) 點選 OK

如圖:

7 . 建立 library 模組

檢查你app中的程式碼,將可模組化的程式碼抽取一個Android Library module,通過這種方式模組化你的程式碼將允許構建系統僅僅只編譯那些有改動的模組,並將其構建結果快取下來以被後面的構建使用。同樣的配置了 on demand 和 parallel project execution (project 並行執行) 將更加高效(當你開啟這些特性時)。

8 . 為自定義構建邏輯建立Tasks

在你建立了 build profile (build profile 後文會講)之後,如果顯示構建時間相對長的一部分時間花在“configure project(配置工程)階段,那麼請 review 你的build.gradle 指令碼,並且查詢可包含到自定義Gradle Task中的程式碼,通過將一些構建邏輯移動到一個task 中,當需要的時候才執行,結果能被快取用於後續的構建,並且這個構建邏輯可以並行執行(如果你開啟了 並行執行project),更多詳細資訊請閱讀Gradle官方文件。 official Gradle documentation

小提示:
如果你的構建包含了大量的自定義任務tasks,你可能想清理你的build.gradle檔案,通過自定義task classes (也就是自定義Gradle 外掛啦),將你的classes 新增到 project-root/buildSrc/src/main/groovy/目錄下,Gradle將自動包含它們到class path ,為專案的所有build.gradle檔案。

9 . 配置 dexOptions 和 開啟 library pre-dexing(dex預處理)

先補充一個知識點:Dex-in-process:新發布的Android Studio 2.1增加了一個新的特性:Dex In Process,可以極大的加快重新編譯的速度,同樣也能提高Instant Run的效能。(第10條優化建議會說到)
詳情請看Faster Android Studio Builds with Dex In Process

Android 外掛提供了 dexOptions script block ,因此你可以配置相應的 DEX 構建特性,它們可以提高構建速度:

(1)preDexLibraaies : 宣告是否對依賴的庫進行dex 預處理來使你的增量構建更快速,因為這個特性可能會使你的clean 構建變慢,因此在你的持續整合伺服器上你可能想關閉這個特性。

(2) maxProcessCount : 設定最大的執行緒數量使用當執行 dex-in-process時,預設值是4。

(3)javaMaxHeapSize: 為DEX 編譯器 設定最大的堆大小,相對於設定這個屬性,你應該增加 Gradle的 堆大小(這個堆大小dex-in-process可用的時候對DEX 編譯器有效)

例子:

android {
  ...
  dexOptions {
    preDexLibraries true
    maxProcessCount 8
    // Instead of setting the heap size for the DEX process, increase Gradle's
    // heap size to enable dex-in-process. To learm more, read the next section.
    // javaMaxHeapSize "2048m"
  }
}

你應該增加它們的值來測試一下這些設定,然後通過profile觀察效果,當你為這個程式分配太多資源的時候,可能會得到一個負面的影響。

10 . 增加Gradle的堆大小 和開啟 dex-in-process

Dex-in-process 允許多個DEX 程式執行在一個單獨的VM 中,這使得增量構建和清理構建變得更快。預設情況下,通過Android Studio2.1 或者更高版本建立的新專案分配了足夠的記憶體來開啟這個特性,如果你沒有使用Android Studio 2.1 或者更高的版本建立專案,你需要給Gradle後臺駐紮程式設定至少1536MB 的堆大小記憶體。預設如下圖:

下面的例子在gradle.properties中將Gradle 堆記憶體大小設定為 2048MB:

org.gradle.jvmargs = -Xmx2048m //設定Gradle 堆大小 2G

在一些大型的專案上,為Gradle堆分配更多的記憶體當然更有利,然而,如果你用的是一個小記憶體的機器,你可能需要給IDE配置更少的記憶體,想知道如何改變分配給IDE資源的數量和Gradle 對構建表現的影響,請看profiling your build這一條。

如果在你的Module build.gradle 檔案中為android.dexOptions.javaMaxHeapSize定義了一個值,那麼你需要給Gradle的堆大小設定 的值為比javaMaxHeapSize多512MB,並且滿足至少為1536MB。舉個例子:在build.gradle中設定javaMaxHeapSize `為1280MB,那麼你就要給Gradle堆大小設定 至少1792MB(1280 + 512),當然了,設定大一點更佳。
build.gradle:

dexOptions {
        javaMaxHeapSize "1280m"
    }

gradle.properties:

org.gradle.jvmargs = -Xmx1792m

11 . 將圖片轉為 WebP格式

WebP是一種圖片檔案格式,它提供了像JPEG一樣的有失真壓縮和像PNG一樣的透明支援,但是同時它的壓縮質量比JPEG或者PNG任何一個都更好,減小Image檔案的大小,而不用在構建時做壓縮,因此它能提高構建速度,尤其是你的APP使用了大量的圖片資源。但是有一點,在解壓WebP格式的圖片的時候,你的裝置的CPU使用將小幅度增加。 用Android Studio 可以很方便的轉WebP格式,詳情請看convert your images to WebP.

小提示:此外,將工程裡面的圖片轉為webP格式也是優化APK體積的一個方向,webp是Android 原生4.0就開始支援的,它能提供和JPEG和PNG相同質量的圖片但是size 更小。沒有任何適配問題。

12 . 禁止使用 PNG crunching

如果你不能(或者不想)轉換你的PNG格式圖片為WebP,你仍然可以通過禁止每次構建app都自動壓縮圖片來提升構建速度,要禁止這項優化,在build.gradle 的新增如下程式碼:

android {
  ...
  aaptOptions {
    cruncherEnabled false
  }
}

13 . 使用 Instant Run

Instant Run顯著的減少了更新app的時間,它通過推送確定的程式碼、資源變更而不用構建一個新的app ,並且在一些情況下,甚至不用重啟當前的activity,在程式碼變更後,使用Instant Run 通過點選Apply Changes(黃色⚡️圖示)。當你做了如下幾步,它會預設開啟:

  • 用debug 構建變體來構建你的app
  • Gradle外掛的版本 2.3.0或者更高
  • 在module層級的build.gradle中設定minSdkVersion為15或者更高
  • 釋出你的app 在Android 5.0(API level 21) 或者更高 點選 Run

14 . 使用構建快取

在構建你的工程的時候,構建快取儲存了Android Gradle外掛生成的確定的產物(如 AAR包和遠端依賴的 pre-dexed)。當你使用快取的時候,你的清理構建更快是因為構建系統後續構建能夠簡單地重用它們的快取而不用重新建立。

新的工程使用Android Gradle 外掛2.3.0或者更高版本預設就開啟了構建快取(除非你手動關閉了),瞭解更多請閱讀Accelerate clean builds with build cache.

15 . 禁止使用註解處理器

Gradle 2.1後可以增量構建Java,當使用註解處理器時增量構建將不可用,如果可以,避免使用註解處理器,讓你從只構建更改的類來獲益。(提升編譯時間)

16 . 分析你的構建(Profile your build)

在大型的專案中(或者實現了大量自定義構建邏輯),可能需要更加深入的瞭解構建程式來尋找瓶頸,你可以通過分析構建生命週期的各個階段 每個gradle task 執行了多長時間。例如:如果你的構建資料顯示Gradle 花了大量的時間來配置你的工程,這建議你需要將自定義構建邏輯放在配置階段之外。另外,如果mergeDevDebugResources 任務 消費了大量的的構建時間,這表明你需要將圖片轉換為WebP格式或者禁止PNG Crunching(第11,12 條優化建議)

通過構建分析來提升你的構建速度通常需要在分析開啟的情況下執行你的構建,多次修改構建配置,分析和觀察結果的變化。

生成和檢視 build profile ,執行下面步驟:

1,用Android Studio開啟專案,選擇 View -> Tool Windows -> Terminal 開啟命令列

2,執行clean build 輸入下面的命令,當你分析你的構建時,每次構建之間需要執行一個 clean build 操作,因為Gradle會跳過輸入沒有 改變的tasks,因此,第二個沒有改變輸入的構建通常會執行得更快因為tasks 沒有重新執行,因此在構建之間執行一個cleantask 保證你分析了全部的構建程式。

//如果在Mac 或者 Linux上 用 ./gradlew
gradlew clean

3,選擇其中一個產品風味(product flavor) 執行debug 構建,比如:dev flavor,如下:

gradlew --profile --recompile-scripts --offline --rerun-tasks assembleFlavorDebug

命令分析:

  • --profile: 開啟profiling
  • --recompile-scripts: 強制指令碼重新編譯跳過cache
  • --offline:禁止 Gradle獲取離線依賴,這是確保任何的延遲都是Gradle試圖更新依賴而導致,不會誤導你的分析資料。你應該先準備好構建一次工程確保Gradle 已經下載好並且快取依賴。
  • --rerun-tasks:強制Gradle返回所有task 並且忽略任何task 優化。

4,構建結束後,project-root/build/reports/profile/ 目錄下:

5右鍵點選profile_timestamp.html,選擇在瀏覽器中開啟,你就會看到下面這張圖,你可以觀察報告中的每一個tab來了解你的構建,比如Tasks Execution 顯示了每一個task執行的時間。

task Execution 顯示每個task的執行時長,如下圖:

6, 可選項:在Project 或者構建配置做出任何修改之前,重複幾次步驟3,但是去掉--rerun-tasks標誌,由於Gradle 試圖節省時間而不會重新執行那些輸入沒有任何修改的task(它們被標誌為UP-TO-DATE 在Task Execution tab下,如下圖:),你可以識別哪些任務沒有被執行,例如,如果:app:processDevUniversalDebugManifest沒有被標記成UP-TO-DATE,那麼它表示你的構建配置是每一次構建都動態更新Manifest檔案的。然而,有一些task還是需要每次都執行的,例如::app:checkDevDebugManifest

現在,你已經有了一個構建分析報告,你可以開始通過觀察構建報告的每一個tab來尋找優化時機,一些構建配置是需要試驗的,因為在不同的專案或者工作空間中它們的獲益不一樣。比如,基於大量程式碼的大型工程,它們可能獲益於使用混淆、清除無用程式碼和縮減APK體積。然而,小型工程可能獲益於關閉混淆(混淆還是挺耗時的)。此外,在第記憶體的機器上增減Gradle 的堆大小,也有可能起到反面作用。

17 . 專案元件化

對於大型的專案,可能上面這些優化建議有一定的效果,但是構建速度還是有些慢,那麼就可以考慮組建化了,將專案拆分成一個個單獨的元件,開發環境每個module 都是一個APK,釋出的時候,每個module都是一個lib 給主工程使用。篇幅有效,這裡就不再詳細介紹元件化,現在元件化是一個趨勢,如果有精力或者有實力,元件化是一個很不錯的選擇。

最後

以上就是一些解決app構建速度慢的優化建議,如果你覺得你的工程構建速度慢,你可以試一下這些優化項。如有問題,歡迎評論區留言。如果你還有什麼更好優化建議,也可以在下面留言,我會追加文章後面。

相關文章