Speed up your app builds with the new Android Gradle plugin

Since the end of 2020, the Android Gradle Plugin (AGP) has started to use a new version number rule, and its version number will be consistent with the major version number of Gradle, so the version after AGP 4.2 is 7.0 (the latest version is currently 7.2). When updating Android Studio , you may be prompted to update Gradle to the latest available version. For best performance, it is recommended that you use the latest versions of both Gradle and the Android Gradle plugin. The 7.0 update to the Android Gradle Plugin brings many useful features, and this article will focus on Gradle performance improvements, configuration caching, and plugin extensions.

If you prefer to see this through video , check it out here .

Gradle performance improvements

Kotlin Symbol Handling Optimizations

Kotlin Symbol Processing ( KSP for short ) is an alternative to kapt (Kotlin annotation processing tool), which brings first-class annotation processing capabilities to the Kotlin language, and the processing speed can be up to twice that of kapt. At present, there are many well-known software libraries that provide KSP-compatible annotation processors, such as Room, Moshi, Kotishi and so on. Therefore, we recommend that you migrate from kapt to KSP as soon as possible when the various annotation processors used in your application support KSP.

nontransitive R class

When non-transitive R-classes are enabled, the R classes in your app will only include resources declared in subprojects, and resources in dependencies will be excluded. As a result, the size of the R class in the subproject will be significantly reduced.

This change avoids recompiling downstream modules when you add new resources to runtime dependencies. In this scenario, it can bring a 40% performance improvement to your application. Also, we saw a 5% to 10% improvement in performance when cleaning build artifacts.

You can add the following markup in your gradle.properties file:

android.nonTransitiveRClass=true

△ Turn on the non-transitive R class function in gradle.properties

You can also use the refactoring tool in Android Studio Arctic Fox and above to enable non-transitive R classes by running Refactor --> Migrate to Non-transitive R Classes from the Android Studio menu bar. This approach can also help you modify the relevant source code if necessary. Currently, the AndroidX library has this feature enabled, so the artifacts from the AAR stage will no longer include resources from transitive dependencies.

Lint performance optimization

Starting from Android Gradle plugin version 7.0, Lint tasks can be displayed as " UP-TO-DATE ", that is, if the source code and resources of a module have not changed, then there is no need for a Lint analysis task for that module. You need to add option in build.gradle:

// build.gradle

android {
  ...
  lintOptions {
    checkDependencies true
  }
}

△ Enable lint performance optimization in build.gradle

This allows Lint analysis tasks to be executed in parallel across modules, significantly increasing the speed of Lint tasks.

Starting with version 7.1.0-alpha 13 of the Android Gradle plugin, Lint analysis tasks are compatible with the Gradle build cache, which can reduce the time of new builds by reusing the results of other builds :

△ Comparison of Lint time in different AGP versions

△ Comparison of Lint time in different AGP versions

We turned on the Gradle build cache and set checkDependencies to true in a demo project, and then built with AGP 4.2, 7.0, and 7.1. As you can see from the graph above, version 7.0 builds twice as fast as 4.2; and when using AGP 7.1, there is an even more significant speedup due to cache hits for all Lint analysis tasks.

Not only can you get better Lint performance directly by updating the Android Gradle plugin version, but you can also further improve efficiency with some configuration. One way to do this is to use a cacheable Lint analysis task. To enable Gradle's build cache, you need to enable the following flags in the gradle.properties file (see Build Cache ):

org.gradle.caching=true

△ Enable Gradle build cache in gradle.properties

Another way to improve the performance of Lint's profiling tasks is to allocate more memory to Lint if you can.

In the meantime, we recommend that you add to the lintOptions block in the Gradle configuration of your app module:

checkDependencies true

△ Add the checkDependencies tag to the module's build.gradle

While this won't make Lint analysis tasks faster, it will allow Lint to catch more problems when analyzing your specific application and generate a Lint report for the entire project.

Gradle configuration cache

△ Gradle build process and stage division

△ Gradle build process and stage division

