关于android:使用新-Android-Gradle-插件加速您的应用构建

9次阅读

共计 11606 个字符,预计需要花费 30 分钟才能阅读完成。

自 2020 年底,Android Gradle 插件 (AGP) 曾经开始应用新的版本号规定,其版本号将与 Gradle 次要版本号保持一致,因而 AGP 4.2 之后的版本为 7.0 (目前最新的版本为 7.2)。在更新 Android Studio 时,您可能会收到一并将 Gradle 更新为最新可用版本的提醒。为了获得最佳性能,建议您应用 Gradle 和 Android Gradle 插件这两者的最新版本。Android Gradle 插件的 7.0 版本更新带来了许多实用的个性,本文将着重为您介绍其中的 Gradle 性能改良、配置缓存和插件扩大等方面的内容。

如果您更喜爱通过视频理解此内容,请在此处查看:

https://www.bilibili.com/vide…

△ 应用新 Android Gradle 插件减速您的利用构建

Gradle 的性能改良

Kotlin 符号解决优化

Kotlin 符号解决 (Kotlin Symbol Processing,简称 KSP) 是 kapt (Kotlin annotation processing tool) 的替代品,它为 Kotlin 语言带来了一流的注解解决能力,处理速度最快能够达到 kapt 的两倍。目前曾经有不少出名的软件库提供了兼容 KSP 的注解处理器,比方 Room、Moshi、Kotishi 等等。因而咱们倡议,当您的利用中所用到的各种注解处理器都反对 KSP 时,应该尽快从 kapt 迁徙到 KSP。

非传递性 R 类

启用非传递性 R 类 (non-transitive R-class) 后,您利用中的 R 类将只会蕴含在子项目中申明的资源,依赖项中的资源会被排除在外。这样一来,子项目中的 R 类大小将会显著缩小。

这一改变能够在您向运行时依赖项中增加新资源时,防止从新编译上游模块。在这种场景下,能够给您的利用带来 40% 的性能晋升。另外,在清理构建产物时,咱们发现性能有 5% 到 10% 的改善。

您能够在 gradle.properties 文件中增加上面的标记:

android.nonTransitiveRClass=true

△ 在 gradle.properties 中开启非传递性 R 类性能

您也能够在 Android Studio Arctic Fox 及以上版本应用重构工具来启用非传递性 R 类,具体须要您运行 Android Studio 菜单栏的 Refactor –> Migrate to Non-transitive R Classes。这种办法还能够在必要时帮忙您批改相干源代码。目前,AndroidX 库曾经启用此个性,因而 AAR 阶段的产物中将不再蕴含来自传递性依赖项的资源。

Lint 性能优化

从 Android Gradle 插件 7.0 版本开始,Lint 工作能够显示为 “UP-TO-DATE”,即如果模块的源代码和资源没有更改,那么就不须要对该模块进行 Lint 剖析工作。您须要在 build.gradle 中增加选项:

// build.gradle

android {
  ...
  lintOptions {checkDependencies true}
}

△ 在 build.gradle 中开启 lint 性能优化

如此一来,Lint 剖析工作就能够在各个模块中并行执行,从而显著晋升 Lint 工作运行的速度。

从 Android Gradle 插件的 7.1.0-alpha 13 版本开始,Lint 剖析工作兼容了 Gradle 构建缓存 (Gradle build cache),它能够通过 复用其余构建的后果来缩小新构建的工夫:

△ 不同 AGP 版本中 Lint 工夫比拟

咱们在一个演示我的项目中开启了 Gradle 构建缓存并设置 checkDependencies 为 true,而后别离应用 AGP 4.2、7.0 和 7.1 进行构建。从上图中可看出,7.0 版本的构建速度是 4.2 的两倍;并且在应用 AGP 7.1 时,因为所有 Lint 剖析工作都命中了缓存而带来了更加显著的速度晋升。

