編譯提速最佳實踐

天之界線2010發表於2019-03-04

本文會不定期更新,推薦watch下專案。如果喜歡請star,如果覺得有紕漏請提交issue,如果你有更好的點子可以提交pull request。本文意在分享作者在實踐中對於打包和build的提速方案,如果你有更好的點子可以在下方進行評論補充。

本文固定連線:github.com/tianzhijiex…

需求

讓打包變得更快一點,再快一點!
這個需求簡單明瞭,就是要更快的打包,減少時間上的浪費。

實現

分析目前現狀

build-time-tracke

build-time-tracke是可以檢測出build耗時的gradle外掛,會形成十分美觀的圖表,可以方便的確定當前的編譯時間和主要耗時的task。

編譯提速最佳實踐

gradlew build -profile

gradlew build -profile命令會生成報表,gradlew build assembleDebug -profile會產生debug模式下的報表,報表位置在工程的根目錄下的build目錄中。

編譯提速最佳實踐

編譯提速最佳實踐

dexcount

dexcount也是一個相當不錯的統計工具,也是一個gradle外掛。
配置後執行build會產生目前的方法數的輸出,也會輸出一個圖表。

> ./gradlew assembleDebug

...buildspam...
:app:compileDebugSources
:app:preDexDebug UP-TO-DATE
:app:dexDebug
:app:packageDebug
:app:zipalignDebug
:app:assembleDebug
Total methods in MyApp-debug-5.3.14.apk: 56538

BUILD SUCCESSFUL

Total time: 33.017 secs複製程式碼

編譯提速最佳實踐

利用這個圖表我們可以方便的分析出不同庫的不同方法,對於檢視複雜庫和精簡方法有著相當重要的幫助。

精簡工程

刪除不必要的module

AS的程式碼結構和eclipse完全不同,它為開發者提供了單工程多module的形式。但多建立一個module就需要多維護一個module,而且依賴的module越多,編輯時間越久,甚至有人說是依賴aar或jar的四倍時間。
如果僅僅是為了方便寫程式碼而建立一個module是不可取的,我強烈建議先做好專案結構的梳理再考慮是否需要建立module。

下面是一個多module的app結構圖:

編譯提速最佳實踐
framework

在as中通過自帶的預覽工具,也可以幫助我們進行modules的梳理:

編譯提速最佳實踐
module

這個專案中的module有很多,所以gradle在編譯的時候會去檢測module的依賴鏈,gradle會幫助我們層層梳理module之間的關係,避免因為module之間相互引用而來帶的問題。這些梳理工作和module的合併工作都會增加build的時間。
如果你的專案build十分緩慢,我強烈建議你去梳理下module的關係,合併部分module,將穩定的底層module打包為aar,上傳到公司的maven倉庫,藉此來加快build速度。

刪除module中的無用檔案

as預設在建立module的同時會建立test目錄:

編譯提速最佳實踐
test

如果你根本沒有編寫過測試用例,你完全可以刪除test目錄,到要寫的時候再加上就好。
當然,如果你的module就是純程式碼,根本沒用到資原始檔,也請一併把res目錄刪除掉。

編譯提速最佳實踐
res

刪除主專案中無用的資原始檔

專案開發中多少都會存留一些無用的程式碼和資源,資源越多打包合併資源的時間就越長。對於資原始檔,as提供了自動檢測失效檔案和刪除的功能,絕對值得一試。

編譯提速最佳實踐
remove res

在彈出的對話方塊中,我強烈建議不要勾選刪除無用的id,因為databinding會用到一些id,但這在程式碼中是沒有直接依賴的,因此as會認為這些是無用id,會直接刪除。如果你刪除了這些id,那麼就等著編譯失敗吧。(別問我是怎麼知道的T_T)。順便說一下,每次做這種操作前記得commit一下,方便做diff。

編譯提速最佳實踐
dialog

優化依賴庫

利用no-op加快debug的速度

如果專案中有很多公司自己的module依賴,那麼你完全可以採用類似於這篇文章提到的技巧,給私有的module做no-op(什麼是no-op可以看這篇的例子)。

一般私有的module會比較穩定,並且對外暴露的方法不多,甚至會是別的專案組開發的。在不影響功能的前提下,建議和開發團隊商量提供no-op版本。

debugCompile(project(':share-lib-no-op')) {}
releaseCompile(project(':share-lib')) {}
debugCompile(project(':zxing-no-op')) {}
releaseCompile(project(':zxing')) {}複製程式碼

用no-op版本的好處就是隻使用介面而不使用實現,將實現的程式碼全部剔除,如果做的好的話甚至可以在debug時不用multidex,但壞處就是需要進行協作交流。如果module對外的介面變動了,還應該考慮到對no-op版本的影響。

