深入探索 Android Gradle 插件的緩存配置

什么是配置緩存?

配置緩存是一個提升 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着逐、PropertyProvider) 以及所有用戶定義的可序列化類型意蛀。在此階段結(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 跟蹤 向我們報告您所遇到的任何問題姊途。

編碼愉快!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末知态,一起剝皮案震驚了整個濱河市捷兰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌负敏,老刑警劉巖贡茅,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異其做,居然都是意外死亡顶考,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門妖泄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驹沿,“玉大人,你說我怎么就攤上這事蹈胡≡荆” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵罚渐,是天一觀的道長却汉。 經(jīng)常有香客問我,道長荷并,這世上最難降的妖魔是什么合砂? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮璧坟,結(jié)果婚禮上既穆,老公的妹妹穿的比我還像新娘赎懦。我一直安慰自己,他們只是感情好幻工,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布励两。 她就那樣靜靜地躺著,像睡著了一般囊颅。 火紅的嫁衣襯著肌膚如雪当悔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天踢代,我揣著相機(jī)與錄音盲憎,去河邊找鬼。 笑死胳挎,一個胖子當(dāng)著我的面吹牛饼疙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慕爬,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼窑眯,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了医窿?” 一聲冷哼從身側(cè)響起磅甩,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎姥卢,沒想到半個月后卷要,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡独榴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年僧叉,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片括眠。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡彪标,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出掷豺,到底是詐尸還是另有隱情捞烟,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布当船,位于F島的核電站题画,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏德频。R本人自食惡果不足惜苍息,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧竞思,春花似錦表谊、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至课梳,卻和暖如春距辆,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背暮刃。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工跨算, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人椭懊。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓诸蚕,卻偏偏與公主長得像,于是被迫代替她去往敵國和親灾搏。 傳聞我的和親對象是個殘疾皇子挫望,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345