您岂但能够间接通过更新 Android Gradle 插件版本取得更好的 Lint 性能,还能通过一些配置来进一步晋升效率。其中一种办法是应用可缓存的 Lint 剖析工作。要启用 Gradle 的构建缓存,您须要在 gradle.properties 文件中开启上面的标记 (参见 Build Cache):

org.gradle.caching=true

△ 在 gradle.properties 中开启 Gradle 构建缓存

另一种可改良 Lint 剖析工作性能的办法是,在您条件容许的状况下给 Lint 调配更多的内存。

同时,咱们建议您在 利用模块 的 Gradle 配置中为 lintOptions 块增加:

checkDependencies true

△ 在模块的 build.gradle 中增加 checkDependencies 标记

尽管这样不能让 Lint 剖析工作更快执行,但可能让 Lint 在剖析您指定利用时捕捉到更多问题,并且为整个我的项目生成一份 Lint 报告。

Gradle 配置缓存

△ Gradle 构建过程和阶段划分

每当 Gradle 开始构建时,它都会创立一个工作图用于执行构建操作。咱们称这个过程为配置阶段 (configuration phase),它通常会继续几秒到数十秒。Gradle 配置缓存能够将配置阶段的输入进行缓存,并且在后续构建中复用这些缓存。当配置缓存命中,Gradle 会并行执行所有须要构建的工作。再加上依赖解析的后果也被缓存了,整个 Gradle 构建的过程变得更加疾速。

这里须要阐明,Gradle 配置缓存和构建缓存是不同的,后者缓存的是构建工作的产物。

△ Build 配置的输出内容

在构建过程中,您的构建设置决定了构建阶段的后果。所以配置缓存会将诸如 gradle.properties、构建文件等输出捕捉,放入缓存中。这些内容同您申请构建的工作一起,惟一地确定了在构建中要执行的工作。

△ 配置缓存带来的性能晋升

上图展现蕴含 24 个子我的项目的 Gradle 构建示例,这组构建应用了最新版本的 Kotlin、Gradle 和 Android Gradle 插件。咱们别离记录全量构建、有 ABI 变动和无 ABI 变动增量构建场景下启用配置缓存前后的比照。这里用增加新私有办法的形式进行增量构建,对应了 “ 有 ABI 变动 ” 的数据;用批改既有办法的实现来进行增量构建,对应了 “ 无 ABI 变动 ” 的数据。不言而喻,所有三个构建场景都呈现了 20% 的速度晋升。

接下来,联合代码,一探配置缓存的工作原理:

project.tasks.register("mytask", MyTask).configure {it.classes.from(project.configurations.getByName("compileClasspath"))
  it.name.set(project.name)
}

△ 配置缓存工作原理示例

在 Gradle 计算工作执行图之前,咱们尚处于配置阶段。此时能够应用 Gradle 提供的 project、task 容器、configuration 容器等全局对象来创立蕴含申明的输出和输入的工作。如上代码中,咱们注册了一个工作并进行相应配置。您能够在其中看到全局对象的多种用法,比方 project.tasks 和 project.configurations。

△ 存储配置缓存的过程

当所有工作都配置实现后,Gradle 能够依据咱们的配置计算出最终的工作执行图。随后配置缓存会将这个工作执行图缓存起来,并将各个工作的执行状态进行序列化,再放入缓存中。从上图能够看到,所有的工作输出也会被存储到缓存中,因而它们必须是特定的 Gradle 类型,或是能够序列化的数据。

△ 加载配置缓存的过程

最终,当某个配置缓存被命中时,Gradle 会应用缓存条目来创立工作实例。所以只有先前曾经被序列化的状态才会在新实例化的工作执行时被援用,这个阶段也不容许应用对全局状态的援用。

△ 新的 Build Analyzer 工具面板