減少方法數,不使用multidex

關於什麼是multidex,和怎麼使用它,請參考《使用android-support-multidex解決Dex超出方法數的限制問題》

它是一種不得已而為之的舉措,使用它後我經常會發現在一些特殊的機型上會出現一些奇奇怪怪的錯誤,總之就是有很多坑。

在build時間這一塊,multidex因為有分包和壓縮的過程,所以它對於編譯速度方面有有嚴重的影響。我通過dexcount這個外掛分析了我的專案後,發現專案中有一些庫已經不再用或者有更好的替代品,於是我精簡和替換了第三方庫,並且開啟了support包的混淆,最終讓我們的專案的release包的方法數達到了一個合理的水平。

編譯提速最佳實踐
優化前

編譯提速最佳實踐
精簡庫,開啟support包的混淆後

為了控制變數,我專門用一個空專案進行support包混淆前後的對比。資料如下:

編譯提速最佳實踐
混淆前

編譯提速最佳實踐
混淆後

當一個第三方sdk說不要混淆support包,不要混淆我sdk的程式碼的時候,我強烈建議你考慮下方法數的問題。混淆的作用之一是將程式碼進行優化和縮短方法名、欄位名;作用之二就是刪除沒有被用到的變數和方法。第三方sdk的方法數眾多,如果沒辦法混淆,那麼會帶來大量的方法數,這點需要十分的小心。混淆雖然是一個十分有用的工具,但也是很多錯誤的來源,所以我建議你小心謹慎的多多使用它!

根據使用場景依賴不同庫

上面講到了優化第三方庫會減少方法數,這裡簡單講一下一般的優化策略:

  1. 利用debugCompile來依賴debug時才用到的庫
    debugCompile我在第三方庫開發實踐中已經講到多次了,這裡就不再贅述。

  2. 利用更小的庫替代現有的庫
    這個就要看開發人員的經驗和知識面和判斷能力了。雖然是廢話,如果能真正做到,成果是極其明顯的。

  3. 利用exclude來排出某些不需要的依賴
    react native是一個龐大的庫,引入rn後會依賴很多別的庫:

編譯提速最佳實踐
rn

在我們的專案中,我利用了自己編寫的網路請求模組進行網路請求,所以我就想要剔除掉rn引入的okhttp。我還發現它引入了support包,而我專案中也有support包,所以我也想要排出掉它(不排除support包也沒事,gradle會僅包含最新的庫版本,我這裡僅僅是舉個例子)。

  compile ('com.facebook.react:react-native:+'){
    exclude group: 'com.squareup.okhttp3', module: 'okhttp'
    exclude group: 'com.android.support', module: 'support-v4'
    exclude group: 'com.android.support', module: 'support-v7'
  }複製程式碼

重新build一次後,你會發現okhttp已經被剔除掉了:

編譯提速最佳實踐
exclude okhttp

對於本地的module也是可以這樣處理的:

compile(project(':react-native-custom-module')) {
    exclude group: 'com.facebook.react', module: 'react-native'
}複製程式碼

利用快取

開啟offline

這個是最簡單直接的加速方案了,效果極其明顯,誰用誰知道!

編譯提速最佳實踐
offline

編譯提速最佳實踐

用公司的倉庫做快取

我推薦的做法是專案中所有的依賴(私有或第三方)都通過公司的倉庫進行獲取。公司的倉庫應該能自己查詢jcenter等倉庫,下載好需要的依賴,並進行快取。這樣的好處是:當一個同事引入了新庫或者更新庫版本後,別的同事在build時可以直接拿快取好的庫,大大減少了下載依賴的時間。這點雖然是小優化,但是對於新人和團隊協作來說是相當重要的。

跳過無用的耗時操作

debug時跳過某些task

我們的專案中用到了很多gradle外掛,有些外掛會在build時執行自己的task:

編譯提速最佳實踐
gradle plugin

  • tiny是用來壓縮圖片的
  • buildtime是用來檢測build時間的
  • dexcount是用來分析方法數的

這些外掛對於我們的開發工作帶來了巨大的幫助,但也增加了build時間。

我分享下我的做法:

  1. 在每次發版本前開啟tiny,直接build一次,壓縮完圖片後將其關閉。
  2. 在需要檢測和診斷build時間的時候啟用buildtime,一般的debug時不開啟它。
  3. 在release包中開啟dexcount,並且讓其於Jenkins進行結合。這樣既不會影響debug包,又可以進行方法數的持續監控。

關於dexcount是如何和Jenkins結合的,並且是如何產生下面的圖表的,請參考:
www.th7.cn/Program/And…

編譯提速最佳實踐
dexcount

關於如何在不同的情況下跳過或啟用task的知識,可以參考Gradle配置實踐中的內容。