Whenever Gradle starts a build, it creates a graph of tasks to perform the build. We call this process the configuration phase, and it usually lasts from a few seconds to tens of seconds. The Gradle configuration cache can cache the output of the configuration phase and reuse these caches in subsequent builds. When the configuration cache hits, Gradle executes all tasks that need to be built in parallel. Coupled with the fact that the results of dependency resolution are also cached, the entire Gradle build process becomes much faster.

It should be noted here that the Gradle configuration cache is different from the build cache, which caches the products of build tasks.

△ Build configuration input content

△ Build configuration input content

During the build process, your build settings determine the outcome of the build phase. So the configuration cache will capture inputs such as gradle.properties, build files, etc. and put them in the cache. These, along with the tasks you requested to build, uniquely determine the tasks to be performed in the build.

△ The performance improvement brought by the configuration cache

△ Performance improvement brought by configuration cache

The image above shows an example Gradle build with 24 subprojects using the latest versions of Kotlin, Gradle, and the Android Gradle plugin. We record the comparisons before and after enabling configuration caching in the full build, incremental build scenarios with ABI changes, and incremental builds without ABI changes. Here, incremental construction is performed by adding new public methods, which corresponds to data with "ABI changes"; incremental construction is performed by modifying the implementation of existing methods, corresponding to data with "no ABI changes". A 20% speed improvement is evident for all three build scenarios.

Next, combine the code to explore how the configuration cache works:

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

△ Example of how the configuration cache works

Before Gradle computes the task execution graph, we are still in the configuration phase. At this point, you can use the global objects provided by Gradle such as project, task container, configuration container, etc. to create tasks that contain declared inputs and outputs. In the above code, we register a task and configure it accordingly. There you can see various uses of global objects such as project.tasks and project.configurations.

△ The process of storing the configuration cache

△ The process of storing the configuration cache

When all tasks are configured, Gradle can calculate the final task execution graph based on our configuration. The configuration cache will then cache the task execution graph, serialize the execution status of each task, and put it into the cache. As you can see from the above image, all task inputs are also stored in the cache, so they must be of a specific Gradle type, or serializable data.

△ The process of loading the configuration cache

△ The process of loading the configuration cache

Eventually, when a configuration cache is hit, Gradle uses the cache entry to create a task instance. So only previously serialized state will be referenced when the newly instantiated task executes, and references to global state are not allowed at this stage.

△ New Build Analyzer tool panel

△ New Build Analyzer tool panel

We've added the Build Analyzer tool to the Arctic Fox version of Android Studio to help you check if your build is compatible with the configuration cache. When your build task is complete, open the Build Analyzer panel and you can see how long the build configuration process took. As you can see in the image above, the configuration build process took a total of 9.8 seconds. Click on the Optimize this link and more information will be displayed in a new panel, as shown in the image below:

△ Compatibility report provided by Build Analyzer

△ Compatibility report provided by Build Analyzer

As shown in the figure, all plugins used in the build are compatible with the configuration cache function. Click "Try Configuration cache in a build" and the IDE will update your gradle.properties file to enable configuration caching in it. The Build Analyzer may also recommend that you update some plugins to newer versions that are compatible with the configuration cache in the case of not being fully compatible. If your build is not compatible with the configuration cache, the build task will fail and the Build Analyzer will provide you with corresponding debug information for your reference.

An example of an incompatible configuration cache:

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)

We have a task that computes the current Git SHA and writes the result to an output file. It runs a git command and then writes the output to the given file. When we execute this build task with configuration caching enabled, there are two issues related to configuration caching:

△ Configure the content of the cache report

△ Configure the content of the cache report

When your build task is not compatible with the configuration cache, Gradle generates an HTML file with a list of issues and details. In our case, this HTML file will contain the content of the figure:

△ Configuration cache error report

△ Configuration cache error report

From these you can find stack traces for each error point. Lines 5 and 11 of the build script as in the example cause these problems. Looking back at the source files, you will see that the first problem is due to the use of the project.buildDir method in the function that returns the location of the output file; the second problem is due to the use of the project variable in the TaskAction, which is due to the fact that after enabling configuration caching, we Global state cannot be accessed at runtime.