咱们在 Android Studio 的 Arctic Fox 版本增加了 Build Analyzer 工具来帮忙您查看构建是否兼容配置缓存。当您的构建工作实现后,关上 Build Analyzer 面板,能够看到方才构建配置过程破费的工夫。如上图所示,配置构建过程总共应用了 9.8 秒。点击 Optimize this 链接,新面板中会显示更多信息,如下图所示:

△ Build Analyzer 提供的兼容性报告

如图,构建用到的所有插件都兼容配置缓存性能。点击 “Try Configuration cache in a build”,IDE 会更新您的 gradle.properties 文件,在其中启用配置缓存。在不齐全兼容的状况下,Build Analyzer 也可能会建议您将某些插件更新到与配置缓存兼容的新版本。如果您的构建与配置缓存不兼容,那么构建工作会失败,Build Analyzer 会提供相应的调试信息供您参考。

一个不兼容配置缓存的例子:

abstract class GetGitShaTask extends DefaultTask {@OutputFile File getOutputFile() {return new File(project.buildDir, "sha.txt") }
  @TaskAction void process() {def stdout = new ByteArrayOutputStream()
    project.exec {it.commandLine("git", "rev-parse", "HEAD")
      standardOutput = stdout
    }
    getOutputFile().write(stdout.toString())
  }
}
project.tasks.register("myTask", GetGitShaTask)

咱们有一个计算以后的 Git SHA 并将后果写入输入文件的工作。它会运行一个 git 命令,而后将输入内容写入给定文件中。咱们在启用配置缓存的状况下执行这个构建工作,会呈现两个与配置缓存相干的问题:

△ 配置缓存报告的内容

当您的构建工作与配置缓存不兼容时,Gradle 会生成一个蕴含了问题列表和详细信息的 HTML 文件。在咱们的例子中,这个 HTML 文件会蕴含图中的内容:

△ 配置缓存错误报告

您能够从这些内容中找到各个出错点对应的堆栈跟踪信息。如示例中构建脚本的第 5 和第 11 行导致了这些问题。回看源文件,您会发现第一个问题是因为返回输入文件地位的函数中应用了 project.buildDir 办法;第二个问题是因为 TaskAction 中应用了 project 变量,这是因为启用配置缓存后,咱们无奈在运行时拜访全局状态。

咱们能够对下面的代码进行一些批改。为了在运行时调用 project.buildDir 办法,咱们能够在工作属性中存储必要的信息,这样就能够一起被存入配置缓存中了。另外,咱们能够应用 Gradle 服务注入来执行内部过程并获取输入信息。上面是批改后的代码供您参考:

abstract class GetGitShaTask extends DefaultTask {@OutputFile abstract RegularFileProperty getOutputFile()
  @javax.inject.Inject abstract ExecOperations getExecOperations()
  @TaskAction void process() {def stdout = new ByteArrayOutputStream()
    getExecOperations().exec {// ...}
    getOutputFile().get().asFile.write(stdout.toString())
  }
}
project.tasks.register("myTask", GetGitShaTask) {getOutputFile().set(project.layout.buildDirectory.file("sha.txt")
  )
}

△ 应用 Gradle 服务注入来执行内部过程 (与配置缓存兼容的构建工作例子)

您能够从新代码发现,咱们在工作注册期间,将输入文件的地位捕捉并存入了某个属性中,而后通过注入的 Gradle 服务来执行 git 命令并取得命令的输入信息。这段代码还有另外一个益处,因为 Gradle 的提早属性是理论应用时才计算的,所以 buildDirectory 产生的变动会主动反映在工作的输入文件地位上。

对于 Gradle 配置缓存和如何迁徙您的构建工作的更多信息,请参阅:

  • Gradle 文档
  • 深刻摸索 Android Gradle 插件的缓存配置

扩大 Android Gradle 插件

不少开发者都发现在本人的构建工作中,有一些操作是无奈通过 Android Gradle 插件间接实现的。所以接下来咱们会着重探讨如何通过 AGP 新增的 Variant 和 Artifact API 来实现这些性能。

△ Android Gradle 插件的执行构造

build 类型 (buildTypes) 和产品变种 (productFlavors) 都是您我的项目的 build.gradle 文件中的概念。Android Gradle 插件会依据您的这些定义生成不同的变体对象,并对应各自的构建工作。这些构建工作的输入会被注册为与工作对应的工件 (artifact),并且依据须要被分为私有工件和公有工件。晚期版本的 AGP API 容许您拜访这些构建工作,然而这些 API 并不持重,因为每个工作的具体实现细节是会产生扭转的。Android Gradle 插件在 7.0 版本中引入了新的 API,让您能够拜访到这些变体对象和一些两头工件。这样一来,开发者就能够在不操作构建工作的前提下扭转构建行为。

批改构建时产生的工件

在这个局部,咱们要通过批改 asset 的工件来向 APK 增加额定的 asset,代码如下:

// buildSrc/src/main/kotlin/AddAssetTask.kt
abstract class AddAssetTask: DefaultTask() {
  @get:Input
  abstract val content: Property<String>
 
  @get:OutputDirectory
  abstract val outputDir: DirectoryProperty
 
  @TaskAction
  fun taskAction() {File(outputDir.asFile.get(), "extra.txt").writeText(content.get())  
  }
}

△ 向 APK 增加额定的 asset

下面的代码定义了一个名为 AddAssetTask 的工作,它只有一个字符串输出内容属性和一个输入目录属性 (DirectoryProperty 类型)。这个工作的作用是将输出字符串写入输入目录中的文件。随后咱们须要在 ToyPlugin.kt 中编写一个插件,利用 Variant 和 Artifact API 来将 AddAssetTask 的实例连贯到对应的工件:

// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {override fun apply(project: Project) {val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.onVariants { variant ->
      val taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {it.content.set("foo")
        }
 
      // 外围局部
      variant.artifacts
        .use(taskProvider)
        .wireWith(AddAssetTask::outputDir)
        .toAppendTo(MultipleArtifact.ASSETS)
    }
  }
}

△ 将 AddAssetTask 实例连贯到对应的工件

上述代码中的外围局部会将工作的输入目录增加到 asset 目录的汇合中,并正确连贯工作依赖项。这段代码中咱们将额定 asset 的内容硬编码为 “foo”,但前面的步骤咱们会对这里进行更改,还请您浏览时注意。

△ 可供开发者操作的两头工件举例

上图中展现了您能够拜访到的几种两头工件,咱们的 Toy 示例中就用到了其中的 ASSETS 工件。Android Gradle 插件为不同工件提供了额定的拜访形式,比方当您想要校验某个工件的内容时,能够通过上面的代码来取得 AAR 工件:

androidComponents.onVariants { variant ->
  val aar: RegularFileProperty = variant.artifacts.get(AAR)
}

△ 获取 AAR 工件

请参阅 Android 开发者文档 Variant API、工件和工作 获取对于 Android Gradle 插件新 Variants 和 Artifact API 的材料,这些材料能够帮忙您更深刻理解如何与两头工件进行交互。

批改和扩大 DSL

接下来咱们须要批改 Android Gradle 插件的 DSL,从而容许咱们设置额定 asset 的内容。新版本的 Android Gradle 插件容许您为自定义插件编写额定的 DSL 内容,所以咱们会用这种形式来编辑每个构建类型的额定 asset。上面的代码展现了咱们对模块的 build.gradle 文件的批改。

// app/build.gradle
 
android {
  ...
  buildTypes {
    release {
      toy 
        content = "Hello World"
      }
    }
  }
}

△ 在 build.gradle 中增加自定义 DSL

另外,为了可能扩大 Android Gradle 插件的 DSL,咱们须要创立一个简略的接口。您能够参照上面一段代码:

// buildSrc/src/main/kotlin/ToyExtension.kt
 
interface ToyExtension {var content: String?}

△ 定义 toyExtension 接口

定义好接口之后,咱们须要为每一个 build 类型增加新定义的扩大:

// buildSrc/src/main/kotlin/ToyPlugin.kt
 
abstract class ToyPlugin: Plugin<Project> {override fun apply(project: Project) {val android = project.extensions.getByType(ApplicationExtension::class.java)
 
    android.buildTypes.forEach {it.extensions.add("toy", ToyExtension::class.java)
    }
    // ...
  }
}

△ 为所有 build 类型增加新定义的扩大

您也能够应用自定义接口扩大产品变种,不过在这个例子中咱们不须要这样做。咱们还须要对 ToyPlugin.kt 作进一步批改,让插件能够获取到咱们在 DSL 中为每个变体定义的 asset 内容:

// buildSrc/src/main/kotlin/ToyPlugin.kt
abstract class ToyPlugin: Plugin<Project> {override fun apply(project: Project) {
    // ...
    // 留神这里省略了上一段代码减少的内容
    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.onVariants { variant ->
      val buildType = android.buildTypes.getByName(variant.buildType)
      val toyExtension = buildType.extensions.findByName("toy") as? ToyExtension
 
      val content = toyExtension?.content ?: "foo"
      val taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {it.content.set(content)
        }
 
      // 留神这里省略了批改工件的局部
      // ...
    }
  }
}

△ 在产品变体中应用自定义 DSL

上述代码中,咱们减少了一段代码用于获取新增的 toyExtension 定义的内容,也就是方才批改 DSL 时为每个 build 类型定义的额定 asset。须要您留神,咱们这里定义了备选 asset 内容,也就是当您没有为某个 build 类型定义 asset 时,会默认应用的值。

应用 Variant API 增加自定义属性

您还能够用相似扩大 DSL 的办法来扩大 Variant API,具体来说就是向 Android Gradle 插件的 Variant 对象中增加您本人的 Gradle 属性或某种 Gradle Provider。相比仅扩大 DSL,扩大 Variant API 有这样一些劣势:

  1. DSL 值是固定的,但自定义变体属性能够应用构建工作的输入,Gradle 会主动解决所有构建工作的依赖项。
  2. 您能够很不便地为每个变体的自定义变体属性设置独立的值。
  3. 与自定义 DSL 相比,自定义变体属性能提供与其余插件之间更简略、持重的交互。

当咱们须要增加自定义变体属性时,首先要创立一个简略的接口:

// buildSrc/src/main/kotlin/ToyVariantExtension.kt
 
interface ToyVariantExtension {val content: Property<String>}
 
// 比拟之前的 ToyExtension (您不须要在代码中包含这部分) 
interface ToyExtension {val content: String?}

△ 定义带有自定义变体属性的扩大 (比照一般扩大)

通过与先前的 ToyExtension 定义比照,您会留神到咱们应用了 Property 而不是可空字符串类型。这样做是为了与 Android Gradle 插件外部的代码习惯保持一致,既能反对您将工作的输入作为自定义属性的值,又防止您再去思考简单的插件排序过程。其余插件也能够设置属性值,至于产生在 Toy 插件之前还是之后都没有影响。上面的代码展现了应用自定义属性的形式:

// app/build.gradle
androidComponents {
  onVariants(selector().all(),
    { variant ->
      variant.getExtension(ToyVariantExtension.class)
        ?.content
        ?.set("Hello ${variant.name}")
    }
  )
}

△ 在 build.gradle 中应用带有自定义变体属性的扩大

尽管这样的写法没有间接扩大 DSL 那样简略,但它能够很不便地为每个变体设置自定义属性的值。相应的,还须要批改 ToyPlugin.kt 文件:

// buildSrc/src/main/kotlin/ToyPlugin.kt
 
abstract class ToyPlugin: Plugin<Project> {override fun apply(project: Project) {
    // ...
    // 留神这里省略了局部内容
    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
 