跳過lint

通過gradlew build -profile我們可以得到build的詳細報告,最終報告會生成在根目錄的build/reports/profile中:

編譯提速最佳實踐

編譯提速最佳實踐

我們可以看到lint的時間佔到了build時間的80%之多,所以如果你不在意lint的結果的話,你可以在build時跳過lint檢測,比如:

gradle build -x lint -x lintVitalRelease

因為我用命令列build的次數較少,所以我直接在gradle中加了一個跳過lint的task:

tasks.whenTaskAdded { task ->
    if (task.name.equals("lint") || task.name.equals("lintVitalRelease")) {
        task.enabled = false
    }
}複製程式碼

放棄retrolambda,謹慎使用AspectJ

目前android不支援lambda,所以很多人都引入了retrolambda。一旦你引入了這個庫,你就必須面臨著位元組碼轉換而帶來的build慢的問題。它需要在build時執行一個插入程式碼的task,這個task的執行時間隨著你用的越多而會越來越長。所以,我不推薦在目前的階段使用它,還是等等看看谷歌jack的表現吧(jack目前還是太初級,對庫和增量編譯的支援很差)。

AspectJ是aop的工具,但因為需要在build時進行程式碼的插入,所以使用AspectJ後build時間會明顯的增加,具體看使用量而定。

AspectJ的優缺點十分明顯,我這裡只是提出來,具體如何權衡,就看大家自己了。我因為用了UiBlock所以引入了AspectJ,它讓我debug是build的速度慢了三秒鐘,但UiBlock的好處也十分明顯,所以我還是用了這個庫。

優化配置方案

在dev環境中設定minSdkVersion為21

因為在debug時,我們不會去開啟混淆,所以debug包常常是需要用mulitdex的。

編譯提速最佳實踐
debugApplication

android5.0對於mulitdex做了優化,具體可以參考官方的文章,我就直接說怎麼做就好。先在gradle的配置中新增一個flavors,比如叫做dev,在dev中配置最低支援的android版本為21.

編譯提速最佳實踐
gradle

然後在build時選中devDebug,這樣你debug的時候就是走最低支援21的編譯方式了。

編譯提速最佳實踐
build

特別注意:
現在我們為了提速將最低版本寫為21,假設你最終可能支援的是16,這就有個風險點了。因為as會在你寫程式碼的時候認為你的應用就是支援21的,所以對於一些16~21的api不會有版本風險提示的。因此使用16~21之間的api時需要人為的注意,這是最大的風險!!!

升級jdk和gradle

最新的Gradle要比老版本要快,jdk1.8比jdk1.6要快,所以可以跟著官方升級新版本就好。

The same argument goes for Java versions as well. If you haven’t upgraded yet to Java 8, do it now! Well finish reading this blog post, but do it straight afterwards! You don’t even have to move your project to use Java 8, lambdas and so on. Just make sure your build tool executes with the latest and the most performant Java version out there.

調整gradle的編譯引數

gradle.properties中允許我們進行各種配置:

配置大記憶體:

org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8複製程式碼

守護程式

org.gradle.daemon=true複製程式碼

並行編譯

org.gradle.parallel=true複製程式碼

開啟快取:

android.enableBuildCache=true複製程式碼

開啟孵化模式:

org.gradle.configureondemand=true複製程式碼

以上的配置需要針對自身進行選擇,隨意配置大記憶體可能會出現oom。如果想了解這樣配置的原理,請移步官方文件

我自己的配置如下:

org.gradle.daemon=true
org.gradle.parallel=true
android.enableBuildCache=true
org.gradle.configureondemand=true
org.gradle.jvmargs=-Xmx3072m -XX:MaxPermSize=1024m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8複製程式碼

總的來說,除了增加記憶體這一項感覺還有點用處外,其餘配置都不痛不癢。我最後直接加了16g記憶體,解決了大多數的問題。

配置大記憶體

android {
    dexOptions {
        // 使用增量模式構建
        incremental true
        // 最大堆記憶體
        javaMaxHeapSize "4g"
        // 是否支援大工程模式
        jumboMode = true
        // 預編譯
        preDexLibraries = true
        // 執行緒數
        threadCount = 8
        // 程式數
        maxProcessCount 4
    }
}複製程式碼

在app的build.gradle中配置大記憶體也可以有效的提升build速度。

上面有些配置方案在最新的gradle中已經標記為無效了,可以適當的刪除掉。

優化crashlytics的upload

上面講到的都是build過程中的提速,但打包過程不僅僅包含了build,還包含了混淆,簽名等流程。如果你的專案用了crashlytics,crashlytics會在混淆時自動上傳map檔案到伺服器,這樣可以幫助你在分析崩潰的時候看到的是混淆前的程式碼和行數,十分方便。