We can make some modifications to the above code. In order to call the project.buildDir method at runtime, we can store the necessary information in the task properties so that it can be stored in the configuration cache together. Alternatively, we can use Gradle service injection to execute external processes and get output information. Here is the modified code for your reference:

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

△ Use Gradle service injection to execute external processes (example of build tasks compatible with configuration cache)

You can find from the new code that we capture and store the location of the output file in a property during task registration, and then execute the git command and get the output information of the command through the injected Gradle service. This code has another benefit, since Gradle's deferred properties are calculated only when they are actually used, changes to the buildDirectory are automatically reflected in the task's output file location.

For more information on the Gradle configuration cache and how to migrate your build tasks, see:

Extending the Android Gradle plugin

Many developers have found that in their build tasks, there are some operations that cannot be directly implemented through the Android Gradle plugin. So next we will focus on how to implement these functions through the newly added Variant and Artifact APIs of AGP.

△ Execution structure of the Android Gradle plugin

△ Execution structure of the Android Gradle plugin

Both build types (buildTypes) and product flavors (productFlavors) are concepts in your project's build.gradle file. The Android Gradle plugin will generate different variant objects based on your definitions, corresponding to their respective build tasks. The output of these build tasks is registered as an artifact corresponding to the task, and divided into public and private artifacts as needed. Earlier versions of the AGP API gave you access to these build tasks, but these APIs were not robust because the specific implementation details of each task changed. The Android Gradle plugin introduced new APIs in version 7.0 that give you access to these variant objects and some intermediate artifacts. This allows developers to change build behavior without manipulating build tasks.

Modify the artifacts produced at build time

In this section, we are going to add additional assets to the APK by modifying the asset's artifact, the code is as follows:

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

△ Add additional assets to APK

The above code defines a task called AddAssetTask, which has only a string input content property and an output directory property (of type DirectoryProperty). What this task does is write the input string to a file in the output directory. Then we need to write a plugin in ToyPlugin.kt that uses the Variant and Artifact API to connect the instance of AddAssetTask to the corresponding artifact:

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

△ Connect the AddAssetTask instance to the corresponding artifact

The core part of the code above will add the task's output directory to the collection of asset directories and correctly wire up the task dependencies. In this code, we hardcoded the content of the extra asset as "foo", but we will change this in the next steps, so please keep an eye on it as you read.

△ Examples of intermediate artifacts that can be operated by developers

△ Examples of intermediate artifacts that can be operated by developers

The image above shows several intermediate artifacts that you can access, including the ASSETS artifact used in our Toy example. The Android Gradle plugin provides additional access to different artifacts. For example, when you want to verify the content of an artifact, you can get the AAR artifact through the following code:

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

△ Get AAR artifacts

See the Android developer documentation Variant API, Artifacts, and Tasks for material on the Android Gradle plugin's new Variants and Artifact APIs, which can help you better understand how to interact with intermediate artifacts.

Modify and extend the DSL

Next we need to modify the Android Gradle plugin's DSL to allow us to set the content of additional assets. The new version of the Android Gradle plugin allows you to write additional DSL content for custom plugins, so we'll edit the additional assets for each build type this way. The code below shows our modifications to the module's build.gradle file.

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

△ Add custom DSL in build.gradle

Also, to be able to extend the DSL of the Android Gradle plugin, we need to create a simple interface. You can refer to the following piece of code:

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

△ Define the toyExtension interface

After defining the interface, we need to add the newly defined extension for each build type:

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

△ Add newly defined extensions for all build types

You can also extend the product variant with a custom interface, but we don't need to do that in this example. We also need to make further changes to ToyPlugin.kt so that the plugin can get the content of the asset we defined in the DSL for each variant:

// 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)
        }
 
      // 注意这里省略了修改工件的部分
      // ...
    }
  }
}

△ Use custom DSL in product variants