    androidComponents.beforeVariants { variantBuilder ->
      val buildType = android.buildTypes.getByName(variantBuilder.buildType)
      val toyExtension = buildType.extensions.findByName("toy") as? ToyExtension
 
      val variantExtension = project.objects.newInstance(ToyVariantExtension::class.java)
      variantExtension.content.set(toyExtension?.content ?: "foo")
      variantBuilder.registerExtension(ToyVariantExtension::class.java, variantExtension)
 
      // 留神这里省略了局部内容
      // ...
    }
  }
}

△ 注册带有自定义变体属性的 AGP 扩大

在这段代码里,咱们创立了 ToyVariantExtension 的实例,首先用 toy DSL 中的值作为自定义变体属性对应的 Property 的默认值,随后将这个实例注册到变体对象上。您会发现咱们应用了 beforeVariants 而不是 onVariants,这是因为变体扩大必须在 beforeVariants 块中注册,只有这样,onVariants 块中的其余插件才能够应用新注册的扩大。另外须要您留神,咱们在 beforeVariants 块中获取了自定义 toy DSL 中的值,这个操作其实是平安的。因为当调用 beforeVariants 回调时,DSL 的值会被当作最终后果并锁定,也就不会产生额定的平安问题。获取到 toy DSL 中的值后,咱们将它赋值给自定义变体属性,并最终在变体上注册新的扩大 (ToyVariantExtension)。

实现 beforeVariants 块的各项操作后,咱们能够持续在 onVariants 块将自定义变体属性赋值给工作输出了。这个过程很简略,请参考上面的代码:

// buildSrc/src/main/kotlin/ToyPlugin.kt
 
abstract class ToyPlugin: Plugin<Project> {override fun apply(project: Project) {
    // ...
    // 留神这里省略了上一段展现内容
 
    androidComponents.onVariants { variant ->
      val content = variant.getExtension(VariantExtension::class.java)?.content
      val taskProvider = 
        project.tasks.register(variant.name + "AddAsset", AddAssetTask::class.java) {it.content.set(content)
        }
 
      // 留神这里省略了批改工件的局部
      // ...
    }
  }
}

△ 应用自定义变体属性

下面这段代码很好地展现了应用自定义变体属性的劣势,特地是当您有多个须要以变体专用的形式进行交互的插件时更是如此。如果其余插件也想设置您的自定义变体属性,或者将属性用于它们的构建工作,也只须要应用相似上述 onVariants 代码块的形式。

如果您想要理解更多对于扩大 Android Gradle 插件的内容,敬请关注咱们的 Gradle 与 AGP 构建 API 系列文章。您也能够浏览 Android 开发者 文档: 扩大 Android Gradle 插件 或者研读 GitHub 上的 AGP Cookbook。在不久的未来,咱们还会推出更多构建和同步方面的改良,敬请关注。

下一步工作

Project Isolation

Gradle Project Isolation 是基于配置缓存的一个新个性,旨在提供更快地构建和同步速度。每个我的项目的配置都是彼此隔离的,不容许跨我的项目的援用,于是 Gradle 能够缓存每个我的项目的同步 (sync) 后果,每当构建文件发生变化,只有受影响的我的项目会被重新配置。目前这个性能还在开发中,您能够在 gradle.properties 文件中增加 org.gradle.unsafe.isolated-projects=true 开关来尝试这个个性 (须要 Gradle 7.2 及以上版本)。

改良 Kotlin 增量编译

咱们还和 JetBrains 一起单干改良 Kotlin 的增量编译,指标是反对所有的增量编译场景,比方批改 Android 资源、增加内部依赖项或批改非 Kotlin 的上游子我的项目。

感激所有开发者们的反对,感激大家试用咱们的预览版工具并提供问题反馈。请您继续关注咱们的停顿,也欢迎您遇到问题时与咱们沟通。

欢迎您 点击这里 向咱们提交反馈,或分享您喜爱的内容、发现的问题。您的反馈对咱们十分重要,感谢您的反对!

正文完
 0