萬事有利有弊,我們專案的map檔案為6m左右,crashlytics的伺服器又是在國外,所以每次都會需要很長的一段時間。

優化點主要是提升上行頻寬和網路速度,前者需要硬體的支援,後者可以通過vpn進行優化。在配置release包打包命令的時候,可以不用每次都把build目錄刪除,這在一定程度上也可規避此問題。

利用MultiChannelPackageTool進行多渠道打包

我們的應用可能會被分發到多個渠道,而我們又想進行多個渠道的資料分析,這就產生了目前android要打多個渠道包的現狀。這篇文章詳細的分析了國內最高效的打包方案,文章短小精幹,值得一讀。

我選擇的是MultiChannelPackageTool來進行打包,它的速度是最快的,而且使用方式十分的簡單。他的原理是在zip檔案的comment中加入渠道號,這樣既可以寫入渠道號又不會破壞zip的簽名,因為apk本身就是一個zip檔案,所以這個規則是可靠並完全通用的。

編譯提速最佳實踐
comment

具體的原理和實現方案也不難,這裡可以參考趙林寫的《一種動態為apk寫入資訊的方案》進行深入瞭解。

下面我給大家演示下實際的情況:

編譯提速最佳實踐
package

現在我們可以通過

MCPTool.getChannelId(context, "password", "")複製程式碼

得到渠道名稱。如果你用的是友盟來做監控和統計,那麼你肯定需要在程式碼中設定友盟的key和channel名。通過友盟的文件和論壇我發現友盟最新的sdk提供了這樣的配置機制,於是就有了如下程式碼:

// 設定key和渠道號,在application中就需要進行設定
UMAnalyticsConfig config = new UMAnalyticsConfig(context, appKey, channelId);
MobclickAgent.startWithConfigure(config);

// 得到key和渠道號
String appKey = AnalyticsConfig.getAppkey(activity);
String channel = AnalyticsConfig.getChannel(activity);複製程式碼

採用增量編譯

instant Run

編譯提速最佳實踐

Instant Run是谷歌的一套解決方案,相容性肯定是一流的,但是對於奔潰和除錯來說經常不盡人意,只能靜待其發展了。

jirebel

編譯提速最佳實踐
jirebel

as目前已經支援了增量編譯,但是效果真的很差,甚至經常會增加build時間,所以這裡我還是推薦一直在更新的Jrebel做增量編譯的工具。

我之前寫《Android中UI實時預覽實踐》的時候就有推薦過它,只不過那時候真的太貴了。現在as出了增量編譯,它也坐不住了,立刻降價,價錢還算是可以接受。它的優點是成熟穩定,各種配套服務支援的十分完善,但crash 後仍舊需要重新全量編譯,單次全量編譯、安裝的速度非常慢。一個更大的風險點在於每次gradle更新它都是必須要更新的,使用的時候經常要除錯這方面的問題。

如果你寫的是小型應用的話,效果會更好。現在它已經不用我們單獨配置maven倉庫了,完全和專案解耦,而且它竟然支援註解和aop,堪稱黑科技!所以,如果你有心想要加快打包的速度,我強烈推薦你去試用上21天,看看它是否值得你為之付費。

freeline

編譯提速最佳實踐

如果你想試試免費的工具,那麼freeline絕對是你的首選。
幾個月來,它發展迅速,也配備了as外掛gradle外掛,支援各種平臺。關於它的原理,可以參考《Freeline - Android平臺上的秒級編譯方案》進行了解。

freeline只支援python2.7版本,對於重新命名等操作支援力度不足,不支援 databinding,不支援刪除帶id 的資源。實際使用中還是會發現要經常clean後build,對於不同的flavor也是沒辦法動態切換,只能寫死:

freeline {
    hack true
    productFlavor 'dev'
}複製程式碼

總體來說freeline算是目前免費的增量編譯中的最優秀的作品了,值得一試。

關於各種增量工具的對比可以參考:Android 加速構建方案對比 - DiyCode

尾聲

本文的需求相當簡單明瞭,但實現起來卻都是八仙過海各顯神通,並沒有所謂的一站式解決方案。其實技術的走向也是如此,各個技術的湧現都是為了解決實際需求中的問題,脫離業務而出現的技術方案是走不遠的。早期android中app偏向簡單,對於打包時間並沒有過於在意。隨著移動化程式的發展,app過於複雜和龐大,打包提速變得十分重要,這也是纂寫本文的初衷。

提速的涉及面很廣,涉及到庫開發、方法數、電腦平臺、app瘦身等多個領域,也希望大家在閱讀完畢後能夠有所收穫,在以後遇到提速需求的時候可以有一些解決思路。

編譯提速最佳實踐
developer-kale@foxmail.com

相關文章