1. 程式人生 > >Android Studio編譯慢、卡死和狂佔記憶體

Android Studio編譯慢、卡死和狂佔記憶體

至於加快編譯速度,有一句說一句,我覺著一些答主的答案適用性都並不強,其實還是應該從 gradle 入手,講的有什麼不合適的地方,還請輕噴,有什麼問題也可以留言。

以下我講到的所有步驟,推薦都在終端裡執行。在終端裡執行編譯有很多好處:
  1. 可以觀察到整個編譯過程,有助於理解 gradle 構建流程;
  2. 可以看到編譯過程中哪些任務比較耗時,可以對編譯慢的問題對症下藥;
  3. 可以隨時終止編譯,如果被卡在某個階段,ctrl + c 可以隨時終止編譯,在 Android Studio 裡也終止編譯,但是基本上十次有九次都會失敗;
  4. 因為是在終端裡,對 Android Studio 影響極小,基本不會造成 Android Studio 卡頓;
  5. 不會遇到 Android Studio 的各種 bug 。

先說一下 gradle 的生命週期吧,gradle 構建一個工程主要分為三部分(完全掌握了下面這張圖,整個 gradle 的構建過程能瞭解個十之七八了):
<img src="https://pic2.zhimg.com/50/f084c5f0953eb2792d929dd6738822b5_hd.png" data-rawwidth="1468" data-rawheight="786" class="origin_image zh-lightbox-thumb" width="1468" data-original="https://pic2.zhimg.com/f084c5f0953eb2792d929dd6738822b5_r.png">
  1. 初始化階段:主要是解析 setting.gradle 檔案(因此有人提到減少 setting.gradle 的 module 數量,是很有道理的,但是實際操作過程限制頗多,原因最後會大致說一下);
  2. 讀取配置階段:主要是解析所有的 projects 下的 build.gradle 檔案,包括 rootProject 和其他的 subprojects(子專案),檢查語法,確定 tasks 依賴以建立 task 的有向無迴圈圖,檢查 task 裡引用的檔案目錄是否存在等(這一步也進一步驗證了減少 setting.gradle 裡的 module 數量可以加快編譯速度,因為減少一個 module ,需要解析的 build.gradle 檔案就減少一個,第 3 步裡就不會執行本屬於這個 module 的任務了,但是還是 1 裡面說的問題,限制頗多);
  3. 執行階段:按照 2 中建立的有向無迴圈圖來執行每一個 task ,整個編譯過程中,這一步基本會佔去 9 成以上的時間,尤其是對於 Android 專案來講,將 java 轉為 class
    compileDebugJavaWithJavac/compileReleaseJavaWithJavac
    
    和 將 class 合併成 dex
    transformClassesWithDexForDebug/transformClassesWithDexForRelease
    
    這兩步很耗時,第一步還好,第二步會耗時非常久。首先在 gradle.properties 裡設定
    org.gradle.jvmargs=-Xmx4096m //越大越好
    
    ,然後在工程的 build.gradle 裡的 android 結點下增加 dexOptions 配置,如下:
dexOptions {
    dexInProcess true
    preDexLibraries true
    javaMaxHeapSize "4g"//越大越好
    incremental true
}

明確了 gradle 的生命週期,那麼就可以看到加快編譯速度的關鍵就是從第三步入手,當然,減少 setting.gradle 裡的 modules 數量這一步也是必須的。下面說說我們公司的實踐吧。
  1. 專案外掛化改造,每位業務上的同學只需要編譯一個模組即可,這一點基本上從根本上解決了編譯慢的問題(對於大多數沒有外掛化需求的朋友們可以看下面的一些實踐),首先 setting.gradle 裡的 module 只有自己開發的模組了,而對應的執行階段的任務也只有這一個 module 的任務了。
  2. 執行一次 gradle build ,我們就會發現,在這個過程中,其實是執行了多次打包任務的,在 buildTypes 裡配置了多個編譯打包型別,預設有 debug 和 release ,我們還可以手動配置其他的型別,而且還有 productFlavor 裡的多渠道,這樣就會執行多次編譯打包,而正常開發過程中,只需要打 debug 包去除錯,因此使用 gradle assembleDebug 即可,等發版的時候使用其他方式去打多渠道的包(如美團的方案tech.meituan.com/mt-apk);
  3. 既然編譯主要時間都集中在 gradle 生命週期的第三步執行 task 任務裡,那麼我們就可以把一些無關緊要的任務給禁用掉,比如各種 Test ,各種 lint 等,剛好在 gradle 裡有這樣的指令 -x lint 可以臨時禁掉 lint 任務,-x test 可以禁掉 test 任務,事實上對於一個稍微大一點的專案,lint 也是很耗時的,當然也可以通過 gradle 指令碼徹底禁用 lint 和 test 任務,我也在一些微信群裡分享過相關程式碼,但是不太建議這麼做,因為有時候 lint 和 test 也是挺有用的;
  4. gradle 本身提供了一些指令引數可以加快編譯,比如 --daemon ,開啟守護程序,--parallel ,開啟並行編譯等,這個也可以在 gradle.propertites 裡配置(編譯使用的 jvm 記憶體也可以在這裡配置)。
上面講到的幾點,現有環境就可以做到的大概是這樣(有一點要特別注意,如果工程裡有交叉依賴,一定不要使用 --parallel 引數):
gradle assembleDebug --daemon --parallel -x lint -x test

,如果是要直接安裝到裝置上的話,就把 assembleDebug 換成 installDebug ,assembleDebug 可以簡寫為 asD ,installDebug 可以簡寫為 iD 。

最後講一下,為什麼減少 setting.gradle 裡的 module 數量,確實可以加快編譯,但是卻限制頗多呢?
首先,我們想一下整個編譯過程,先去解析 gradle 配置,建立 tasks 依賴有向圖,然後再去執行每一個 module 的 task ,如果我們通過 maven 依賴,使用 aar 替掉了 module(單指 android library),如果我們要改這個 module 裡的檔案,豈不是每次都要修改上傳再下載,這其實還好,但是有一個致命的問題:不修改版本號的話,SNAPSHOT 在 IDEA 裡經常會不好使這樣就導致修改的東西會不生效,去解決這個問題是非常耗費時間的。不過有一種方式,可以一定程度上解決問題,增加下面的指令碼:
project.configurations.all(new Action<Configuration>() {
@Override
    void execute(Configuration files) {
        files.resolutionStrategy.cacheDynamicVersionsFor(5, TimeUnit.MINUTES)
        files.resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS)
    }
})
那有人會問,外掛化裡,每個人開發一個模組,對於每個模組的維護不也是要打包上傳到 maven ,每次一有修改,哪怕是非常微小的修改,也要做一次上傳,同樣會遇到 SNAPSHOT 不好使的問題。嘿嘿,這個問題嘛,我司自己維護了一個 gradle 外掛,已經解決了,至於解決方案,是公司機密,我是不會講的。
然後,還有一點,我相信大部分開發者平常開發都是單 module 的,多 module 的情況並不多,因此大多數依賴基本也都是 aar 或者 jar ,根本就不存在所謂的將 library 轉成 aar 上傳的情況,因此一些答主說的根本毫無意義,這也是為什麼我會說影響編譯速度的情況主要集中在 gradle 生命週期的第三個階段,至於第三個階段的優化,看我上面的答案就好了。