In the above code, we added a piece of code to get the content defined by the newly added toyExtension, that is, the additional asset defined for each build type when the DSL was modified just now. Need your attention, we define the content of the alternative asset here, that is, when you do not define an asset for a build type, the value will be used by default.

Add custom properties using Variant API

You can also extend the Variant API in a similar way to extending the DSL by adding your own Gradle properties or some kind of Gradle Provider to the Android Gradle plugin's Variant object. Extending the Variant API has several advantages over just extending the DSL:

  1. The DSL values ​​are fixed, but custom variant properties can use the output of the build task, and Gradle will automatically handle all build task dependencies.
  2. You can easily set separate values ​​for each variant's custom variant properties.
  3. Compared to custom DSLs, custom variant properties provide simpler and more robust interactions with other plugins.

When we need to add custom variant properties, we first create a simple interface:

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

△ Define extensions with custom variant attributes (compared to normal extensions)

By comparing with the previous ToyExtension definition, you'll notice that we use a Property instead of a nullable string type. This is done to be consistent with the code conventions inside the Android Gradle plugin, to allow you to use the output of your task as the value of a custom property, and to save you from having to think about the complex plugin sorting process. Other plugins can also set property values, whether it happens before or after the Toy plugin doesn't matter. The following code shows how to use custom properties:

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

△ Use extensions with custom variant properties in build.gradle

While this is not as straightforward as extending the DSL directly, it is convenient to set the value of a custom property for each variant. Correspondingly, you also need to modify the ToyPlugin.kt file:

// 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)
 
      // 注意这里省略了部分内容
      // ...
    }
  }
}

△ Register AGP extensions with custom variant attributes

In this code, we create an instance of ToyVariantExtension, first use the value in the toy DSL as the default value of the Property corresponding to the custom variant attribute, and then register the instance to the variant object. You'll notice that we used beforeVariants instead of onVariants, because variant extensions must be registered in the beforeVariants block so that other plugins in the onVariants block can use the newly registered extensions. Another thing to note is that we get the values ​​in the custom toys DSL in the beforeVariants block, which is actually safe. Because when the beforeVariants callback is called, the value of the DSL is treated as the final result and locked, so there are no additional security issues. After getting the value in the toy DSL, we assign it to the custom variant property and finally register a new extension (ToyVariantExtension) on the variant.

After completing the operations of the beforeVariants block, we can continue to assign the custom variant properties to the task input in the onVariants block. The process is very simple, please refer to the code below:

// 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)
        }
 
      // 注意这里省略了修改工件的部分
      // ...
    }
  }
}

△ Use custom variant properties

The code above is a good example of the advantages of using custom variant properties, especially when you have multiple plugins that need to interact in a variant-specific way. If other plugins also want to set your custom variant properties, or use properties for their build tasks, just use something like the onVariants block above.

If you want to learn more about extending the Android Gradle plugin, stay tuned to our Gradle and AGP build API series . You can also read the Android Developer Documentation : Extending the Android Gradle Plugin or study the AGP Cookbook on GitHub . Stay tuned for more build and sync improvements in the near future.

The next step of the job

Project Isolation

Gradle Project Isolation is a new feature based on configuration caching designed to provide faster builds and syncs. The configuration of each project is isolated from each other, and cross-project references are not allowed, so Gradle can cache the synchronization (sync) results of each project, and whenever the build file changes, only the affected projects will be reconfigured. This feature is currently under development, you gradle.propertiescan org.gradle.unsafe.isolated-projects=truetry this feature by adding a switch to the file (requires Gradle 7.2 and above).

Improve Kotlin incremental compilation

We are also working with JetBrains to improve incremental compilation in Kotlin, with the goal of supporting all incremental compilation scenarios, such as modifying Android resources, adding external dependencies, or modifying non-Kotlin upstream subprojects.

Thank you to all developers for your support, trying out our preview tools and providing feedback on issues. Please continue to pay attention to our progress, and welcome to communicate with us when you encounter problems.

You are welcome to click here to submit feedback to us, or to share what you like, issues you find. Your feedback is very important to us, thank you for your support!

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/androiddevs/blog/5506397