前言
當前,大多數 Android 工程都是基於 Gradle 工具進行構建和編譯的,一開始,當你的工程不夠複雜,或者還只是小型專案的情況下,基本都不需要去關心構建優化的事情,而隨著業務變得複雜、程式碼量的增多以及越來越多的依賴,原有的 單 module 工程變成了多 module 工程,構建時間變得也越來越多。
說到這裡,有的同學可能會有疑惑,對於大專案來說,這麼多模組和依賴,本來就需要更多的編譯時間,還怎麼減少構建時間?恰恰相反,實際上越大的專案越能省出來時間。
為了讓開發者引起對構建分析的重視,Gradle 官方在最近的版本更新中推出了一個神器 build scan
,視覺化的深入分析和診斷所有構建相關的資料,並基於此分析結果幫助開發者找出構建問題以及針對構建效能進行優化。
背景
零售通買家端 App 是面向小店的一站式採購平臺,可以快速幫助小店老闆通過手機完成進貨等功能。隨著業務的不斷髮展,我們的工程規模和程式碼量也得到了極大的發展,目前我們的客戶端工程裡的 module 數量達到了40多個,而涉及到的相關依賴庫則有 200 多個,已然成為一箇中大型專案,可以想象每次編譯的時候,我和我的小夥伴們是多麼痛苦。
問題
-
編譯速度慢到懷疑人生,一般,我們的專案正常編譯時間大概在每次 3 分多鐘,如果中間連續編譯好幾次,時間也會相應成倍增加,同時,電腦 cpu 運轉的聲音也會越來越響,會有種錯覺是在小霸王學習機上進行開發,印象裡最深的有一次編譯花了十幾分鍾時間。當然,後面變聰明瞭,只要聽到小霸王學習機的聲音,就意味著可以重啟電腦了。
筆者電腦配置:公司標配 MacBook Pro,16G 記憶體,i7 處理器
-
莫名其妙的編譯問題,無法通過 IDE 的
Run
按鈕執行,必須要執行命令列編譯./gradlew clean assembleDebug
,本來時間就很慢了,加上clean
命令後,更是雪上加霜。 -
module 找不到間接依賴庫的類,中間有一次升級 Android Studio 版本後,工程裡的 module 裡的間接依賴庫裡的類都找不到了,直接飆紅了。不過,並不影響正常編譯和執行。原因是 *.iml 檔案裡的沒有自動生成間接依賴的 library 導致的。應該是 Android Studio 和 Gradle 低版本相容問題導致,後面升級到最新版本後解決。
以上編譯相關的問題,幾乎每天都在消耗著我和我的小夥伴的寶貴時間,也嚴重影響了日常的開發效率,所以,當看到多個小夥伴在長時間編譯後受不了而投來的幽幽眼神後,我想,是時候開始解決這些問題了。
解決
1. 升級 Gradle 版本
在開始任何分析優化工作前,第一件事情是升級你的 Gradle 版本,這也是最簡單見效的方法,新的版本,通常意味更好的效能和特性,這也是一條來自 Gradle 官方的優化建議
A guide on performance tuning would normally start with profiling and something about premature optimisation being the root of all evil. Profiling is definitely important and the guide discusses it later, but there are some things you can do that will impact all your builds for the better at the flick of a switch. Use latest Gradle and JVM versions, The Gradle team works continuously on improving the performance of different aspects of Gradle builds.
但是,對於我們的工程來說,升級 Gradle 版本並不是一件輕鬆的事情,我們有專門的內部打包平臺,並引入了它的打包外掛,而內部打包外掛強依賴了低版本的 Gradle 。雖然,內部打包外掛也提供了基於新版本的外掛,但是,嘗試升級後產生了一些外掛相容問題,而且,後續我們也無法跟隨官方的 Gradle 版本進行升級,考慮到通常只有在發版本或者整合測試的時候才會使用內部平臺打包。於是,我們採用了另一種方式,通過指令碼的控制,讓本地日常開發和編譯使用新版本的 Gradle,內部平臺打包依然走老版本的 Gradle 打包。
在升級 Gradle 和 Android Gradle Plugin 版本後,所帶來的編譯時間的提升非常明顯。所以,如果你使用的 Gradle 版本越低,那麼升級新版本後,所帶來的提升也越明顯。
2. 優化和減少 module
我們回顧下 Gradle 的構建生命週期:
- 初始化階段: 在初始化階段,支援單個或者多專案構建,它決定哪些專案模組要參與構建,併為每個專案模組建立一個工程例項。
- 配置階段: 在這一階段,通常是配置每個專案模組。 並執行所有專案模組的構建中的一部分指令碼。
- 任務執行階段: 在初始化和配置階段執行完成後,Gradle 開始執行每個參與的任務。
Gradle 提供了一個 --profile
命令,來幫助我們瞭解在每個階段所花費的時間,並生成一個報告。比如,你可以執行以下命令來獲取到一份報告,位於 rootProject/build/report/profile/***.html
./gradlew assembleDebug --profile
複製程式碼
上圖是在我們專案執行的命令後生成的報告,總構建時間花了 3分多種。
- Summary:構建時間概要
- Configuration:配置階段花費的時間
- Dependency Resolution:依賴解析花費的時間
- Task Execution:每個任務執行的時間,也是耗時最多的階段
Tips:Summary 概要裡的 Task Execution 時間是每個模組累計相加,實際上多模組的任務是並行執行的。
Task Execution 裡是每個模組編譯所花費的具體時間。同時,也可以看出編譯一個 module 的成本還是比較大的,因為有很多 task 需要執行。所以,接下來的工作就是減少和優化這些的模組。
在重新梳理了下我們的專案模組後,發現有一部分 module 裡面其實就只有兩三個類,完全沒有必要單獨 module,可以轉移到 app 或者 common-business 模組裡,而有一些 module 裡的核心邏輯已經抽成了獨立 aar 引用,針對殘留的程式碼邏輯,則進行了優化。在完成這些工作後,我們的工程模組數量由 40 多個減少到了 20 多個,是的,我幹掉了將近一半的 module。
建議:能不建 module,就不要新建 module,如果確實需要,可以使用 aar 方式代替,aar 編譯是有快取的話,理論上應該比 module 要快的,具體我沒有對比過。有興趣的同學,可以實際對比下試試
3. 一些配置優化
-
增加 snapshot 快取策略開關,有時候,為了 snapshot 版本的變動可以實時生效,會加上配置
cacheChangingModulesFor 0, 'seconds'
,但是,這樣就會在每次編譯都要去雲端比對是否有變動。所以,你可以通過在local.properties
增加開關來控制,在不需要的時候,關閉它。configurations.all { resolutionStrategy { if (rootProject.ext.cacheChangingModulesForDisable == false) { cacheChangingModulesFor 0, 'seconds' //針對 dynamic (例如2.+)的配置同理 //cacheDynamicVersionsFor 0, 'seconds' } } } 複製程式碼
-
避免編譯不必要的資源,比如不必要的語言本地化
android { ... productFlavors { dev { ... // 只編譯以下語言和解析度的資源 resConfigs "zh", 'zh-rCN', "xxhdpi" } ... } } 複製程式碼
-
不同的 Gradle 版本,一些配置優化也會有區別,更多配置優化可以參考 Gradle 提速:每天為你省下一杯喝咖啡的時間
4. 其他優化
- 指令碼邏輯優化,如避免使用一些網路請求或者 IO 操作。
- 去除重複和不必要的依賴
結果
好了,經過以上種種努力,終於到了收穫的時刻了。因為 Gradle 每次編譯的時間誤差還是比較大的,為了儘可能保證比對結果的客觀性,我們使用以下命令分別在優化前和優化後進行了 3 次編譯:
gradlew --profile --recompile-scripts --offline --rerun-tasks assembleDebug
複製程式碼
--recompile-scripts
: 繞過快取,強制重新編譯指令碼--rerun-tasks
: 強制重新執行所有 task,忽略任何優化offline
: 離線模式編譯
構建次數 | 優化前 | 優化後 | 減少率 |
---|---|---|---|
第一次 | 3分20秒 | 1分59秒 | - 40% |
第二次 | 2分50秒 | 1分7秒 | - 60% |
第三次 | 2分40秒 | 54秒 | - 67% |
以上資料可以看到,在優化後的工程裡,編譯次數越多,編譯時間越少,實際所提升的速度也遠超過 50%。
而且,升級版本後,終於可以通過 Run
按鈕編譯了,最快的一次增量編譯時間只有 9s !
總結
評論中 明朗同學 理解和總結的很到位,我就直接貼上來了,感謝~
- 升級到最近的gradle 版本 並採用最新語法 比如implementation 代替compile等
- 程式碼層面的優化 比如減少module 使用 aar 依賴
- 其他的一些優化配置等
感想
雖然我們的編譯時間減少到了 1 分多鐘,但仍然有較大優化空間,比如為了相容老的 Gradle 版本,目前我們還沒有辦法使用 implementation
替換 compile
,這樣,就可以有效的減少編譯時的依賴項,另外,後續也會藉助於 build scan
去更深入分析和了解我們的構建資訊。
如果你所在的專案,構建編譯時間成了一種負擔,那麼,很有必要引起你的重視。也許,每個專案的實際情況可能都不一樣,但是,希望本文可以為你提供一些思路,我相信,隨著你的深入分析之後,一定會有更好的辦法去解決,另外,重要的是,很少有這樣的效能優化,可以在短時間內帶來實際的提升效果,所以,現在,立刻開始你的構建優化吧。
如果,對構建優化有什麼問題或者不瞭解的地方,歡迎留言討論。
參考
最後
一條簡短的廣告
零售通是阿里巴巴五新戰略之一,也是探索新零售模式的一隻創業大軍,在業務快速發展的同時,我們所面臨的技術挑戰也越來越高!服務每家店,只為每個家是我們的使命,為了更好的服務我們的使命,我們需要大量的客戶端(Android & iOS)、前端、後端同學,期待您和我們一起戰鬥!
簡歷請投:yonglan.whl@alibaba-inc.com
雙十一來臨之際,奉上團隊風采照一張
PS:本廣告長期有效