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

什麼是配置緩存?

配置緩存是一個提高 IDE 和命令行構建速度的基礎構建塊。這是 Gradle 6.6 版本提供的一個高度實驗性功能,它可使構建系統記錄一次任務的圖譜信息,並在接下來的構建中進行復用,從而避免再一次配置整個工程。這一功能也是配置階段改進的延續,這些改進中引入了 惰性配置 (lazy configuration),以免在構建的配置階段進行沒必要要的工做。這些改進對於快速迭代開發的重要性不言自明,然後者也是 Android Studio 團隊所持續關注的一個用例。html

性能改進

這一功能的主要目標即是提高構建速度。在 Android 版 Santa Tracker 工程的基準化分析中,對於啓用了配置緩存的構建過程,咱們測量出其在 Android Studio 中的總構建時間減小了 35% (從 688ms 到 443ms,測試平臺爲 Linux,使用 Intel® Xeon® Gold 6154 CPU @ 3.00GHz )。下圖展現了使用和不使用配置緩存進行 100 次構建的平均總構建時間 (以毫秒爲單位):java

對於一些工程,配置階段可能會消耗 10 秒鐘以上,節省時間的效果也所以更加顯著。不管運行的是全新構建、增量構建仍是更新構建,配置階段的開銷都是相同的。要衡量您的構建過程當中配置階段所消耗的時間,能夠以空運行模式 (dry run mode) 運行任務,例如: ./gradlew :app:assembleDebug --dry-runandroid

爲了進一步避免重複運行配置過程,配置緩存還容許來自同一工程的任務並行運行。之前,只有利用 Worker API 的任務能夠同時運行,可是因爲配置緩存能夠確保任務獨立且沒法訪問全局共享狀態 (例如 Project 實例),所以能夠默認啓用此行爲。並且,依賴關係解析結果能夠在運行間進行緩存,從而有助於優化總體構建時間。git

如何試用?

配置緩存功能如今還處於實驗階段,咱們但願您能夠嘗試它並向咱們提供反饋。爲了在您的構建中使用它,須要保證全部工程所應用的全部插件都是兼容的,這是爲了安全地 (反) 序列化任務圖。您可能須要更新某些 Gradle 插件。您能夠經過此 issue 來獲取受支持插件的完整列表,若是您使用的插件不在其中,請在它們的問題跟蹤器中提交問題,並從 Gradle 問題中連接至該 issue。 github

最新版的 Android Gradle 插件版本爲 4.1 (目前爲 4.1.0-rc03),但若是您但願獲取全部的錯誤修復,請嘗試最新的 4.2 版本 (目前爲 4.2.0-alpha13)。Gradle 的版本應爲 6.6,同時若是您正在使用 Kotlin,請將 Kotlin Gradle 插件更新爲最新的 1.4 版 (相關 Kotlin issue)。最後使用如下代碼更新 gradle.properties:web

org.gradle.unsafe.configuration-cache=true
# 當心使用這一標記,由於有些插件尚未徹底兼容
org.gradle.unsafe.configuration-cache-problems=warn

查看全部 Android Gradle 插件版本,請參考以下頁面:api

https://maven.google.com/web/index.html#com.android.tools.build:gradle緩存

若是啓用了配置緩存,您應該能夠在第一次運行時經過 Android Studio 的 Build 輸出窗口或命令行看到 "Calculating task graph as no configuration cache is available for tasks…" (因爲當前任務沒有可用配置,正在生成任務圖譜...) 字樣;而在第二次運行中會複用配置緩存,因此輸出中會包含 "Reusing configuration cache. (複用配置緩存)"。安全

不管您遇到任何問題,均可以在 Android Studio issue 跟蹤 或 Gradle issue 跟蹤 中向咱們反饋。服務器

它是如何工做的?

