什么是配置緩存?
配置緩存是一個提升 IDE 和命令行構(gòu)建速度的基礎(chǔ)構(gòu)建塊。這是 Gradle 6.6 版本提供的一個高度實驗性功能矮瘟,它可以使構(gòu)建系統(tǒng)記錄一次任務(wù)的圖譜信息瞳脓,并在接下來的構(gòu)建中進(jìn)行復(fù)用,從而避免再一次配置整個工程澈侠。這一功能也是配置階段改進(jìn)的延續(xù)劫侧,這些改進(jìn)中引入了 惰性配置 (lazy configuration),以避免在構(gòu)建的配置階段進(jìn)行不必要的工作埋涧。這些改進(jìn)對于快速迭代開發(fā)的重要性不言自明板辽,而后者也是 Android Studio 團(tuán)隊所持續(xù)關(guān)注的一個用例。
性能改進(jìn)
這一功能的主要目標(biāo)便是提升構(gòu)建速度棘催。在 Android 版 Santa Tracker 工程的基準(zhǔn)化分析中劲弦,對于啟用了配置緩存的構(gòu)建過程,我們測量出其在 Android Studio 中的總構(gòu)建時間減少了 35% (從 688ms 到 443ms醇坝,測試平臺為 Linux邑跪,使用 Intel? Xeon? Gold 6154 CPU @ 3.00GHz )。下圖展示了使用和不使用配置緩存進(jìn)行 100 次構(gòu)建的平均總構(gòu)建時間 (以毫秒為單位):
對于一些工程呼猪,配置階段可能會消耗 10 秒鐘以上画畅,節(jié)省時間的效果也因此更加顯著。無論運行的是全新構(gòu)建宋距、增量構(gòu)建還是更新構(gòu)建轴踱,配置階段的開銷都是相同的。要衡量您的構(gòu)建過程中配置階段所消耗的時間谚赎,可以以空運行模式 (dry run mode) 運行任務(wù)淫僻,例如: ./gradlew :app:assembleDebug --dry-run
。
為了進(jìn)一步避免重復(fù)運行配置過程壶唤,配置緩存還允許來自同一工程的任務(wù)并行運行雳灵。以前,只有利用 Worker API 的任務(wù)可以同時運行闸盔,但是由于配置緩存可以確保任務(wù)獨立且無法訪問全局共享狀態(tài) (例如 Project 實例)悯辙,因此可以默認(rèn)啟用此行為。而且迎吵,依賴關(guān)系解析結(jié)果可以在運行間進(jìn)行緩存躲撰,從而有助于優(yōu)化整體構(gòu)建時間。
如何試用钓觉?
配置緩存功能現(xiàn)在還處于實驗階段茴肥,我們希望您可以嘗試它并向我們提供反饋。為了在您的構(gòu)建中使用它荡灾,需要保證所有工程所應(yīng)用的所有插件都是兼容的,這是為了安全地 (反) 序列化任務(wù)圖。您可能需要更新某些 Gradle 插件批幌。您可以通過此 issue 來獲取受支持插件的完整列表础锐,如果您使用的插件不在其中,請在它們的問題跟蹤器中提交問題荧缘,并從 Gradle 問題中鏈接至該 issue皆警。
最新版的 Android Gradle 插件版本為 4.1 (目前為 4.1.0-rc03),但如果您希望獲取所有的錯誤修復(fù)截粗,請嘗試最新的 4.2 版本 (目前為 4.2.0-alpha13)信姓。Gradle 的版本應(yīng)為 6.6,同時如果您正在使用 Kotlin绸罗,請將 Kotlin Gradle 插件更新為最新的 1.4 版 (相關(guān) Kotlin issue)意推。最后使用以下代碼更新 gradle.properties:
org.gradle.unsafe.configuration-cache=true
# 小心使用這一標(biāo)記,因為有些插件還沒有完全兼容
org.gradle.unsafe.configuration-cache-problems=warn
查看所有 Android Gradle 插件版本珊蟀,請參考如下頁面:
https://maven.google.com/web/index.html#com.android.tools.build:gradle
如果啟用了配置緩存菊值,您應(yīng)該可以在第一次運行時通過 Android Studio 的 Build 輸出窗口或命令行看到 "Calculating task graph as no configuration cache is available for tasks…" (由于當(dāng)前任務(wù)沒有可用配置,正在生成任務(wù)圖譜...) 字樣育灸;而在第二次運行中會復(fù)用配置緩存腻窒,所以輸出中會包含 "Reusing configuration cache. (復(fù)用配置緩存)"。
無論您遇到任何問題磅崭,都可以在 Android Studio issue 跟蹤 或 Gradle issue 跟蹤 中向我們反饋儿子。
它是如何工作的?
想要深入了解配置緩存砸喻,我們要從了解構(gòu)建的配置階段開始柔逼。就算您開啟了配置緩存,第一次構(gòu)建仍會經(jīng)歷這一過程恩够。在配置階段卒落,所有被包含的工程 (在評估 settings.gradle 時獲取) 都會依據(jù)其構(gòu)建文件的評估結(jié)果進(jìn)行配置。通常首先會應(yīng)用所有插件蜂桶,同時 DSL 對象會被實例化儡毕;接下來會繼續(xù)評估構(gòu)建文件,而 DSL 對象將會被分配您所指定的值扑媚。當(dāng)構(gòu)建文件的評估完成時腰湾,會調(diào)用 Android Gradle 插件 (以及許多遵循相同模式的其他插件) 的 Project.afterEvaluate 回調(diào)。在此回調(diào)的調(diào)用期間疆股,Android Gradle 插件會完成其絕大部分的工作费坊,包括創(chuàng)建變體以及注冊任務(wù)。
在評估 DSL 以及注冊任務(wù)之后旬痹,接下來的階段會構(gòu)建一個任務(wù)圖附井。您所要求執(zhí)行的任務(wù)以及它們所依賴的任務(wù)都會被完全配置讨越。這一過程將會持續(xù)到觸達(dá)沒有依賴的葉子任務(wù)為止。配置的這一階段將會輸出一個任務(wù)圖永毅,Gradle 中的調(diào)度機(jī)制會使用該任務(wù)圖來運行構(gòu)建操作把跨。當(dāng)任務(wù)圖被完成后,配置緩存會將其存儲在磁盤中 (在 Gradle 6.6 中位于根工程的 .gradle/configuration-cache directory 目錄下) 沼死。它可以序列化所有的 Gradle-managed 類型 (如 FileCollection着逐、Property、Provider) 以及所有用戶定義的可序列化類型意蛀。在此階段結(jié)束時耸别,每個任務(wù)的狀態(tài)都將被完全記錄并保留下來。
在第二次構(gòu)建時县钥,假設(shè) Gradle 能夠復(fù)用記錄的緩存秀姐,則會加載所請求任務(wù)的任務(wù)圖、跳過 DSL 評估魁蒜,任務(wù)配置等囊扳。這意味著所有任務(wù)都將被實例化,而它們的所有屬性都將從緩存中加載兜看。從這一時刻起锥咸,構(gòu)建過程基本與無緩存構(gòu)建無異,區(qū)別只是默認(rèn)情況下可以并行運行任務(wù)以及復(fù)用緩存中的依賴項解析結(jié)果的優(yōu)勢细移。
為了保證正確性搏予,Gradle 會持續(xù)跟蹤會影響已緩存的任務(wù)圖的所有輸入,包括構(gòu)建文件弧轧、請求執(zhí)行的任務(wù)以及配置過程中對于 Gradle 和系統(tǒng)屬性的的訪問雪侥。請求運行一組不同的任務(wù)會產(chǎn)生一個不同的任務(wù)圖,所以需要創(chuàng)建一個新的緩存記錄精绎。一個需要使?fàn)顟B(tài)失效的例子是: 您修改了 build 文件或 buildSrc速缨,并向環(huán)境變量或系統(tǒng)屬性傳遞了一個不同的值。為了檢測這類變更代乃,構(gòu)建系統(tǒng)會創(chuàng)建一個緩存任務(wù)圖時所使用的 build 文件的快照旬牲;此外,它還會檢測 buildSrc 中是否有未更新的任務(wù)搁吓。最后原茅,任何會影響配置階段的值都應(yīng)當(dāng)被包裝為 Gradle-managed 類型,這有助于構(gòu)建系統(tǒng)對配置階段中所使用的變量進(jìn)行持續(xù)跟蹤堕仔。
使用兼容的 Gradle API
構(gòu)建中應(yīng)用的所有 Gradle 插件都必須與配置緩存兼容擂橘,Gradle 也因此引入了一組新的 API。下面是我們對于配置緩存和新 API 所帶來的約束進(jìn)行的考察:
在任務(wù)中使用 Project 實例
Gradle 插件中最常見的兼容性問題來自于在任務(wù)操作中使用 Task.getProject()摩骨。在使用配置緩存時通贞,為了保持每個任務(wù)完全獨立朗若,任務(wù)將無法訪問這一共享狀態(tài)。由于 Project 實例可以訪問 TaskContainer滑频、ConfigurationContainer 以及其他在啟用緩存的運行期間不會填充的對象捡偏,從而導(dǎo)致反映出無效的狀態(tài)唤冈,所以禁用它是必須的峡迷。引入了很多可替代的 API,比如用于延遲對象創(chuàng)建的 ObjectFactory你虹,還有可以用于獲取項目文件系統(tǒng)分布情況的接口绘搞,比如 ProjectLayout,如果需要在構(gòu)建中啟動進(jìn)程傅物,可以使用 ExecOperations夯辖。您可以參考 完整的 API 列表 來進(jìn)行遷移工作。
訪問 Gradle/系統(tǒng) 屬性與環(huán)境變量
如果您使用系統(tǒng)屬性董饰、Gradle 屬性蒿褂、環(huán)境變量或者額外文件來指定構(gòu)建的邏輯輸入時,會產(chǎn)生怎樣的結(jié)果崭歧?構(gòu)建系統(tǒng)已經(jīng)在跟蹤 build 文件的修改喝检,但是任何影響任務(wù)圖的額外值都應(yīng)當(dāng)使用 ProviderFactory API 進(jìn)行獲取咳燕。下面的示例展示了如何獲取影響配置的 enableTask 系統(tǒng)屬性值,以及如何獲取僅作為任務(wù)輸入的系統(tǒng)屬性 anotherFlag昙楚。如果前者的值發(fā)生改變,則緩存失效诈嘿;而如果后者的值改變堪旧,則緩存會被復(fù)用,而任務(wù)也不會處于最新的狀態(tài):
val systemProperty = project.providers.systemProperty("enableTask").forUseAtConfigurationTime()
if (systemProperty.orNull == "enabled") {
project.tasks.register("myTask", …) {
it.anotherFlag.set(project.providers.systemProperty("anotherFlag"))
}
}
在內(nèi)部奖亚,Gradle 會對在配置階段解析的值提供者 (value provider) 進(jìn)行持續(xù)跟蹤淳梦,每個值提供者都會被視為一個構(gòu)建邏輯輸入。另外昔字,除非調(diào)用 Provider.forUseAtConfigurationTime()爆袍,否則無法解析提供者,從而使得意外引入配置階段輸入的情況很難發(fā)生李滴。如前文所述螃宙,任何 Gradle 會在 build 文件發(fā)生改變時使配置緩存失效,這一特性與 ProviderFactory API 一起確保了 Gradle 可以捕獲影響任務(wù)圖的所有內(nèi)容所坯。
在任務(wù)間共享工作
如果您希望可以在任務(wù)間共享一些工作谆扎,例如: 避免多次連接到網(wǎng)絡(luò)服務(wù)器或者避免多次解析某些信息,那么可以使用兼容配置緩存的 共享構(gòu)建服務(wù) 來進(jìn)行實現(xiàn)芹助。就像任務(wù)一樣堂湖,構(gòu)建服務(wù)可以包含輸入信息闲先,并且這些內(nèi)容會在第一次運行后序列化。緩存的運行將會簡單地反序列化參數(shù)并實例化任務(wù)所需的構(gòu)建服務(wù)无蜂。構(gòu)建服務(wù)的額外好處是它與構(gòu)建生命周期非常契合伺糠,如果您希望在構(gòu)建完成后釋放一些資源,那么在您的構(gòu)建服務(wù)中使用 AutoCloseable 便可以實現(xiàn)這一功能斥季。由于無法被安全地序列化至磁盤训桶,添加構(gòu)建監(jiān)聽的操作與配置緩存不兼容。
從遷移 Android Gradle 插件獲得的經(jīng)驗教訓(xùn)
在努力使 Android Gradle 插件兼容配置緩存的過程中酣倾,我們學(xué)到了一些可能對插件和腳本作者有用的東西舵揭。
首先,在啟用配置緩存后躁锡,如果在構(gòu)建輸出中看到下面這樣的內(nèi)容午绳,不要氣餒,因為許多問題都是重復(fù)的映之,可以輕松解決:
428 problems were found reusing the configuration cache, 4 of which seem unique.
(在復(fù)用配置緩存后拦焚,發(fā)現(xiàn)了 428 處問題,其中 4 處看起來比較特別)
通過遷移到新的 API杠输,我們可以輕松解決許多問題赎败。例如:
舊代碼
abstract class MyTask: DefaultTask() {
@TaskAction
fun process() {
project.exec(…)
project.logger().log(…)
}
}
遷移過的代碼
abstract class MyTask: DefaultTask() {
@get:Inject
abstract val execOperations: ExecOperations
@TaskAction
fun process() {
execOperations.exec(…)
this.logger.log(…)
}
}
如果您仍在任務(wù)中使用 Project 實例,那么您需要找到一個替代 API抬伺。對于大多數(shù)情況螟够,都會有一個兼容的 API,您只需直接遷移即可峡钓。
另一個方便之處是避免了在任務(wù)創(chuàng)建時創(chuàng)建不可序列化或者開銷昂貴的對象妓笙,作為替代,會在我們的任務(wù)操作中需要時才創(chuàng)建它們能岩。例如寞宫,在下面的示例中,我們不必強(qiáng)制要求 Handler 類型可被序列化拉鹃,因為我們僅在需要時才創(chuàng)建它:
舊代碼
abstract class Mytask: DefaultTask() {
private val handler: Handler by lazy { createHandler(someInput) }
@TaskAction
fun process() {
handler.doSomething(…)
}
}
遷移過的代碼
abstract class Mytask: DefaultTask() {
@TaskAction
fun process() {
val handler = createHandler(someInput)
}
}
在創(chuàng)作任務(wù)時辈赋,請確保任務(wù)輸入正確反映了任務(wù)在執(zhí)行過程中所需的一切。避免訪問環(huán)境對象或任何可以從 Project 實例訪問的其他對象膏燕。例如: 如果您的插件創(chuàng)建了配置钥屈,請將其作為 FileCollection 傳遞給任務(wù)。如果您需要構(gòu)建目錄位置坝辫,請將其記錄在 task 的屬性中:
舊代碼
abstract class MyTask: DefaulTask() {
private val userConfiguration: MyDslObjects
@InputFiles
fun getClasses(): FileCollection {
return project.configurations.getByName(userConfiguration.name)
}
@Internal
fun getBuildDir(): File {
return project.buildDir
}
@TaskAction
fun process() { … }
}
遷移過的代碼
abstract class MyTask: DefaulTask() {
@get:InputFiles
abstract val classes: ConfigurableFileCollection
@get:Internal
abstract val buildDir: DirectoryProperty
@TaskAction
fun process() { … }
}
project.tasks.register("myTask", MyTask::class.java) {
it.classes.from(project.configurations.getByName(userConfiguration.name))
it.buildDir.set(project.layout.buildDirectory)
}
Android Gradle 插件曾依賴的一種常見模式篷就,是在首次使用時初始化一些對象,將其存儲在靜態(tài)字段中近忙,并利用構(gòu)建監(jiān)聽器在構(gòu)建完成時清除這些狀態(tài)竭业。正如上文所述智润,針對這種用例應(yīng)當(dāng)使用 共享構(gòu)建服務(wù)。請參閱下面的示例以了解如何使用它:
abstract MyBuildService: BuildService<BuildServiceParameters.None>, AutoCloseable {
fun doAndCacheSomeComplexWork() { ... }
override fun close() {
// 清除所有狀態(tài)未辆,釋放內(nèi)存
}
}
abstract class MyTask: DefaultTask() {
@get:Internal
abstract val myService: Property<MyBuildService>
}
最后一條建議是窟绷,當(dāng)您實現(xiàn)自定義可序列化類型時,要注意被序列化的內(nèi)容咐柜。確保不要序列化派生屬性兼蜈,并讓這些屬性成為臨時的或使用函數(shù)作為替代。舉例來說炕桨,在緩存運行時饭尝,您將會為 allLines 屬性獲取到一個舊的值,因此這一操作是必須的献宫。
舊代碼
class StringsFromFiles(private val inputs: FileCollection) {
val allLines = inputFiles.files.flatMap { it.readLines() }
}
遷移過的代碼
class StringsFromFiles(private val inputs: FileCollection): Serializable {
fun getAllLines() {
return inputFiles.files.flatMap { it.readLines() }
}
}
配置緩存目前還處于實驗階段,我們希望您可以嘗試并向我們提供反饋实撒。您可以通過 Android Studio issue 跟蹤 或 Gradle 的 issue 跟蹤 向我們報告您所遇到的任何問題姊途。
編碼愉快!