想要深刻了解配置緩存,咱們要從瞭解構建的配置階段開始。就算您開啓了配置緩存,第一次構建仍會經歷這一過程。在配置階段,全部被包含的工程 (在評估 settings.gradle 時獲取) 都會依據其構建文件的評估結果進行配置。一般首先會應用全部插件,同時 DSL 對象會被實例化;接下來會繼續評估構建文件,而 DSL 對象將會被分配您所指定的值。當構建文件的評估完成時,會調用 Android Gradle 插件 (以及許多遵循相同模式的其餘插件) 的 Project.afterEvaluate 回調。在此回調的調用期間,Android Gradle 插件會完成其絕大部分的工做,包括建立變體以及註冊任務。

在評估 DSL 以及註冊任務以後,接下來的階段會構建一個任務圖。您所要求執行的任務以及它們所依賴的任務都會被徹底配置。這一過程將會持續到觸達沒有依賴的葉子任務爲止。配置的這一階段將會輸出一個任務圖,Gradle 中的調度機制會使用該任務圖來運行構建操做。當任務圖被完成後,配置緩存會將其存儲在磁盤中 (在 Gradle 6.6 中位於根工程的 .gradle/configuration-cache directory 目錄下) 。它能夠序列化全部的 Gradle-managed 類型 (如 FileCollectionPropertyProvider) 以及全部用戶定義的可序列化類型。在此階段結束時,每一個任務的狀態都將被徹底記錄並保留下來。

在第二次構建時,假設 Gradle 可以複用記錄的緩存,則會加載所請求任務的任務圖、跳過 DSL 評估,任務配置等。這意味着全部任務都將被實例化,而它們的全部屬性都將從緩存中加載。從這一時刻起,構建過程基本與無緩存構建無異,區別只是默認狀況下能夠並行運行任務以及複用緩存中的依賴項解析結果的優點。

爲了保證正確性,Gradle 會持續跟蹤會影響已緩存的任務圖的全部輸入,包括構建文件、請求執行的任務以及配置過程當中對於 Gradle 和系統屬性的的訪問。請求運行一組不一樣的任務會產生一個不一樣的任務圖,因此須要建立一個新的緩存記錄。一個須要使狀態失效的例子是: 您修改了 build 文件或 buildSrc,並向環境變量或系統屬性傳遞了一個不一樣的值。爲了檢測這類變動,構建系統會建立一個緩存任務圖時所使用的 build 文件的快照;此外,它還會檢測 buildSrc 中是否有未更新的任務。最後,任何會影響配置階段的值都應當被包裝爲 Gradle-managed 類型,這有助於構建系統對配置階段中所使用的變量進行持續跟蹤。

使用兼容的 Gradle API

構建中應用的全部 Gradle 插件都必須與配置緩存兼容,Gradle 也所以引入了一組新的 API。下面是咱們對於配置緩存和新 API 所帶來的約束進行的考察:

在任務中使用 Project 實例

Gradle 插件中最多見的兼容性問題來自於在任務操做中使用 Task.getProject()。在使用配置緩存時,爲了保持每一個任務徹底獨立,任務將沒法訪問這一共享狀態。因爲 Project 實例能夠訪問 TaskContainerConfigurationContainer 以及其餘在啓用緩存的運行期間不會填充的對象,從而致使反映出無效的狀態,因此禁用它是必須的。引入了不少可替代的 API,好比用於延遲對象建立的 ObjectFactory,還有能夠用於獲取項目文件系統分佈狀況的接口,好比 ProjectLayout,若是須要在構建中啓動進程,可使用 ExecOperations。您能夠參考 完整的 API 列表 來進行遷移工做。

訪問 Gradle/系統 屬性與環境變量

若是您使用系統屬性、Gradle 屬性、環境變量或者額外文件來指定構建的邏輯輸入時,會產生怎樣的結果?構建系統已經在跟蹤 build 文件的修改,可是任何影響任務圖的額外值都應當使用 ProviderFactory API 進行獲取。下面的示例展現瞭如何獲取影響配置的 enableTask 系統屬性值,以及如何獲取僅做爲任務輸入的系統屬性 anotherFlag。若是前者的值發生改變,則緩存失效;而若是後者的值改變,則緩存會被複用,而任務也不會處於最新的狀態:

val systemProperty = project.providers.systemProperty("enableTask").forUseAtConfigurationTime()
if (systemProperty.orNull == "enabled") {
    project.tasks.register("myTask", …) {
        it.anotherFlag.set(project.providers.systemProperty("anotherFlag"))
    }
}

在內部,Gradle 會對在配置階段解析的值提供者 (value provider) 進行持續跟蹤,每一個值提供者都會被視爲一個構建邏輯輸入。另外,除非調用 Provider.forUseAtConfigurationTime(),不然沒法解析提供者,從而使得意外引入配置階段輸入的狀況很難發生。如前文所述,任何 Gradle 會在 build 文件發生改變時使配置緩存失效,這一特性與 ProviderFactory API 一塊兒確保了 Gradle 能夠捕獲影響任務圖的全部內容。

在任務間共享工做

若是您但願能夠在任務間共享一些工做,例如: 避免屢次鏈接到網絡服務器或者避免屢次解析某些信息,那麼可使用兼容配置緩存的 共享構建服務 來進行實現。就像任務同樣,構建服務能夠包含輸入信息,而且這些內容會在第一次運行後序列化。緩存的運行將會簡單地反序列化參數並實例化任務所需的構建服務。構建服務的額外好處是它與構建生命週期很是契合,若是您但願在構建完成後釋放一些資源,那麼在您的構建服務中使用 AutoCloseable 即可以實現這一功能。因爲沒法被安全地序列化至磁盤,添加構建監聽的操做與配置緩存不兼容。

從遷移 Android Gradle 插件得到的經驗教訓

在努力使 Android Gradle 插件兼容配置緩存的過程當中,咱們學到了一些可能對插件和腳本做者有用的東西。

首先,在啓用配置緩存後,若是在構建輸出中看到下面這樣的內容,不要氣餒,由於許多問題都是重複的,能夠輕鬆解決:

428 problems were found reusing the configuration cache, 4 of which seem unique.

(在複用配置緩存後,發現了 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(…)
   }
}

若是您仍在任務中使用 Project 實例,那麼您須要找到一個替代 API。對於大多數狀況,都會有一個兼容的 API,您只需直接遷移便可。

另外一個方便之處是避免了在任務建立時建立不可序列化或者開銷昂貴的對象,做爲替代,會在咱們的任務操做中須要時才建立它們。例如,在下面的示例中,咱們沒必要強制要求 Handler 類型可被序列化,由於咱們僅在須要時才建立它:

舊代碼

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)
    }
}

在創做任務時,請確保任務輸入正確反映了任務在執行過程當中所需的一切。避免訪問環境對象或任何能夠從 Project 實例訪問的其餘對象。例如: 若是您的插件建立了配置,請將其做爲 FileCollection 傳遞給任務。若是您須要構建目錄位置,請將其記錄在 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 插件曾依賴的一種常見模式,是在首次使用時初始化一些對象,將其存儲在靜態字段中,並利用構建監聽器在構建完成時清除這些狀態。正如上文所述,針對這種用例應當使用 共享構建服務。請參閱下面的示例以瞭解如何使用它:

abstract MyBuildService: BuildService<BuildServiceParameters.None>, AutoCloseable {
    
    fun doAndCacheSomeComplexWork() { ... }
 
    override fun close() {
        // 清除全部狀態,釋放內存
    }
}

abstract class MyTask: DefaultTask() {
    @get:Internal
    abstract val myService: Property<MyBuildService>
}

最後一條建議是,當您實現自定義可序列化類型時,要注意被序列化的內容。確保不要序列化派生屬性,並讓這些屬性成爲臨時的或使用函數做爲替代。舉例來講,在緩存運行時,您將會爲 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 跟蹤 向咱們報告您所遇到的任何問題。

編碼愉快!

相關文章
相關標籤/搜索