• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

Gradle Receipes (AGP-7.3) 和amp; AGP 使用指南

武飞扬头像
川峰
帮助1

本文所有代码是基于 https://github.com/android/gradle-recipes agp-7.3分支版本的官方 sample。由于缺乏清晰易懂的说明文档(目前我没有找到可读性较好的文档,如果你知道,请留言告知),有些sample代码即便能够跑通,但在细节方便仍然不是很容易让人理解。

从 AGP 7.0 开始,AGP的API相较AGP 7.0 以前的变化较大,且迭代更新比较频繁,目前官方最新已迭代至8.0,请注意对照你所使用的AGP版本。但是据官方的说法是,从 AGP 7.0 之后的API将逐渐趋于稳定,你不用担心升级之后API会变化太大而导致不能使用了。(我信你个鬼)

另外Gradle插件和Android Studio的版本之间存在着兼容要求,请参考这里

我的电脑目前使用的Android Studio的版本是小海豚,所以AGP的要求是7.3,Gradle的要求是至少7.4:
学新通
学新通

Groovy Use

获取所有Class文件

import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.file.RegularFile
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction
import org.gradle.api.provider.ListProperty
import com.android.build.api.artifact.MultipleArtifact

abstract class GetAllClassesTask extends DefaultTask {

    @InputFiles
    abstract ListProperty<Directory> getAllClasses()

    @InputFiles
    abstract ListProperty<RegularFile> getAllJarsWithClasses()

    @TaskAction
    void taskAction() {
        allClasses.get().forEach { directory ->
            println("Directory : ${directory.asFile.absolutePath}")
            directory.asFile.traverse(type: groovy.io.FileType.FILES) { file ->
                println("File : ${file.absolutePath}")
            }
            allJarsWithClasses.get().forEach { file ->
                println("JarFile : ${file.asFile.absolutePath}")
            }
        }
    }
}


androidComponents {
    onVariants(selector().all(), { variant ->
        project.tasks.register(variant.getName()   "GetAllClasses", GetAllClassesTask.class) {
            it.allClasses.set(variant.artifacts.getAll(MultipleArtifact.ALL_CLASSES_DIRS.INSTANCE))
            it.allJarsWithClasses.set(variant.artifacts.getAll(MultipleArtifact.ALL_CLASSES_JARS.INSTANCE))

        }
    })
}
学新通

学新通
执行任务后输出:
学新通

修改Class文件

在根目录build.gradle中引入javassist插件:

buildscript {
    dependencies {
        classpath("org.javassist:javassist:3.22.0-GA")
    }
}
plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
}

javassist是业界知名的用于字节码插桩的AOP框架(它与ASM并称为字节码手术刀,但是ASM使用需要知道更多字节码底层知识,javassist则不需要),关于 javassist 的使用,可以自行搜索相关资料或直接参考其官网教程

app/build.gradle中添加任务:

import org.gradle.api.DefaultTask;
import org.gradle.api.file.Directory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.TaskAction;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.io.FileInputStream;

abstract class ModifyClassesTask extends DefaultTask {

    @InputFiles
    abstract ListProperty<Directory> getAllClasses();

    @OutputFiles
    abstract DirectoryProperty getOutput();

    @TaskAction
    void taskAction() {
        ClassPool pool = new ClassPool(ClassPool.getDefault());
        allClasses.get().forEach { directory ->
            System.out.println("Directory : ${directory.asFile.absolutePath}");
            directory.asFile.traverse(type: groovy.io.FileType.FILES) { file ->
                System.out.println(file.absolutePath)
                if (file.name == "SomeSource.class") {
                    System.out.println("File : ${file.absolutePath}")
                    CtClass interfaceClass = pool.makeInterface("com.android.api.tests.SomeInterface");
                    System.out.println("Adding $interfaceClass")
                    System.out.println("Write to ${output.get().asFile.absolutePath}")
                    interfaceClass.writeFile(output.get().asFile.absolutePath)
                    new FileInputStream(file).withCloseable {
                        CtClass ctClass = pool.makeClass(it)
                        ctClass.addInterface(interfaceClass)
                        CtMethod m = ctClass.getDeclaredMethod("toString")
                        if (m != null) {
                            m.insertBefore("{ System.out.println(\"Some Extensive Tracing\"); }")
                        }
                        ctClass.writeFile(output.get().asFile.absolutePath)
                    }
                }
            }
        }
    }
}

androidComponents {
    onVariants(selector().all(), { variant ->
        TaskProvider<ModifyClassesTask> taskProvider = project.tasks.register(variant.getName()   "ModifyAllClasses", ModifyClassesTask.class)
        variant.artifacts.use(taskProvider)
                .wiredWith( { it.getAllClasses() }, { it.getOutput() })
                .toTransform(MultipleArtifact.ALL_CLASSES_DIRS.INSTANCE)
    })
}
学新通

学新通

新建一个 SomeSource 类:

package com.android.api.tests;

class SomeSource {
    public String toString() {
        return "Something !";
    }
}

执行 assembleDebug 后会自动触发 debugModifyAllClasses 任务被执行
学新通
在build\intermediates\all_classes_dirs\debug\debugModifyAllClasses目录下生成一个SomeInterface.class接口类文件,同时SomeSource.class文件中实现了该接口:

学新通
学新通

添加Class文件

abstract class AddClassesTask extends DefaultTask {

    @OutputFiles
    abstract DirectoryProperty getOutput();

    @TaskAction
    void taskAction() {

        ClassPool pool = new ClassPool(ClassPool.getDefault());
        CtClass interfaceClass = pool.makeInterface("com.android.api.tests.SomeInterface2")
        System.out.println("Adding $interfaceClass")
        interfaceClass.writeFile(output.get().asFile.absolutePath)
    }
}

androidComponents {
    onVariants(selector().all(), { variant ->
        TaskProvider<AddClassesTask> taskProvider = project.tasks.register(variant.getName()   "AddAllClasses", AddClassesTask.class)
        variant.artifacts.use(taskProvider)
                .wiredWith( { it.getOutput() })
                .toAppendTo(MultipleArtifact.ALL_CLASSES_DIRS.INSTANCE)
    })
}
学新通

执行 assembleDebug 后会自动触发 debugAddAllClasses 任务,在 build\intermediates\all_classes_dirs\debug\debugAddAllClasses 目录下生成一个接口类文件SomeInterface2.class.

获取APK文件

import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.BuiltArtifacts
import com.android.build.api.variant.BuiltArtifactsLoader

abstract class DisplayApksTask extends DefaultTask {

    @InputFiles
    abstract DirectoryProperty getApkFolder()

    @Internal
    abstract Property<BuiltArtifactsLoader> getBuiltArtifactsLoader()

    @TaskAction
    void taskAction() {
        BuiltArtifacts artifacts = getBuiltArtifactsLoader().get().load(getApkFolder().get())
        if (artifacts == null) {
            throw new RuntimeException("Cannot load APKs")
        }
        artifacts.elements.forEach {
            println("Got an APK at ${it.outputFile}")
        }
    }
}

androidComponents {
    onVariants(selector().all(), { variant ->
        project.tasks.register(variant.getName()   "DisplayApks", DisplayApksTask.class) {
            it.apkFolder.set(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
            it.builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
        }
    })
}
学新通

学新通
执行 debugDisplayApks 任务,输出:

学新通

修改 ApplicationId

abstract class ApplicationIdProducerTask extends DefaultTask {

    @OutputFile
    abstract RegularFileProperty getOutputFile()

    @TaskAction
    void taskAction() {
        getOutputFile().get().getAsFile().write("set.from.task."   name)
    }
}

androidComponents {
    onVariants(selector().withBuildType("debug")) { variant ->
        TaskProvider appIdProducer = tasks.register(variant.name   "AppIdProducerTask", ApplicationIdProducerTask.class) { task ->
            File outputDir = new File(getBuildDir(), task.name)
            println("outputDir: ${outputDir.absolutePath}")
            task.getOutputFile().set(new File(outputDir, "appId.txt"))

        }
        variant.setApplicationId(appIdProducer.flatMap { task ->
            task.getOutputFile().map { it.getAsFile().text }
        })
    }
}
学新通

执行 assembleDebug 任务,会自动执行 debugAppIdProducerTask 任务,然后运行app,发现BuildConfig.APPLICATION_ID和context.getPackageName() 返回的都是set.from.task.debugAppIdProducerTask。

重写 Manifest 文件

abstract class GitVersionTask extends DefaultTask {

    @OutputFile
    abstract RegularFileProperty getGitVersionOutputFile()

    @TaskAction
    void taskAction() {
        // this would be the code to get the tip of tree version,
        // String gitVersion = "git rev-parse --short HEAD".execute().text.trim()
        // if (gitVersion.isEmpty()) {
        //    gitVersion="12"
        //}
        getGitVersionOutputFile().get().asFile.write("1234")
    }
}

abstract class ManifestProducerTask extends DefaultTask {
    @InputFile
    abstract RegularFileProperty getGitInfoFile()

    @OutputFile
    abstract RegularFileProperty getOutputManifest()

    @TaskAction
    void taskAction() {
        String manifest = """<?xml version="1.0" encoding="utf-8"?>
                <manifest xmlns:android="http://schemas.android.com/apk/res/android"
                    package="com.android.build.example.minimal"
                    android:versionName="${new String(getGitInfoFile().get().asFile.readBytes())}"
                    android:versionCode="1" >

                    <application android:label="Minimal">
                        <activity android:name="MainActivity">
                            <intent-filter>
                                <action android:name="android.intent.action.MAIN" />
                                <category android:name="android.intent.category.LAUNCHER" />
                            </intent-filter>
                        </activity>
                    </application>
                </manifest>
                    """
        println("Writes to "   getOutputManifest().get().getAsFile().getAbsolutePath())
        getOutputManifest().get().getAsFile().write(manifest)
    }
}

androidComponents {
    onVariants(selector().all(), {
        TaskProvider gitVersionProvider = tasks.register(it.getName()   'GitVersionProvider', GitVersionTask) {
            task ->
                task.gitVersionOutputFile.set(
                        new File(project.buildDir, "intermediates/gitVersionProvider/output")
                )
                task.outputs.upToDateWhen { false }
        }

        TaskProvider manifestProducer = tasks.register(it.getName()   'ManifestProducer', ManifestProducerTask) {
            task ->
                task.gitInfoFile.set(gitVersionProvider.flatMap { it.getGitVersionOutputFile() })
        }
        it.artifacts.use(manifestProducer)
                .wiredWith({ it.outputManifest })
                .toCreate(SingleArtifact.MERGED_MANIFEST.INSTANCE)
    })
}
学新通

学新通
执行 assembleDebug 后会自动触发 debugManifestProducer 任务,而 debugManifestProducer 任务的输入依赖于 debugGitVersionProvider 的输出,因此 debugGitVersionProvider 任务先被执行,在 build/intermediates/gitVersionProvider/output 文件中生成版本号。然后会执行 debugManifestProducer 任务,在 build\intermediates\merged_manifest\debug\debugManifestProducer 目录下生成被覆写的AndroidManifest.xml 文件,其中android:versionName的值被替换为上面output 文件中生成的版本号:

学新通

<?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.build.example.minimal"
        android:versionName="1234"
        android:versionCode="1" >
        <application android:label="Minimal">
            <activity android:name="MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    </manifest>

修改 Manifest 文件内容

abstract class GitVersionTask extends DefaultTask {

    @OutputFile
    abstract RegularFileProperty getGitVersionOutputFile()

    @TaskAction
    void taskAction() {
        // this would be the code to get the tip of tree version,
        // String gitVersion = "git rev-parse --short HEAD".execute().text.trim()
        // if (gitVersion.isEmpty()) {
        //    gitVersion="12"
        //}
        getGitVersionOutputFile().get().asFile.write("8888")
    }
}

abstract class ManifestTransformerTask extends DefaultTask {

    @InputFile
    abstract RegularFileProperty getGitInfoFile()

    @InputFile
    abstract RegularFileProperty getMergedManifest()

    @OutputFile
    abstract RegularFileProperty getUpdatedManifest()

    @TaskAction
    void taskAction() {
        String gitVersion = new String(getGitInfoFile().get().asFile.readBytes())
        String manifest = new String(getMergedManifest().get().asFile.readBytes())
        manifest = manifest.replace("android:versionCode=\"1\"",
                "android:versionCode=\""  gitVersion  "\"")
        println("Writes to "   getUpdatedManifest().get().asFile.getAbsolutePath())
        getUpdatedManifest().get().asFile.write(manifest)
    }
}


import com.android.build.api.artifact.SingleArtifact

TaskProvider gitVersionProvider = tasks.register('gitVersionProvider', GitVersionTask) {
    task ->
        task.gitVersionOutputFile.set(
                new File(project.buildDir, "intermediates/gitVersionProvider/output")
        )
        task.outputs.upToDateWhen { false }
}

androidComponents {
    onVariants(selector().all(), {
        TaskProvider manifestUpdater = tasks.register(it.getName()   'ManifestUpdater', ManifestTransformerTask) {
            task ->
                task.gitInfoFile.set(gitVersionProvider.flatMap { it.getGitVersionOutputFile() })
        }
        it.artifacts.use(manifestUpdater)
                .wiredWithFiles(
                        { it.mergedManifest },
                        { it.updatedManifest })
                .toTransform(SingleArtifact.MERGED_MANIFEST.INSTANCE)
    })
}
学新通

学新通
学新通
学新通

执行 assembleDebug 后会自动触发 debugManifestUpdater 任务,而 debugManifestUpdater 任务的输入依赖于 gitVersionProvider 任务的输出,因此 gitVersionProvider 任务先被执行,在 build/intermediates/gitVersionProvider/output 文件中生成版本号。然后会执行 debugManifestUpdater 任务,在 build\intermediates\merged_manifest\debug\目录下生成被覆写的AndroidManifest.xml 文件,其中android:versionCode的值被替换为上面output 文件中生成的版本号:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.build.example.minimal"
    android:versionCode="8888"
    android:versionName="1.0.0" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="33" />

    <application
        android:debuggable="true"
        android:label="Minimal" >
        <activity
            android:name="com.android.build.example.minimal.MainActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
学新通

拷贝apk

import com.android.build.api.artifact.ArtifactTransformationRequest
import com.android.build.api.variant.BuiltArtifact

import javax.inject.Inject
import java.nio.file.Files

interface WorkItemParameters extends WorkParameters {
    RegularFileProperty getInputApkFile()
    RegularFileProperty getOutputApkFile()
}

abstract class WorkItem implements WorkAction<WorkItemParameters> {

    WorkItemParameters workItemParameters

    @Inject
    WorkItem(WorkItemParameters parameters) {
        this.workItemParameters = parameters
    }

    void execute() {
        workItemParameters.getOutputApkFile().get().getAsFile().delete()
        Files.copy(
                workItemParameters.getInputApkFile().getAsFile().get().toPath(),
                workItemParameters.getOutputApkFile().get().getAsFile().toPath())
    }
}

abstract class CopyApksTask extends DefaultTask {

    private WorkerExecutor workers

    @Inject
    CopyApksTask(WorkerExecutor workerExecutor) {
        this.workers = workerExecutor
    }

    @InputFiles
    abstract DirectoryProperty getApkFolder()

    @OutputDirectory
    abstract DirectoryProperty getOutFolder()

    @Internal
    abstract Property<ArtifactTransformationRequest<CopyApksTask>> getTransformationRequest()

    @TaskAction
    void taskAction() {

        transformationRequest.get().submit(this, workers.noIsolation(), WorkItem, {
            BuiltArtifact builtArtifact, Directory outputLocation, WorkItemParameters param ->
                File inputFile = new File(builtArtifact.outputFile)
                param.getInputApkFile().set(inputFile)
                param.getOutputApkFile().set(new File(outputLocation.asFile, inputFile.name))
                param.getOutputApkFile().get().getAsFile()
        }
        )
    }
}

import com.android.build.api.artifact.SingleArtifact

androidComponents {

    onVariants(selector().all(), { variant ->
        TaskProvider copyApksProvider = tasks.register('copy'   variant.getName()   'Apks', CopyApksTask)

        ArtifactTransformationRequest request =
                variant.artifacts.use(copyApksProvider)
                        .wiredWithDirectories(
                                { it.getApkFolder() },
                                { it.getOutFolder()})
                        .toTransformMany(SingleArtifact.APK.INSTANCE)

        copyApksProvider.configure {
            it.transformationRequest.set(request)
        }
    })
}
学新通

执行 assembleDebug 后会自动触发 copydebugApks 任务被执行,apk 文件从 app\build\outputs\apk\debug\ 目录被拷贝到了 app\build\intermediates\apk\copydebugApks 目录下,但是说实话,这个代码我很懵逼,我真是没看懂。。。

官方的这个例子可能主要是想演示Transformation的使用,如果只是想要拷贝apk文件,可以使用更加简单的写法(参见下文buildSrc部分的拷贝apk文件部分)。

禁用 UnitTest

androidComponents {
    beforeVariants(selector().all(), { variantBuilder ->
        variantBuilder.enableUnitTest = false
    })
    onVariants(selector().withName("debug"), { variant ->
        if (variant.unitTest != null) {
            throw new RuntimeException("UnitTest is active while it was deactivated")
        }
        if (variant.androidTest == null) {
            throw new RuntimeException("AndroidTest is not active, it should be")
        }
    })
}

禁用 AndroidTest

androidComponents {
    beforeVariants(selector().withName("debug"), { variantBuilder ->
        variantBuilder.enableAndroidTest = false
    })
    onVariants(selector().withName("debug"), { variant ->
        if (variant.unitTest == null) {
            throw new RuntimeException("Unit test is not active, it should be")
        }
        if (variant.androidTest != null) {
            throw new RuntimeException("AndroidTest is active while it was deactivated")
        }
    })
}

以上是官方sample中的Groovy示例。

Kotlin Use

以下是官方sample中的Kotlin示例。

Gradle 文件 kts 版本配置

首先将gradle文件配置成kotlin版本的形式(可自行搜索如何从 Groovy 迁移到 KTS相关资料,或者直接参考官网)。迁移到KTS的好处是终于有代码提示了,且所有类型都可以点击跳转查看源码,可读性好一些,但是编译速度会慢一些。

settings.gradle.kts:


pluginManagement {
    repositories {
        mavenCentral()
        // 阿里云镜像
        maven(url = "https://maven.aliyun.com/repository/central")
        maven(url = "https://maven.aliyun.com/repository/public")
        maven(url = "https://maven.aliyun.com/repository/jcenter")
        maven(url = "https://maven.aliyun.com/repository/谷歌")
        maven(url = "https://maven.aliyun.com/repository/releases")
        maven(url = "https://maven.aliyun.com/repository/snapshots")
        maven(url = "https://maven.aliyun.com/repository/gradle-plugin")
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
       // 阿里云镜像
        maven(url = "https://maven.aliyun.com/repository/central")
        maven(url = "https://maven.aliyun.com/repository/public")
        maven(url = "https://maven.aliyun.com/repository/jcenter")
        maven(url = "https://maven.aliyun.com/repository/谷歌")
        maven(url = "https://maven.aliyun.com/repository/releases")
        maven(url = "https://maven.aliyun.com/repository/snapshots")
        maven(url = "https://maven.aliyun.com/repository/gradle-plugin")
    }
}

include(":app")

rootProject.name = "getApksTest" 
学新通

根目录下build.gradle.kts:

buildscript {
    dependencies {
        classpath("com.android.tools.build:gradle:7.3.1")
    }
}
plugins {
//    id("com.android.application") version "7.3.1" apply false
//    id("com.android.library") version "7.3.1" apply false
    id("org.jetbrains.kotlin.android") version "1.5.31" apply false
}

(gradle-wrapper.properties中使用的是gradle-7.5)

app目录下的build.gradle.kts:

plugins {
    id("com.android.application")
    kotlin("android")
}
android {
    compileSdk = 33
    defaultConfig {
    	applicationId = "com.android.build.example.minimal"
        minSdk = 21
		targetSdk = 33
        ...
    }
    ...
}

获取 apk 文件

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction
import com.android.build.api.variant.BuiltArtifactsLoader
import com.android.build.api.artifact.SingleArtifact
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal

abstract class DisplayApksTask : DefaultTask() {

    @get:InputFiles
    abstract val apkFolder: DirectoryProperty

    @get:Internal
    abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>

    @TaskAction
    fun taskAction() { 
        val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
            ?: throw RuntimeException("Cannot load APKs")
        builtArtifacts.elements.forEach {
            println("Got an APK at ${it.outputFile}")
        }
    }
}

androidComponents {
    onVariants { variant ->
        project.tasks.register<DisplayApksTask>("${variant.name}DisplayApks") {
            apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
            builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
        }
    }
}
学新通

执行 debugDisplayApks 任务,输出:

学新通

获取 aar 文件

新建一个library module,然后在其 build.gradle.kts 中添加:

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction

import com.android.build.api.variant.BuiltArtifactsLoader
import com.android.build.api.artifact.SingleArtifact
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal

abstract class AarUploadTask: DefaultTask() {

    @get:InputFile
    abstract val aar: RegularFileProperty

    @TaskAction
    fun taskAction() {
        println("Uploading ${aar.get().asFile.absolutePath} to fantasy server...")
    }
}
androidComponents {
    onVariants { variant ->
        project.tasks.register<AarUploadTask>("${variant.name}AarUpload") {
            aar.set(variant.artifacts.get(SingleArtifact.AAR))
        }
    }
}
学新通

执行 debugAarUpload 任务,输出:

学新通

动态添加 Build Type

androidComponents.finalizeDsl { extension ->
    extension.buildTypes.create("extra").let {
        it.isJniDebuggable = true
    }
}

学新通

动态添加 BuildConfigField

import com.android.build.api.variant.BuildConfigField
androidComponents {
   onVariants {
       it.buildConfigFields.put("FloatValue", BuildConfigField("Float", "1f", "Float Value" ))
       it.buildConfigFields.put("LongValue", BuildConfigField("Long", "1L", "Long Value" ))
       it.buildConfigFields.put("VariantName", BuildConfigField("String", "\"${name}\"", "Variant Name" ))
   }
}

build 后生成 BuildConfig 类中新增三个常量字段:

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.android.build.example.minimal";
  public static final String BUILD_TYPE = "debug";
  // Float Value
  public static final Float FloatValue = 1f;
  // Long Value
  public static final Long LongValue = 1L;
  // Variant Name
  public static final String VariantName = "app";
}

根据其他任务的输出来生成 BuildConfigField 字段

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.BuildConfigField

abstract class GitVersionTask: DefaultTask() {

    @get:OutputFile
    abstract val gitVersionOutputFile: RegularFileProperty

    @ExperimentalStdlibApi
    @TaskAction
    fun taskAction() {

        // this would be the code to get the tip of tree version,
        // val firstProcess = ProcessBuilder("git","rev-parse --short HEAD").start()
        // val error = firstProcess.errorStream.readBytes().decodeToString()
        // if (error.isNotBlank()) {
        //      System.err.println("Git error : $error")
        // }
        // var gitVersion = firstProcess.inputStream.readBytes().decodeToString()

        // but here, we are just hardcoding :
        gitVersionOutputFile.get().asFile.writeText("1234")
    }
}

val gitVersionProvider = tasks.register<GitVersionTask>("gitVersionProvider") {
    File(project.buildDir, "intermediates/gitVersionProvider/output").also {
        it.parentFile.mkdirs()
        gitVersionOutputFile.set(it)
    }
    outputs.upToDateWhen { false }
}

androidComponents {
    onVariants {
        it.buildConfigFields.put("GitVersion", gitVersionProvider.map {  task ->
            BuildConfigField(
                "String",
                "\"${task.gitVersionOutputFile.get().asFile.readText(Charsets.UTF_8)}\"",
                "Git Version")
        })
    }
}
学新通

执行 build 构建后,会先执行 gitVersionProvider 任务,在build目录下生成版本号:

学新通
然后会生成 BuildConfigField 字段,在 BuildConfig 类中发现新增 GitVersion 字段:

public final class BuildConfig {
  public static final boolean DEBUG = Boolean.parseBoolean("true");
  public static final String APPLICATION_ID = "com.android.build.example.minimal";
  public static final String BUILD_TYPE = "debug"; 
  // Git Version
  public static final String GitVersion = "1234"; 
} 

修改 Manifest 文件中的占位符

import com.android.build.api.artifact.SingleArtifact

abstract class ManifestReaderTask: DefaultTask() {

    @get:InputFile
    abstract val mergedManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {
        val manifest = mergedManifest.asFile.get().readText()
        // ensure that merged manifest contains the right activity name.
        if (!manifest.contains("activity android:name=\"com.android.build.example.minimal.MyRealName\""))
            throw RuntimeException("Manifest Placeholder not replaced successfully")
        else
            println(manifest)     
    }
}

androidComponents {
    onVariants {
        tasks.register<ManifestReaderTask>("${it.name}ManifestReader") {
            mergedManifest.set(it.artifacts.get(SingleArtifact.MERGED_MANIFEST))
        }
        it.manifestPlaceholders.put("MyName", "MyRealName")
    }
}
学新通

执行任务前,AndroidManifest.xml内容如下:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
 package="com.android.build.example.minimal">
 <application android:label="Minimal">
     <activity android:name="MainActivity"
         android:exported="true"
         >
         <intent-filter>
             <action android:name="android.intent.action.MAIN" />
             <category android:name="android.intent.category.LAUNCHER" />
         </intent-filter>
     </activity>
     <activity android:name="${MyName}"
         >
     </activity>
 </application>
</manifest>
学新通

执行任务后,在 build\intermediates\merged_manifest\debug\AndroidManifest.xml 中 ${MyName} 被替换为MyRealName:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.build.example.minimal" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="33" />

    <application
        android:debuggable="true"
        android:label="Minimal" >
        <activity
            android:name="com.android.build.example.minimal.MainActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.android.build.example.minimal.MyRealName" >
        </activity>
    </application>

</manifest>
学新通

根据其他任务的输出来修改 Manifest 文件的占位符

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import com.android.build.api.artifact.SingleArtifact

abstract class StringProducerTask: DefaultTask() {

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @ExperimentalStdlibApi
    @TaskAction
    fun taskAction() {
        outputFile.get().asFile.writeText("MyActivity")
    }
}


val androidNameProvider = tasks.register<StringProducerTask>("androidNameProvider") {
    File(project.buildDir, "intermediates/androidNameProvider/output").also {
        it.parentFile.mkdirs()
        outputFile.set(it)
    }
    outputs.upToDateWhen { false }
}

abstract class ManifestReaderTask: DefaultTask() {

    @get:InputFile
    abstract val mergedManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {
        val manifest = mergedManifest.asFile.get().readText()
        // ensure that merged manifest contains the right activity name.
        if (!manifest.contains("activity android:name=\"com.android.build.example.minimal.MyActivity\""))
            throw RuntimeException("Manifest Placeholder not replaced successfully")
        else
            println(manifest)
    }
}

androidComponents {
    onVariants {
        tasks.register<ManifestReaderTask>("${it.name}ManifestReader") {
            mergedManifest.set(it.artifacts.get(SingleArtifact.MERGED_MANIFEST))
        }
        // it.manifestPlaceholders.put("MyName", "MyRealName")
        it.manifestPlaceholders.put("MyName", androidNameProvider.flatMap { task ->
            task.outputFile.map { it.asFile.readText(Charsets.UTF_8) }
        })
    }
}
学新通

由于 manifestPlaceholders 的输入依赖 androidNameProvider 任务的输出,因此执行build后,会先执行 androidNameProvider 任务,在 intermediates/androidNameProvider/output文件中生成"MyActivity"字符串。然后执行debugManifestReader任务,执行任务后,在 build\intermediates\merged_manifest\debug\AndroidManifest.xml 中的 ${MyName} 被替换为MyActivity:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.build.example.minimal" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="33" />

    <application
        android:debuggable="true"
        android:label="Minimal" >
        <activity
            android:name="com.android.build.example.minimal.MainActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.android.build.example.minimal.MyActivity" >
        </activity>
    </application>

</manifest>
学新通

添加自定义 ResValue 字段

import com.android.build.api.variant.ResValue

androidComponents {
    onVariants { variant ->
        variant.resValues.put(variant.makeResValueKey("string", "VariantName"),
            ResValue(name, "Variant Name"))
    }
}

执行build后,在build\generated\res\resValues\debug\values\gradleResValues.xml中生成名为VariantName的string资源:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- Automatically generated file. DO NOT MODIFY -->

    <!-- Variant Name -->
    <string name="VariantName" translatable="false">app</string>

</resources>

可以在Activity中直接使用:

class MainActivity : Activity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState) 
        val label = TextView(this) 
        label.text = "Hello ${R.string.VariantName}"
        setContentView(label)
    }
}

根据其他任务的输出来生成 ResValue 字段

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.variant.ResValue

abstract class GitVersionTask: DefaultTask() {

    @get:OutputFile
    abstract val gitVersionOutputFile: RegularFileProperty

    @ExperimentalStdlibApi
    @TaskAction
    fun taskAction() {

        // this would be the code to get the tip of tree version,
        // val firstProcess = ProcessBuilder("git","rev-parse --short HEAD").start()
        // val error = firstProcess.errorStream.readBytes().decodeToString()
        // if (error.isNotBlank()) {
        //      System.err.println("Git error : $error")
        // }
        // var gitVersion = firstProcess.inputStream.readBytes().decodeToString()

        // but here, we are just hardcoding :
        gitVersionOutputFile.get().asFile.writeText("1234")
    }
}

val gitVersionProvider = tasks.register<GitVersionTask>("gitVersionProvider") {
    File(project.buildDir, "intermediates/gitVersionProvider/output").also {
        it.parentFile.mkdirs()
        gitVersionOutputFile.set(it)
    }
    outputs.upToDateWhen { false }
}

androidComponents {
    onVariants { variant ->
        variant.resValues.put(variant.makeResValueKey("string", "GitVersion"),
            gitVersionProvider.map {  task ->
                ResValue(task.gitVersionOutputFile.get().asFile.readText(Charsets.UTF_8), "git version")
            })
    }
}
学新通

执行build后,会先触发 gitVersionProvider 任务执行,在build\generated\res\resValues\debug\values\gradleResValues.xml中生成名为GitVersion的string资源:

<?xml version="1.0" encoding="utf-8"?>
<resources>

    <!-- Automatically generated file. DO NOT MODIFY -->

    <!-- Variant Name -->
    <string name="VariantName" translatable="false">app</string>
    <!-- git version -->
    <string name="GitVersion" translatable="false">1234</string>

</resources>

动态添加Asset文件

abstract class AssetCreatorTask: DefaultTask() {
    @get:OutputFiles
    abstract val outputDirectory: DirectoryProperty

    @ExperimentalStdlibApi
    @TaskAction
    fun taskAction() {
        outputDirectory.get().asFile.mkdirs()
        File(outputDirectory.get().asFile, "custom_asset.txt")
            .writeText("some real asset file")
    }
}

androidComponents {
    onVariants(selector().withBuildType("debug")) { variant ->
        val assetCreationTask = project.tasks.register<AssetCreatorTask>("create${variant.name}Asset")
        variant.sources.assets?.addGeneratedSourceDirectory(
            assetCreationTask,
            AssetCreatorTask::outputDirectory)
    }
}
学新通

执行 assembleDebug 后会自动触发 createdebugAsset 任务执行,会在build\ASSETS\createdebugAsset目录下生成一个custom_asset.txt文件,该文件会被打包到Apk的assets资源目录中。

学新通

注册新的source源文件类型

androidComponents {
    registerSourceType("toml")
}

同步之后,src/debug/toml目录可以被项目工程识别。

学新通

添加自定义类型的源文件

abstract class AddCustomSources: DefaultTask() {

    @get:OutputDirectory
    abstract val outputFolder: DirectoryProperty

    @TaskAction
    fun taskAction() {
        val outputFile = File(outputFolder.asFile.get(), "com/foo/bar.toml")
        outputFile.parentFile.mkdirs()
        println("writes to ${outputFile.parentFile.absolutePath}")
        outputFile.writeText("""
            [clients]
            data = [ ["gamma", "delta"], [1, 2] ]
        """)
    }
}

abstract class DisplayAllSources: DefaultTask() {

    @get:InputFiles
    abstract val sourceFolders: ListProperty<Directory>

    @TaskAction
    fun taskAction() {
        sourceFolders.get().forEach { directory ->
            println("--> Got a Directory $directory")
            println("<-- done")
        }
    }
}

androidComponents {
    onVariants { variant ->
        val addSourceTaskProvider =  project.tasks.register<AddCustomSources>("${variant.name}AddCustomSources") {
            outputFolder.set(File(project.layout.buildDirectory.asFile.get(), "toml/gen"))
        }

        variant.sources.getByName("toml").also {
            it.addStaticSourceDirectory("src/${variant.name}/toml")
            it.addGeneratedSourceDirectory(addSourceTaskProvider, AddCustomSources::outputFolder)
        }
        println(variant.sources.getByName("toml"))

        project.tasks.register<DisplayAllSources>("${variant.name}DisplayAllSources") {
            sourceFolders.set(variant.sources.getByName("toml").all)
        }
    }
}
学新通

执行 debugDisplayAllSources 任务会触发 debugAddCustomSources 任务先执行,会在 build\toml\debugAddCustomSources目录下生成一个toml类型文件:

学新通
然后会输出显示所有包含toml类型的源文件目录:
学新通

注册新的源文件类型同时添加新的源文件

就是将前面两种结合起来一起使用

abstract class AddCustomSources: DefaultTask() {

    @get:OutputDirectory
    abstract val outputFolder: DirectoryProperty

    @TaskAction
    fun taskAction() {
        val outputFile = File(outputFolder.asFile.get(), "com/foo/bar.toml")
        outputFile.parentFile.mkdirs()
        println("writes to ${outputFile.parentFile.absolutePath}")
        outputFile.writeText("""
            [clients]
            data = [ ["gamma", "delta"], [1, 2] ]
        """)
    }
}

abstract class DisplayAllSources: DefaultTask() {

    @get:InputFiles
    abstract val sourceFolders: ListProperty<Directory>

    @TaskAction
    fun taskAction() {
        sourceFolders.get().forEach { directory ->
            println("--> Got a Directory $directory")
            println("<-- done")
        }
    }
}
androidComponents {
    registerSourceType("toml")
    onVariants { variant ->
        val addSourceTaskProvider =  project.tasks.register<AddCustomSources>("${variant.name}AddCustomSources") {
            outputFolder.set(File(project.layout.buildDirectory.asFile.get(), "toml/gen"))
        }
        File(project.projectDir, "third_party/${variant.name}/toml").mkdirs()

        variant.sources.getByName("toml").also {
            it.addStaticSourceDirectory("third_party/${variant.name}/toml")
            it.addGeneratedSourceDirectory(addSourceTaskProvider, AddCustomSources::outputFolder)
        }
        println(variant.sources.getByName("toml"))

        project.tasks.register<DisplayAllSources>("${variant.name}DisplayAllSources") {
            sourceFolders.set(variant.sources.getByName("toml").all)
        }
    }
}
学新通

Merge自定义类型源文件

abstract class MergeTomlSources: DefaultTask() {

    @get:InputFiles
    abstract val sourceFolders: ListProperty<Directory>

    @get:OutputDirectory
    abstract val mergedFolder: DirectoryProperty

    @TaskAction
    fun taskAction() {
        sourceFolders.get().forEach { directory ->
            println("--> Got a Directory $directory")
            directory.asFile.walk().forEach { sourceFile ->
                println("Source: "   sourceFile.absolutePath)
            }
            println("<-- done")
        }
    }
}

abstract class ConsumeMergedToml: DefaultTask() {

    @get:InputDirectory
    abstract val mergedFolder: DirectoryProperty

    @TaskAction
    fun taskAction() {
        println("Merged folder is "   mergedFolder.get().asFile)
    }
}


androidComponents {
    registerSourceType("toml")
    onVariants { variant ->
        val outFolder = project.layout.buildDirectory.dir("intermediates/${variant.name}/merged_toml")
        val mergingTask = project.tasks.register<MergeTomlSources>("${variant.name}MergeTomlSources") {
            sourceFolders.set(variant.sources.getByName("toml").all)
            mergedFolder.set(outFolder)
        }
        project.tasks.register<ConsumeMergedToml>("${variant.name}ConsumeMergedToml") {
            mergedFolder.set(mergingTask.flatMap { it.mergedFolder })
        }
    }
}
学新通

官方的这个例子只是进行了相关文件目录的输出,并没有真的进行Merge操作。不知道有什么用。。。

添加Java源文件

import org.gradle.api.DefaultTask
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction

abstract class DisplayAllSources: DefaultTask() {

    @get:InputFiles
    abstract val sourceFolders: ListProperty<Directory>

    @TaskAction
    fun taskAction() {

        sourceFolders.get().forEach { directory ->
            println(">>> Got a Directory $directory")
            println("<<<")
        }
    }
}

androidComponents {
    onVariants { variant ->
//        variant.sources.getByName("java").also {
//            it.addStaticSourceDirectory("custom/src/java/${variant.name}")
//        }
        variant.sources.java?.let { java ->
            java.addStaticSourceDirectory("custom/src/java/${variant.name}")
            project.tasks.register<DisplayAllSources>("${variant.name}DisplayAllSources") {
                sourceFolders.set(java.all)
            }
        }
    }
}
学新通

学新通
同步以后,位于与src同级的custom目录下的java源码文件会被打包到apk中。

执行 debugDisplayAllSources 任务,输出的java源文件目录包含custom目录下的java源码文件目录

学新通

添加其他任务输出的java源文件

import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.file.FileTree
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Internal

abstract class AddJavaSources: DefaultTask() {

    @get:OutputDirectory
    abstract val outputFolder: DirectoryProperty

    @TaskAction
    fun taskAction() {
        val outputFile = File(outputFolder.asFile.get(), "com/foo/Bar.java")
        outputFile.parentFile.mkdirs()
        println("writes to ${outputFile.parentFile.absolutePath}")
        outputFile.writeText("""
        package com.foo;

        public class Bar {
            public String toString() {
                return "a Bar instance";
            }
        }
        """)
    }
}

abstract class DisplayAllSources: DefaultTask() {

    @get:InputFiles
    abstract val sourceFolders: ListProperty<Directory>

    @TaskAction
    fun taskAction() {
        sourceFolders.get().forEach { directory ->
            println("--> Got a Directory $directory")
            println("<-- done")
        }
    }
}

androidComponents {
    onVariants { variant ->
        val addSourceTaskProvider =  project.tasks.register<AddJavaSources>("${variant.name}AddJavaSources") {
            outputFolder.set(project.layout.buildDirectory.dir("gen"))
        }

        variant.sources.java?.let { java ->
            java.addGeneratedSourceDirectory(addSourceTaskProvider, AddJavaSources::outputFolder)

            project.tasks.register<DisplayAllSources>("${variant.name}DisplayAllSources") {
                sourceFolders.set(java.all)
            }
        }
    }
}
学新通

执行 assembleDebug 会触发 debugAddJavaSources 任务执行,会在build\JAVA\debugAddJavaSources目录下创建一个java文件,该java文件会作为源文件被打包进apk中。

学新通
执行 debugDisplayAllSources 任务,输出的源文件目录会包含build\JAVA\debugAddJavaSources目录
学新通

修改 minSdkVersion

import com.android.build.api.variant.AndroidVersion

androidComponents {
    beforeVariants(selector().withName("release")) { variantBuilder ->
        variantBuilder.minSdk = 23
    }
}

修改 ApplicationId

abstract class ApplicationIdProducerTask: DefaultTask() {

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun taskAction() {
        outputFile.get().asFile.writeText("set.from.task.$name")
    }
}

androidComponents {
    onVariants(selector().withBuildType("debug")) { variant ->
        val appIdProducer = tasks.register<ApplicationIdProducerTask>("${variant.name}AppIdProducerTask") {
            File(buildDir, name).also {
                outputFile.set(File(it, "appId.txt"))
            }

        }
        // variant.applicationId.set("com.my.set.aaa")
        variant.applicationId.set(appIdProducer.flatMap { task ->
            task.outputFile.map { it.asFile.readText() }
        })
    }
}
学新通

执行 assembleDebug 任务,会自动执行 debugAppIdProducerTask 任务, 然后运行app,发现BuildConfig.APPLICATION_ID和context.getPackageName() 返回的都是set.from.task.debugAppIdProducerTask。

获取所有Class文件

import com.android.build.api.artifact.MultipleArtifact

import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction

abstract class GetAllClassesTask: DefaultTask() {

    @get:InputFiles
    abstract val allClasses: ListProperty<Directory>

    @get:InputFiles
    abstract val allJarsWithClasses: ListProperty<RegularFile>

    @TaskAction
    fun taskAction() {
        allClasses.get().forEach { directory ->
            println("Directory : ${directory.asFile.absolutePath}")
            directory.asFile.walk().filter(File::isFile).forEach { file ->
                println("File : ${file.absolutePath}")
            }
        }
        allJarsWithClasses.get().forEach { file ->
            println("JarFile : ${file.asFile.absolutePath}")
        }
    }
}
androidComponents {
    onVariants { variant ->
        project.tasks.register<GetAllClassesTask>("${variant.name}GetAllClasses") {
            allClasses.set(variant.artifacts.getAll(MultipleArtifact.ALL_CLASSES_DIRS))
            allJarsWithClasses.set(variant.artifacts.getAll(MultipleArtifact.ALL_CLASSES_JARS))
        }
    }
}
学新通

执行 debugGetAllClasses 任务,输出:

学新通

添加Class文件

根目录下build.gradle.kts中添加javassist依赖:

buildscript {
    dependencies {
        classpath("org.javassist:javassist:3.22.0-GA")
    }
}

app/build.gradle.kts中:

import com.android.build.api.artifact.MultipleArtifact

import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction
import javassist.ClassPool
import javassist.CtClass
import java.io.FileInputStream

abstract class AddClassesTask: DefaultTask() {

    @get:OutputFiles
    abstract val output: DirectoryProperty

    @TaskAction
    fun taskAction() {
        val pool = ClassPool(ClassPool.getDefault())
        val interfaceClass = pool.makeInterface("com.android.api.tests.SomeInterface")
        println("Adding $interfaceClass to ${output.get().asFile.absolutePath} ")
        interfaceClass.writeFile(output.get().asFile.absolutePath)
    }
}

androidComponents {
    onVariants { variant ->
        val taskProvider = project.tasks.register<AddClassesTask>("${variant.name}AddClasses")
        variant.artifacts.use(taskProvider)
            .wiredWith(AddClassesTask::output)
            .toAppendTo(MultipleArtifact.ALL_CLASSES_DIRS)
    }
}
学新通

执行 assembleDebug 后会自动触发 debugAddClasses 任务的执行,在build\intermediates\all_classes_dirs\debug\debugAddClasses 目录下生成一个SomeInterface.class文件。

学新通

修改Class文件

import com.android.build.api.artifact.MultipleArtifact

import org.gradle.api.DefaultTask
import org.gradle.api.file.Directory
import org.gradle.api.provider.ListProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.TaskAction
import javassist.ClassPool
import javassist.CtClass
import java.io.FileInputStream

abstract class ModifyClassesTask: DefaultTask() {

    @get:InputFiles
    abstract val allClasses: ListProperty<Directory>

    @get:OutputFiles
    abstract val output: DirectoryProperty

    @TaskAction
    fun taskAction() {
        val pool = ClassPool(ClassPool.getDefault())
        allClasses.get().forEach { directory ->
            println("Directory : ${directory.asFile.absolutePath}")
            directory.asFile.walk().filter(File::isFile).forEach { file ->
                if (file.name == "SomeSource.class") {
                    println("File : ${file.absolutePath}")
                    val interfaceClass = pool.makeInterface("com.android.api.tests.SomeInterface");
                    println("Adding $interfaceClass to ${output.get().asFile.absolutePath}")
                    interfaceClass.writeFile(output.get().asFile.absolutePath)
                    FileInputStream(file).use {
                        val ctClass = pool.makeClass(it)
                        ctClass.addInterface(interfaceClass)
                        ctClass.getDeclaredMethod("toString")
                            ?.insertBefore("{ System.out.println(\"Some Extensive Tracing\"); }")
                        ctClass.writeFile(output.get().asFile.absolutePath)
                    }
                }
            }
        }
    }
}

androidComponents {
    onVariants { variant ->
        val taskProvider = project.tasks.register<ModifyClassesTask>("${variant.name}ModifyClasses")
        variant.artifacts.use(taskProvider)
            .wiredWith(ModifyClassesTask::allClasses, ModifyClassesTask::output)
            .toTransform(MultipleArtifact.ALL_CLASSES_DIRS)
    }
}
学新通

在 src/main中新建一个SomeSource的java类:

学新通

package com.android.api.tests;

class SomeSource {
    public String toString() {
        return "Something !";
    }
}

执行 assembleDebug 后会自动触发 debugModifyClasses 任务的执行,在build\intermediates\all_classes_dirs\debug\debugModifyClasses下生成SomeInterface.class文件,同时SomeSource.class文件内容被修改:

学新通

学新通

重写 Manifest 文件

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import com.android.build.api.artifact.SingleArtifact

abstract class GitVersionTask: DefaultTask() {

    @get:OutputFile
    abstract val gitVersionOutputFile: RegularFileProperty

    @ExperimentalStdlibApi
    @TaskAction
    fun taskAction() {

        // this would be the code to get the tip of tree version,
        // val firstProcess = ProcessBuilder("git","rev-parse --short HEAD").start()
        // val error = firstProcess.errorStream.readBytes().decodeToString()
        // if (error.isNotBlank()) {
        //      System.err.println("Git error : $error")
        // }
        // var gitVersion = firstProcess.inputStream.readBytes().decodeToString()

        // but here, we are just hardcoding :
        gitVersionOutputFile.get().asFile.writeText("1234")
    }
}


abstract class ManifestProducerTask: DefaultTask() {
    @get:InputFile
    abstract val gitInfoFile: RegularFileProperty

    @get:OutputFile
    abstract val outputManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {

        val gitVersion = gitInfoFile.get().asFile.readText()
        val manifest = """<?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.build.example.minimal"
        android:versionName="${gitVersion}"
        android:versionCode="1" >
        <application android:label="Minimal">
            <activity android:name="MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    </manifest>
        """
        println("Writes to "   outputManifest.get().asFile.absolutePath)
        outputManifest.get().asFile.writeText(manifest)
    }
}

androidComponents {
    val gitVersionProvider = tasks.register<GitVersionTask>("gitVersionProvider") {
        gitVersionOutputFile.set(
            File(project.buildDir, "intermediates/gitVersionProvider/output"))
        outputs.upToDateWhen { false }
    }
    onVariants { variant ->
        val manifestProducer = tasks.register<ManifestProducerTask>("${variant.name}ManifestProducer") {
            gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
        }
        variant.artifacts.use(manifestProducer)
            .wiredWith(ManifestProducerTask::outputManifest)
            .toCreate(SingleArtifact.MERGED_MANIFEST)
    }
}
学新通

执行 assembleDebug 后会自动触发 debugManifestProducer 任务,而debugManifestProducer 任务的输入依赖 gitVersionProvider 任务的输出,因此gitVersionProvider 任务会先被执行,在 build/intermediates/gitVersionProvider/output 文件中生成版本号

学新通

然后会执行 debugManifestProducer 任务,在 build\intermediates\merged_manifest\debug\debugManifestProducer 目录下生成被覆写的AndroidManifest.xml 文件,其中android:versionName的值被替换为上面output 文件中生成的版本号:

<?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.android.build.example.minimal"
        android:versionName="1234"
        android:versionCode="1" >
        <application android:label="Minimal">
            <activity android:name="MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    </manifest>

修改 Manifest 文件内容

abstract class GitVersionTask: DefaultTask() {

    @get:OutputFile
    abstract val gitVersionOutputFile: RegularFileProperty

    @ExperimentalStdlibApi
    @TaskAction
    fun taskAction() {

        // this would be the code to get the tip of tree version,
        // val firstProcess = ProcessBuilder("git","rev-parse --short HEAD").start()
        // val error = firstProcess.errorStream.readBytes().decodeToString()
        // if (error.isNotBlank()) {
        //      System.err.println("Git error : $error")
        // }
        // var gitVersion = firstProcess.inputStream.readBytes().decodeToString()

        // but here, we are just hardcoding :
        gitVersionOutputFile.get().asFile.writeText("1234")
    }
}
abstract class ManifestTransformerTask: DefaultTask() {

    @get:InputFile
    abstract val gitInfoFile: RegularFileProperty

    @get:InputFile
    abstract val mergedManifest: RegularFileProperty

    @get:OutputFile
    abstract val updatedManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {

        val gitVersion = gitInfoFile.get().asFile.readText()
        var manifest = mergedManifest.asFile.get().readText()
        manifest = manifest.replace("android:versionCode=\"1\"", "android:versionCode=\"${gitVersion}\"")
        println("Writes to "   updatedManifest.get().asFile.absolutePath)
        updatedManifest.get().asFile.writeText(manifest)
    }
}
androidComponents {
    onVariants { variant ->
        val gitVersionProvider = tasks.register<GitVersionTask>("${variant.name}GitVersionProvider") {
            gitVersionOutputFile.set(
                File(project.buildDir, "intermediates/gitVersionProvider/output"))
            outputs.upToDateWhen { false }
        }

        val manifestUpdater = tasks.register<ManifestTransformerTask>("${variant.name}ManifestUpdater") {
            gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
        }
        variant.artifacts.use(manifestUpdater)
            .wiredWithFiles(
                ManifestTransformerTask::mergedManifest,
                ManifestTransformerTask::updatedManifest)
            .toTransform(SingleArtifact.MERGED_MANIFEST)
    }
}
学新通

学新通

执行 assembleDebug 后会自动触发 debugManifestUpdater 任务,而 debugManifestUpdater 任务的输入依赖于 debugGitVersionProvider 的输出,因此 debugGitVersionProvider 任务先会被执行,在 build/intermediates/gitVersionProvider/output 文件中生成版本号。然后 debugManifestUpdater 任务会被执行,在 build\intermediates\merged_manifest\debug\目录下生成被覆写的AndroidManifest.xml 文件,其中android:versionCode的值被替换为上面output 文件中生成的版本号:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.build.example.minimal"
    android:versionCode="1234"
    android:versionName="1.0.0" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="33" />

    <application
        android:debuggable="true"
        android:label="Minimal" >
        <activity
            android:name="com.android.build.example.minimal.MainActivity"
            android:exported="true" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
学新通

修改 library 模块中的 Manifest 文件内容

新建一个 library module,其 AndroidManifest.xml 内容为:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application>
    </application>
</manifest>

在 library module 的 build.gradle.kts 中添加:

abstract class ManifestTransformerTask: DefaultTask() {

    @get:InputFile
    abstract val mergedManifest: RegularFileProperty

    @get:OutputFile
    abstract val updatedManifest: RegularFileProperty

    @TaskAction
    fun taskAction() {
        var manifest = mergedManifest.asFile.get().readText()
        manifest = manifest.replace("<application",
            "<uses-permission android:name=\"android.permission.INTERNET\"/>\n<application")
        println("Writes to "   updatedManifest.get().asFile.absolutePath)
        updatedManifest.get().asFile.writeText(manifest)
    }
}

androidComponents {
    onVariants { variant ->
        val manifestUpdater = tasks.register<ManifestTransformerTask>("${variant.name}ManifestUpdater")
        variant.artifacts.use(manifestUpdater)
            .wiredWithFiles(
                ManifestTransformerTask::mergedManifest,
                ManifestTransformerTask::updatedManifest)
            .toTransform(SingleArtifact.MERGED_MANIFEST)
    }
}
学新通

执行 assembleDebug 后会自动触发 debugManifestUpdater 任务执行, 在 library module 的build\intermediates\merged_manifest\debug\目录下生成被覆写的AndroidManifest.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.build.example.mymodule" >

    <uses-sdk
        android:minSdkVersion="21"
        android:targetSdkVersion="33" />

    <uses-permission android:name="android.permission.INTERNET"/>
<application>
    </application>

</manifest>

修改 aar 文件

新建一个 library module 文件,在其 build.gradle.kts 中添加:

import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.TaskAction 
import com.android.build.api.artifact.SingleArtifact

abstract class UpdateArtifactTask: DefaultTask() {
    @get:InputFiles
    abstract val  initialArtifact: RegularFileProperty

    @get:OutputFile
    abstract val updatedArtifact: RegularFileProperty

    @TaskAction
    fun taskAction() {
        val versionCode = "artifactTransformed = true"
        println("artifactPresent = "   initialArtifact.isPresent)
        println("initialArtifact = "   initialArtifact.get().asFile)
        println("updatedArtifact = "   updatedArtifact.get().asFile)
        updatedArtifact.get().asFile.writeText(versionCode)
    }
}
abstract class ConsumeArtifactTask: DefaultTask() {
    @get:InputFiles
    abstract val finalArtifact: RegularFileProperty

    @TaskAction
    fun taskAction() {
        println(finalArtifact.get().asFile.readText())
    }
}
androidComponents {
    onVariants {
        val updateArtifact = project.tasks.register<UpdateArtifactTask>("${it.name}UpdateArtifact")
        it.artifacts.use(updateArtifact)
            .wiredWithFiles(
                UpdateArtifactTask::initialArtifact,
                UpdateArtifactTask::updatedArtifact)
            .toTransform(SingleArtifact.AAR)
        project.tasks.register<ConsumeArtifactTask>("${it.name}ConsumeArtifact") {
            finalArtifact.set(it.artifacts.get(SingleArtifact.AAR))
        }
    }
}
学新通

执行 assembleDebug,构建任务输出aar文件后,然后会执行 debugUpdateArtifact 任务,aar 文件内容被覆写,输出到build\outputs\aar\下:

学新通
再执行 debugConsumeArtifact 任务,输出:
学新通
注意,这个sample官方是乱写的,把aar文件内容改写成了一句话,然后读取aar文件内容输出。目的是告诉我们可以人为的去修改你想要修改的内容到aar文件中,千万不要把上面代码直接拷贝用在项目中。

设置 signingConfig

plugins {
    id("com.android.application")
    kotlin("android")
}

android {
    namespace = "com.android.build.example.minimal"
    compileSdk = 33
    defaultConfig {
		applicationId = "com.android.build.example.minimal"
        minSdk = 21
		targetSdk = 33
        versionCode = 1
        versionName = "1.0.0"
    }
    signingConfigs { 
        create("default") {
            keyAlias = "pretend"
            keyPassword = "some password"
            storeFile = file("/path/to/supposedly/existing/keystore.jks")
            storePassword = "some keystore password"
        }
        create("other") {
            keyAlias = "invalid"
            keyPassword = "some password"
            storeFile = file("/path/to/some/other/keystore.jks")
            storePassword = "some keystore password"
        }
    }

    flavorDimensions  = listOf("version")

    buildTypes {
        create("special")
    }
    productFlavors {
        create("flavor1") {
            dimension = "version"
            signingConfig = signingConfigs.getByName("default")
        }
        create("flavor2") {
            dimension = "version"
            signingConfig = signingConfigs.getByName("default")
        }
    }
}

dependencies {
    implementation(project(":my_module"))
}

androidComponents {
    onVariants(selector()
        .withFlavor("version" to "flavor1")
        .withBuildType("special")
    ) { variant ->
        variant.signingConfig.setConfig(android.signingConfigs.getByName("other"))
    }
}
学新通

拷贝 apk 文件

import java.io.Serializable
import javax.inject.Inject
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.workers.WorkerExecutor
import com.android.build.api.artifact.SingleArtifact
import com.android.build.api.artifact.ArtifactTransformationRequest
import com.android.build.api.variant.BuiltArtifact

interface WorkItemParameters: WorkParameters, Serializable {
    val inputApkFile: RegularFileProperty
    val outputApkFile: RegularFileProperty
}

abstract class WorkItem @Inject constructor(private val workItemParameters: WorkItemParameters)
    : WorkAction<WorkItemParameters> {
    override fun execute() {
        workItemParameters.apply {
            outputApkFile.get().asFile.delete()
            inputApkFile.asFile.get().copyTo(outputApkFile.get().asFile)
        }
    }
}
abstract class CopyApksTask @Inject constructor(private val workers: WorkerExecutor): DefaultTask() {

    @get:InputFiles
    abstract val apkFolder: DirectoryProperty

    @get:OutputDirectory
    abstract val outFolder: DirectoryProperty

    @get:Internal
    abstract val transformationRequest: Property<ArtifactTransformationRequest<CopyApksTask>>

    @TaskAction
    fun taskAction() {
        transformationRequest.get().submit(this, workers.noIsolation(), WorkItem::class.java) {
                builtArtifact: BuiltArtifact, outputLocation: Directory, param: WorkItemParameters ->
            val inputFile = File(builtArtifact.outputFile)
            println("inputFile: ${inputFile.absolutePath}")
            param.inputApkFile.set(inputFile)
            println("outputLocation: ${outputLocation.asFile.absolutePath}")
            param.outputApkFile.set(File(outputLocation.asFile, inputFile.name))
            param.outputApkFile.get().asFile
        }
    }
}

androidComponents {
    onVariants { variant ->
        val copyApksProvider = tasks.register<CopyApksTask>("copy${variant.name}Apks")

        val transformationRequest = variant.artifacts.use(copyApksProvider)
            .wiredWithDirectories(
                CopyApksTask::apkFolder,
                CopyApksTask::outFolder)
            .toTransformMany(SingleArtifact.APK)

        copyApksProvider.configure {
            this.transformationRequest.set(transformationRequest)
        }
    }
}
学新通

执行 assembleDebug 后会自动触发 copydebugApks 任务的执行,输出:

学新通
虽然结果很明显:apk 文件从 build\outputs\apk\debug\ 目录被拷贝到了 build\intermediates\apk\copydebugApks 目录下,但是说实话,这个代码我很懵逼,我真是没看懂。。。

官方的这个例子可能主要是想演示Transformation的使用,如果只是想要拷贝apk文件,可以使用更加简单的写法(参见下文buildSrc部分的拷贝apk文件部分)。

获取 MappingFile

abstract class MappingFileUploadTask: DefaultTask() {

    @get:InputFile
    abstract val mappingFile: RegularFileProperty

    @TaskAction
    fun taskAction() {
        println("Uploading ${mappingFile.get().asFile.absolutePath} to fantasy server...")
    }
}
androidComponents {
    onVariants { variant ->
        project.tasks.register<MappingFileUploadTask>("${variant.name}MappingFileUpload") {
            mappingFile.set(variant.artifacts.get(SingleArtifact.OBFUSCATION_MAPPING_FILE))
        }
    }
}
学新通

buildSrc Use

buildSrc 简单配置

在项目根目录下新建一个 buildSrc 目录,其中新建 build.gradle.kts,添加配置:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.5.31"
}

repositories {
//    谷歌()
//    jcenter()
    // 阿里云镜像
    maven(url = "https://maven.aliyun.com/repository/central")
    maven(url = "https://maven.aliyun.com/repository/public")
    maven(url = "https://maven.aliyun.com/repository/jcenter")
    maven(url = "https://maven.aliyun.com/repository/谷歌")
    maven(url = "https://maven.aliyun.com/repository/releases")
    maven(url = "https://maven.aliyun.com/repository/snapshots")
    maven(url = "https://maven.aliyun.com/repository/gradle-plugin")
}

tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions.apiVersion = "1.3"
}

dependencies {
    implementation("com.android.tools.build:gradle-api:7.3.1")
    implementation(kotlin("stdlib"))
    gradleApi()
}
学新通

在 src/main/kotlin下面新建一个kotlin文件:

学新通
ExamplePlugin.kt内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.finalizeDsl { extension ->
            extension.buildTypes.create("extra").let {
                it.isJniDebuggable = true
            }
        }
    }
}

就是将前面动态添加 build Type 的代码放在 apply() 方法中。然后在 app/build.gradle.kts 中应用该插件:

plugins {
    id("com.android.application")
    kotlin("android")
}

apply<ExamplePlugin>()

android {
    namespace = "com.android.build.example.minimal"
    compileSdk = 33
    defaultConfig {
		applicationId = "com.android.build.example.minimal"
        minSdk = 21
		targetSdk = 33
        versionCode = 1
        versionName = "1.0.0"
    }
}
学新通

同步一下,即可在 Build Variants 中看到名为 extra 的构建变体。

获取 apk 文件

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.artifact.SingleArtifact

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java).apply {
            onVariants { variant ->
                project.tasks.register("${variant.name}DisplayApks", DisplayApksTask::class.java) {
                    it.apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
                    it.builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
                }
            }
        }
    }
}
学新通

其中 DisplayApksTask 跟前文中提到的一样,同步项目后,运行结果跟之前一样。

重写 Manifest 文件

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.artifact.SingleArtifact
import java.io.File

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val gitVersionProvider =
            project.tasks.register("gitVersionProvider", GitVersionTask::class.java) {
                it.gitVersionOutputFile.set(
                    File(project.buildDir, "intermediates/gitVersionProvider/output")
                )
                it.outputs.upToDateWhen { false }
            }

        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)

        androidComponents.onVariants { variant ->
            val manifestProducer =
                project.tasks.register("${variant.name}ManifestProducer", ManifestProducerTask::class.java) {
                    it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
                }
            variant.artifacts.use(manifestProducer)
                .wiredWith(ManifestProducerTask::outputManifest)
                .toCreate(SingleArtifact.MERGED_MANIFEST)
        }
    }
}
学新通

其中 GitVersionTask 和 ManifestProducerTask 跟前文中提到的一样,同步项目后,运行结果跟之前一样。

修改 Manifest 文件内容

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.artifact.SingleArtifact
import java.io.File

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val gitVersionProvider =
            project.tasks.register("gitVersionProvider", GitVersionTask::class.java) {
                it.gitVersionOutputFile.set(
                    File(project.buildDir, "intermediates/gitVersionProvider/output")
                )
                it.outputs.upToDateWhen { false }
            }

        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)

        androidComponents.onVariants { variant ->
            val manifestUpdater =
                project.tasks.register("${variant.name}ManifestUpdater", ManifestTransformerTask::class.java) {
                    it.gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
                }
            variant.artifacts.use(manifestUpdater)
                .wiredWithFiles(
                    ManifestTransformerTask::mergedManifest,
                    ManifestTransformerTask::updatedManifest)
                .toTransform(SingleArtifact.MERGED_MANIFEST)
        }
    }
}
学新通

其中 GitVersionTask 和 ManifestTransformerTask 跟前文中提到的一样,同步项目后,运行结果跟之前一样。

修改版本号

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.VariantOutputConfiguration

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.findByType(ApplicationAndroidComponentsExtension::class.java)?.apply {
            configure(project)
        }
    }
}

private fun ApplicationAndroidComponentsExtension.configure(project: Project) {
    // Note: Everything in there is incubating.

    // onVariants registers an action that configures variant properties during
    // variant computation (which happens during afterEvaluate)
    onVariants {
        // applies to all variants. This excludes test components (unit test and androidTest)
    }

    // use filter to apply onVariants to a subset of the variants
    onVariants(selector().withBuildType("release")) { variant ->
        // Because app module can have multiple output when using mutli-APK, versionCode/Name
        // are only available on the variant output.
        // Here gather the output when we are in single mode (ie no multi-apk)
        val mainOutput = variant.outputs.single { it.outputType == VariantOutputConfiguration.OutputType.SINGLE }

        // create version Code generating task
        val versionCodeTask = project.tasks.register("computeVersionCodeFor${variant.name}", VersionCodeTask::class.java) {
            it.outputFile.set(project.layout.buildDirectory.file("versionCode.txt"))
        }

        // wire version code from the task output
        // map will create a lazy Provider that
        // 1. runs just before the consumer(s), ensuring that the producer (VersionCodeTask) has run
        //    and therefore the file is created.
        // 2. contains task dependency information so that the consumer(s) run after the producer.
        mainOutput.versionCode.set(versionCodeTask.flatMap { it.outputFile.map { it.asFile.readText().toInt() } })

        // same for version Name
        val versionNameTask = project.tasks.register("computeVersionNameFor${variant.name}", VersionNameTask::class.java) {
            it.outputFile.set(project.layout.buildDirectory.file("versionName.txt"))
        }
        mainOutput.versionName.set(versionNameTask.flatMap { it.outputFile.map { it.asFile.readText() }})
    }
}
学新通
// VersionCodeTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

abstract class VersionCodeTask : DefaultTask() {

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun action() {
        outputFile.get().asFile.writeText("886")
    }
}
学新通
// VersionNameTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction

abstract class VersionNameTask : DefaultTask() {

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun action() {
        outputFile.get().asFile.writeText("versionName from task")
    }
}
学新通

同步项目后,执行 assembleRelease 后会自动先执行 computeVersionCodeForrelease 和 computeVersionNameForrelease 任务,在app/build 目录下会生成versionCode.txt和versionName.txt文件

学新通
然后查看build/outputs/apk/release目录下生成的apk中的AndroidManifest.xml的versionCode和versionName:

学新通

打印 compileClasspath

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension

abstract class ExamplePlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.onVariants { variant ->
            project.tasks.register(
                "${variant.name}PrintCompileClasspath",
                PrintClasspathTask::class.java
            ) {
                it.classpath.from(variant.compileClasspath)
            }
        }
    }
}
学新通
// PrintClasspathTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.TaskAction

abstract class PrintClasspathTask: DefaultTask() {

    @get:Classpath
    abstract val classpath: ConfigurableFileCollection

    @TaskAction
    fun taskAction() {
        for (file in classpath.files) {
            println("classpath: ${file.absolutePath}")
        }
    }
}
学新通

同步项目后,执行 debugPrintCompileClasspath 任务,输出如下:

学新通

自定义扩展 DSL

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.plugins.ExtensionAware
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.AndroidComponentsExtension

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        // attach the BuildTypeExtension to each elements returned by the android buildTypes API.
        // val android = project.extensions.getByType(ApplicationExtension::class.java)
        val android = project.extensions.findByType(ApplicationExtension::class.java)
        if (android != null) {
	        android.buildTypes.forEach {
	            (it as ExtensionAware).extensions.add("myExampleDsl", BuildTypeExtension::class.java)
	        }
	        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
	        // hook up task configuration on the variant API.
	        androidComponents.onVariants { variant ->
	            // get the associated DSL BuildType element from the variant name
	            val buildTypeDsl = android.buildTypes.getByName(variant.name) as ExtensionAware
	            // find the extension on that DSL element.
	            val buildTypeExtension = buildTypeDsl.extensions.findByName("myExampleDsl") as BuildTypeExtension
	            // create and configure the Task using the extension DSL values.
	            project.tasks.register("${variant.name}MyExampleDSL", ExampleTask::class.java) { task ->
	                task.parameters.set(buildTypeExtension.invocationParameters ?: "")
	            }
	        }
        }
    }
}
学新通
// BuildTypeExtension.kt
/**
* Simple DSL extension interface that will be attached to the android build type DSL element.
*/
interface BuildTypeExtension {
    var invocationParameters: String?
}
// ExampleTask.kt
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.TaskAction

abstract class ExampleTask: DefaultTask() {
    @get:Input
    abstract val parameters: Property<String>

    @TaskAction
    fun taskAction() {
        println("Task executed with : \"${parameters.get()}\"")
    }
}

在 app/build.gradle.kts中应用:

plugins {
    id("com.android.application")
    kotlin("android")
}

apply<ExamplePlugin>()

android {
    ...
    buildTypes {
        debug {
            the<BuildTypeExtension>().invocationParameters = "-debug -log"
        }
    }
}

如果是 Groovy, 按照如下方式应用:

apply plugin: ExamplePlugin

android {
	...
    buildTypes {
        debug {
            myExampleDsl {
                invocationParameters = "-debug -log"
            }
        }
    }
}

同步项目后,执行 debugMyExampleDSL 任务:

学新通
输出:

学新通
这个例子向我们展示了第三方插件如何在任何地方添加 DSL 元素到 Android DSL 树中。当一个自定义插件需要配置某种行为绑定到 Android DSL 树的 buildTypes 或者 Flavor 声明中时,这会非常的有用。

在这个例子中,BuildTypeExtension类型是一个DSL接口声明,通过使用"myExampleDsl"命名空间附加到了Android Gradle Plugin的 build-type DSL元素中。

任何继承自 ExtensionAware 的DSL元素都可以定义第三方的扩展DSL附加到它上面,详细请参考 Android Gradle Plugin DSL 文档。

更多内容请到 Gradle 官网上查看: Developing Custom Gradle Plugins

自定义 DSL(通过向Variant注册扩展)

在上一个例子的基础上,修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java)
            .onVariants { variant ->
                project.tasks.register("${variant.name}MyExampleDSL", ExampleTask::class.java) { task ->
                    variant.getExtension(VariantExtension::class.java)?.parameters?.let {
                        task.parameters.set(it)
                    }
                }
            }
    }
}
学新通
/**
* Simple Variant scoped extension interface that will be attached to the AGP variant object.
*/
import org.gradle.api.provider.Property

interface VariantExtension {
    /**
     * the parameters is declared a Property<> so other plugins can declare a
     * task providing this value that will then be determined at execution time.
     */
    val parameters: Property<String>
}

然后再定义一个 ProviderPlugin:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.dsl.ApplicationExtension
import com.android.build.api.variant.AndroidComponentsExtension

abstract class ProviderPlugin: Plugin<Project> {

    override fun apply(project: Project) {
        val objects = project.objects

        val android = project.extensions.getByType(ApplicationExtension::class.java)
        android.buildTypes.forEach {
            it.extensions.add("myExampleDsl", BuildTypeExtension::class.java)
        }

        val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
        androidComponents.beforeVariants { variantBuilder ->
            val variantExtension = objects.newInstance(VariantExtension::class.java)

            val debug = android.buildTypes.getByName(variantBuilder.name)
            val buildTypeExtension = debug.extensions.findByName("myExampleDsl") as BuildTypeExtension
            variantExtension.parameters.set(buildTypeExtension.invocationParameters ?: "")

            variantBuilder.registerExtension(VariantExtension::class.java, variantExtension)
        }
    }
}
学新通

在上一个基础上,这个示例还添加了一个扩展到Android Gradle Plugin 的 Variant 接口。这在插件需要向其他第三方插件提供一个可以查询的Variant作用域对象时非常有用。

在这个例子中,BuildTypeExtension类型是一个DSL接口声明,通过使用"myExampleDsl"命名空间附加到了Android Gradle Plugin的 build-type DSL元素中。然后在 beforeVariants 方法中使用扩展的DSL元素来创建了一个 Variant 作用域对象并注册了它。

名为ExamplePlugin的插件会查询 Variant 作用域对象来配置 ExampleTask。这样两个插件不用直接连接就可以共享同一个 Variant 作用域对象。

在 app/build.gradle.kts 中同时应用这两个插件:

apply<ProviderPlugin>()
apply<ExamplePlugin>()

android {
    ...
    buildTypes {
        debug {
            the<BuildTypeExtension>().invocationParameters = "-debug -log"
        }
    }
}

同步项目后,执行 debugMyExampleDSL 任务,结果跟前一个例子一样。

关于这个示例,官方示例中还提供了一个使用更加简便的API版本:

import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.variant.DslExtension

abstract class ProviderPlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java)
            .registerExtension(
                DslExtension.Builder("myExampleDsl")
                    .extendBuildTypeWith(BuildTypeExtension::class.java)
                    .build()
            ) { variantExtensionConfig ->
                project.objects.newInstance(ExampleVariantExtension::class.java).also {
                    it.parameters.set(
                        variantExtensionConfig.buildTypeExtension(BuildTypeExtension::class.java).invocationParameters
                    )
                }
            }
    }
}
学新通
/**
* Simple Variant scoped extension interface that will be attached to the AGP
* variant object.
*/
import org.gradle.api.provider.Property
import com.android.build.api.variant.VariantExtension

interface ExampleVariantExtension: VariantExtension {
    /**
     * the parameters is declared a Property<> so other plugins can declare a
     * task providing this value that will then be determined at execution time.
     */
    val parameters: Property<String>
}
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.AndroidComponentsExtension

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java)
            .onVariants { variant ->
                project.tasks.register("${variant.name}MyExampleDSL", ExampleTask::class.java) { task ->
                    variant.getExtension(ExampleVariantExtension::class.java)?.parameters?.let {
                        task.parameters.set(it)
                    }
                }
            }
    }
}
学新通

动态添加 Asset 文件(通过自定义DSL实现)

通过前面的自定义DSL方式来提供 Asset 文件内容,修改ExamplePlugin内容如下:

import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import com.android.build.api.variant.DslExtension

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) { 
        project.extensions.getByType(AndroidComponentsExtension::class.java).apply {
            registerExtension(
                DslExtension.Builder("toy")
                    .extendBuildTypeWith(ToyExtension::class.java)
                    .build()
            ) { variantExtensionConfig ->
                project.objects.newInstance(ToyVariantExtension::class.java).also {
                    it.content.set(
                        variantExtensionConfig.buildTypeExtension(ToyExtension::class.java).content ?: ""
                    )
                }
            }
            onVariants { variant ->
                val content = variant.getExtension(ToyVariantExtension::class.java)?.content
                val taskProvider = project.tasks.register("${variant.name}AddAsset", AddAssetTask::class.java) { task ->
                    if (content != null) task.content.set(content)
                }
                variant.sources.assets?.addGeneratedSourceDirectory(taskProvider, AddAssetTask::outputDir)
            }
        }
    }
}
学新通
/**
* Simple DSL extension interface that will be attached to the android build type DSL element.
*/
interface ToyExtension {
    var content: String?
}
/**
* Simple Variant scoped extension interface that will be attached to the AGP variant object.
*/
import org.gradle.api.provider.Property
import com.android.build.api.variant.VariantExtension

interface ToyVariantExtension: VariantExtension {
    /**
     * content is declared a Property<> so other plugins can declare a task
     * providing this value that will then be determined at execution time.
     */
    val content: Property<String>
}
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.provider.Property
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction

abstract class AddAssetTask: DefaultTask() {

    @get:Optional
    @get:Input
    abstract val content: Property<String>

    @get:OutputDirectory
    abstract val outputDir: DirectoryProperty

    @TaskAction
    fun taskAction() {
    	if (content.get().isEmpty()) return
        File(outputDir.asFile.get(), "extra.txt").writeText(content.get())
        println("Asset added with content : ${content.get()}")
    }
}
学新通

在 app/build.gradle.kts 中应用

apply<ExamplePlugin>()

android {
    ...
    buildTypes {
        debug {
            the<ToyExtension>().content = "Toy"
        }
    }
}

执行 assembleDebug 后,生成的apk中的assets包含一个内容为"Toy"的extra.txt文件。

或者可以通过在 onVariants 方法中获取 ToyVariantExtension 的扩展来设置Asset内容:

apply<ExamplePlugin>()

android {
    ...
//    buildTypes {
//        debug {
//            the<ToyExtension>().content = "Toy"
//        }
//    }
}
androidComponents {
    onVariants { variant ->
        variant.getExtension(ToyVariantExtension::class.java)?.content?.set("Hello World")
    }
}

使用 ASM 访问 Class 文件

在 buildSrc 的 build.gradle.kts 添加asm依赖:

dependencies {
    implementation("org.ow2.asm:asm-util:9.2")
}

修改ExamplePlugin内容如下:

import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.instrumentation.AsmClassVisitorFactory
import com.android.build.api.instrumentation.ClassContext
import com.android.build.api.instrumentation.ClassData
import com.android.build.api.instrumentation.FramesComputationMode
import com.android.build.api.instrumentation.InstrumentationParameters
import com.android.build.api.instrumentation.InstrumentationScope
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.util.TraceClassVisitor
import java.io.File
import java.io.PrintWriter

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java)
            .onVariants { variant ->
                variant.instrumentation.transformClassesWith(ExampleClassVisitorFactory::class.java,
                    InstrumentationScope.ALL) {
                    it.writeToStdout.set(true)
                }
                variant.instrumentation.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
            }
    }

    interface ExampleParams : InstrumentationParameters {
        @get:Input
        val writeToStdout: Property<Boolean>
    }

    abstract class ExampleClassVisitorFactory : AsmClassVisitorFactory<ExampleParams> {

        override fun createClassVisitor(
            classContext: ClassContext,
            nextClassVisitor: ClassVisitor
        ): ClassVisitor {
            return if (parameters.get().writeToStdout.get()) {
                TraceClassVisitor(nextClassVisitor, PrintWriter(System.out))
            } else {
                TraceClassVisitor(nextClassVisitor, PrintWriter(File("trace_out")))
            }
        }

        override fun isInstrumentable(classData: ClassData): Boolean {
            return classData.className.startsWith("com.example")
        }
    }
}
学新通

这个示例代码的作用是,使用ASM类访问者来转换类。示例中的转换器跟踪com.example包中的所有类,并将类的字节码通过TraceClassVisitor打印到标准输出中。

执行 transformDebugClassesWithAsm 任务,输出:

学新通

过滤 Variant 的 BuildType 类型

在 buildSrc 的 build.gradle.kts 添加依赖:

dependencies {
    implementation("com.android.tools.build:gradle:7.3.1")
}

修改ExamplePlugin内容如下:

import com.android.build.api.variant.ApplicationAndroidComponentsExtension
import com.android.build.api.variant.LibraryAndroidComponentsExtension
import com.android.build.gradle.AppPlugin
import com.android.build.gradle.LibraryPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withType(AppPlugin::class.java) {
            project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)
                .beforeVariants {
                    // disable all unit tests for apps (only using instrumentation tests)
                    it.enableUnitTest = false
                    println("AppPlugin Variant Name: ${it.name}")
                }
        }
        project.plugins.withType(LibraryPlugin::class.java) {
            project.extensions.getByType(LibraryAndroidComponentsExtension::class.java).apply {
                beforeVariants(selector().withBuildType("debug")) {
                    // Disable instrumentation for debug
                    it.enableAndroidTest = false
                    println("LibraryPlugin Variant Name: ${it.name}")
                }
                beforeVariants(selector().withBuildType("release")) {
                    // disable all unit tests for apps (only using instrumentation tests)
                    it.enableUnitTest = false
                    println("LibraryPlugin Variant Name: ${it.name}")
                }
            }
        }
    }
}
学新通

在 app 和 library 模块中分别应用插件,同步后即可生效。

注意:在buildSrc中,通常应该只需依赖com.android.tools.build:gradle-api ,而不应该依赖完整的com.android.tools.build:gradle 前者包括公开的API, 后者包括完整的API,但是这些api中非公开的部分可能会被修改、弃用。查找某个类型可以考虑使用 findByType.

拷贝 apk 文件

修改ExamplePlugin内容如下:

import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.artifact.SingleArtifact
abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {

        project.extensions.getByType(AndroidComponentsExtension::class.java)
             .onVariants { variant ->
                val copyApksProvider = project.tasks.register("copy${variant.name}Apks", CopyApksTask::class.java)
                val transformationRequest = variant.artifacts.use(copyApksProvider)
                    .wiredWithDirectories(
                        CopyApksTask::apkFolder,
                        CopyApksTask::outFolder)
                    .toTransformMany(SingleArtifact.APK)

                copyApksProvider.configure {
                    it.transformationRequest.set(transformationRequest)
                }
            }
    }
}
学新通

其中 CopyApksTask 内容跟前文提到的一样,同步项目,执行 assembleDebug ,结果跟之前一样。

另外这个例子我想了一下,如果只是需要拷贝apk文件,可以更加简单一点:

import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File
import com.android.build.api.variant.AndroidComponentsExtension
import com.android.build.api.artifact.SingleArtifact
abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java)
             .onVariants { variant ->
                project.tasks.register("copy${variant.name}Apks", CopyApksTask::class.java) {
                    it.apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
                    it.builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
                    it.outFolder.set(File("${project.rootDir.absolutePath}/apks/${variant.name}"))
                }
            }
    }
}
学新通
import java.io.File
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.TaskAction
import org.gradle.api.provider.Property
import com.android.build.api.variant.BuiltArtifactsLoader

abstract class CopyApksTask: DefaultTask() {

    @get:OutputDirectory
    abstract val outFolder: DirectoryProperty

    @get:InputFiles
    abstract val apkFolder: DirectoryProperty

    @get:Internal
    abstract val builtArtifactsLoader: Property<BuiltArtifactsLoader>

    @TaskAction
    fun taskAction() {
        val builtArtifacts = builtArtifactsLoader.get().load(apkFolder.get())
        outFolder.get().asFile.apply {
            delete()
            builtArtifacts?.elements?.forEach {
                println("Copy APK from ${it.outputFile} to $absolutePath")
                val inputFile = File(it.outputFile)
                inputFile.copyTo(File(this, inputFile.name))
            }
        }
    }
}
学新通

同步项目后,执行copydebugApks任务,apk文件会被拷贝到根目录下的apks目录中:

学新通

AGP 使用总结 | 概念理解

定义使用规范

首先所有的 variant API 和 lambda 代码块都是在gradle文件中的 androidComponents{ } 中使用,而不是 android{ } 中。

如果是在buildSrc自定义插件中使用,则需要在 apply(project: Project) { } 方法中通过 project.extensions.getByType(AndroidComponentsExtension::class.java) 查找到对应的 androidComponents 类型,然后调用相关的 variant API 。

abstract class ExamplePlugin: Plugin<Project> {
    override fun apply(project: Project) {
        project.extensions.getByType(AndroidComponentsExtension::class.java)
             .onVariants { variant ->
                project.tasks.register(...) {...}
                ...
            }
        }
    }
}

在自定义插件中使用如 project.extensions.getByType(ApplicationAndroidComponentsExtension::class.java)project.extensions.getByType(ApplicationExtension::class.java)时,如果是在library module中应用该插件会报错,因为 library module中不存在该类型, library module对应的class是LibraryXXX开头的类,app module 对应的class是ApplicationXXX开头的类。如果插件只是在app module 或者 library module 二者之一中使用,可以不用做兼容处理,选择对应的类即可。如果插件是想在二者中都应用,要保证不报错,可以把 getByTypegetByName 换成 findByTypefindByName, 区别是前者找不到时会直接抛出异常,而后者会返回可空类型,找不到时会返回null, 方便判断。

定义 Task

从目前官方的所有示例代码来看,定义 Task 只使用了如下一种方式(这也说明以前定义task的方式不被推荐,潜台词就是以前的方式未来会被弃用,所以最好是都按照这种方式来使用)。

abstract class MyTask : DefaultTask() {

	@get:InputFile
    abstract val inputFile: RegularFileProperty

    @get:OutputFile
    abstract val outputFile: RegularFileProperty

    @TaskAction
    fun taskAction() { 
         // 读取属性值,执行读写操作等
    }
}

它的格式很固定,就是定义一个继承DefaultTask抽象类,不需要实现它,使用@get:InputFile @get:OutputFile等注解来标注属性(且属性也是抽象的,一般这些抽象属性主要是输入和输出文件相关),使用 @TaskAction注解来标注执行任务的具体方法。

注册 Task

从目前官方的所有示例代码来看,注册任务的格式也比较固定,基本上都是类似下面的模板:

androidComponents {
    onVariants { variant ->
        val taskProvider = tasks.register<MyTask>("MyTaskName") {
            inputFile.set(xxx)
            outputFile.set(xxx)
        }
    }
} 

这里设置任务的输入输出文件属性有两种方式,一种是在register()方法后面的lambda块中调用属性进行设置,另一种则是需要将register()方法返回的taskProvider 对象连接到variantArtifact上面,由系统构建输出Artifact产物时自动设置Task中的输入输出文件属性,例如:

val manifestUpdater = tasks.register<MyTask>("MyTaskName")
variant.artifacts.use(manifestUpdater)
	.wiredWithFiles(
	   ManifestTransformerTask::mergedManifest,
	   ManifestTransformerTask::updatedManifest)
	.toTransform(SingleArtifact.MERGED_MANIFEST) 

finalizeDsl、beforeVariants、onVariants

Gradle工程的整个生命周期可以分为三个阶段:初始化阶段、配置阶段、执行阶段。它们的作用如下:
学新通
因此以前主要使用 beforeEvaluateafterEvaluatebuildFinished 这三个方法来寻找切入点:

/**
* 配置阶段开始前的监听回调
*/
this.beforeEvaluate {
    println "配置阶段开始前--->"
}
/**
* 配置阶段完成以后的监听回调
*/
this.afterEvaluate {
    println "配置阶段执行完毕--->"
}
/**
* gradle执行完毕后的监听回调
*/
this.gradle.buildFinished {
    println "执行阶段执行完毕--->"
}
学新通

而现在的 beforeVariantsonVariantsfinalizeDsl 这三个只能说可以类比之前的阶段,但是又不完全一样,因为它们是以 Variant 变体作为单位来执行的, Variant 变体 就是指构建类型 buildTypeFlavor 的组合,一般没有 Flavor 的话,Variant默认就只有releasedebug两种buildType构建类型。

其中 finalizeDsl 可以类比之前的 afterEvaluate ,可以从它的源码注释中得到解释:

    /**
     * API to customize the DSL Objects programmatically after they have been evaluated from the
     * build files and before used in the build process next steps like variant or tasks creation.
     *
     * Example of a build type creation:
     * ```kotlin
     * androidComponents.finalizeDsl { extension ->
     *     extension.buildTypes.create("extra")
     * }
     * ```
     */
    fun finalizeDsl(callback: (T) -> Unit)

就是在build文件配置完毕之后,且在build任务开始使用之前的阶段。因此可以认为它和 afterEvaluate 是对等的。

beforeVariantsonVariants则是在 finalizeDsl 之后、build任务处理之前的中间部分分成的两个阶段:
学新通
这三个方法中只有 onVariants 是可以访问到 variant 变体对象的。

  • finalizeDsl: 在此回调中,您可以访问和修改通过解析 build 文件中 android 代码块的信息而创建的 DSL 对象。这些 DSL 对象将用来在 build 的后续阶段中初始化和配置变体。例如,您可以通过编程方式创建新配置或替换属性,但请注意,所有值都必须在配置时进行解析,因此它们不能依赖于任何外部输入。此回调执行完毕后,DSL 对象将不再有用,您不应再保留对它们的引用或修改它们的值。
  • beforeVariants: 在 build 过程的这一阶段,您可以访问 VariantBuilder 对象,这些对象决定了将要创建的变体及其属性。例如,您可以通过编程方式停用某些变体及其测试,或者仅针对所选变体更改某个属性的值(例如 minSdk)。与 finalizeDsl() 类似,您提供的所有值都必须在配置时进行解析,且不得依赖于外部输入。beforeVariants() 回调执行完毕后,您不得修改 VariantBuilder 对象。
  • onVariants: 在调用 onVariants() 时,AGP 将会创建的所有工件均已确定,因此您无法再停用它们。不过,您可以通过为 Variant 对象中的 Property 属性设置值来修改任务所使用的某些值。由于 Property 值只会在执行 AGP 的任务时进行解析,因此您可以放心地将这些值从您自己的自定义任务连接到提供程序;您的自定义任务将执行任何所需的计算,包括从文件或网络等外部输入中读取内容。

在 app/build.gradle.kts 中添加如下内容,来打印一下

androidComponents {
    finalizeDsl {
        println(">>>>>>finalizeDsl")
    }
    beforeVariants {
        println("${it.name}>>>>>>beforeVariants")
    }
    onVariants {
        println("${it.name}>>>>>>onVariants")
    }
}

同步项目之后,执行build输出:

学新通

可以看到 finalizeDsl 只执行一次,beforeVariants 和 onVariants 针对 debug 和 release 分别执行两次。并且它们全部是执行在Android构建任务执行之前,也就是说这三个方法其实都还是发生在Gradle的配置阶段,还没到执行阶段,因此最多只能用来注册任务,还没到真正执行任务的时候。

所以在 onVariants 方法中,即便能够访问变体,也不能直接调用variant的API,因此在配置阶段是获取不到值的。例如:

androidComponents { 
    onVariants { variant ->
        println("${variant.name}>>>>>>onVariants")
        val apkDir : Provider<Directory> = variant.artifacts.get(SingleArtifact.APK)
        println("apkDir>>>>>>${apkDir.get().asFile.absolutePath}")
    }
}

这样写构建会直接报错,只能在注册任务时作为参数设置给任务的输入文件。

Artifact 中间件

Artifact 翻译过来意思是手工艺品、工件、手工制品,这里我觉得使用中间件来表达比较好一些,因为它就是代表着Android构建过程中的一些中间产物,如 manifest、aar、class、apk等等。这些中间件可能来自构建过程中不同任务的产出:

学新通

AGP 7.0 允许通过API访问变体对象和一些中间件,以方便用户在不接触任务的情况下影响build的行为。AGP 的 com.android.tools.build:gradle-api 依赖提供了可以访问 Variant变体以及 Artifact 中间件的public公开的API,这些API是稳定的,可以放心使用。而AGP其余部分是非公开的,尤其是Task部分。AGP现在允许用户与多个不同的中间件进行交互。

学新通
SingleArtifact支持的中间件产物如下

中间件 说明
SingleArtifact.AAR 将发布的最终 AAR 文件。
SingleArtifact.APK APK 文件所在的目录。
SingleArtifact.ASSETS 将打包到生成的 APK 或 Bundle 中的assets。
SingleArtifact.BUNDLE 可在 Play 商店消费的最终Bundle。
SingleArtifact.MERGED_MANIFEST 将在 APK、Bundle 和 InstantApp 包中使用的合并清单文件。
SingleArtifact.METADATA_LIBRARY_DEPENDENCIES_REPORT 库依赖项的元数据。
SingleArtifact.OBFUSCATION_MAPPING_FILE  
SingleArtifact.PUBLIC_ANDROID_RESOURCES_LIST library工程导出的公共资源文件列表。

MultipleArtifact 支持的中间件产物如下

中间件 说明
MultipleArtifact.ALL_CLASSES_DIRS 最终将为此模块进行 dex 处理的类,这些类是作为目录生成或处理的。
MultipleArtifact.ALL_CLASSES_JARS 最终将为此模块进行 dex 处理的类,这些类是作为 jar 文件生成或处理的。
MultipleArtifact.MULTIDEX_KEEP_PROGUARD 带有附加 ProGuard 规则的文本文件,用于确定将哪些类编译到主 dex 文件中。

Android Gradle Plugin 管理着一个中间件的注册表,这些中间件附加到所有输入、输出和中间文件的每个变体上,表现形式为持有附加依赖信息的provider。这个注册表在整个Android Gradle Plugin 中使用,以取代手工连接任务,并管理中间件的位置,以避免意外冲突和不一致。

这些中间件的子集通过新的变体属性API公开。

这个注册表的键是Artifact的实例。中间件是文件还是目录都表示为其类型的一部分。当编写自定义任务时,使用正确的对应属性是很重要的(如DirectoryPropertyRegularFileProperty)。中间件的基数也在其类型中编码,在处理MultipleArtifact类型时,必须使用特定的apiListProperty交互。

目前对 Artifact 支持的操作有:

  • Get : 此操作获取中间件的最终版本。不管中间件是如何生成的,也不管其如何由task转换而来(有时),调用 get(或MultipleArtifactsgetAll) 方法将始终确保提供中间件的最终版本。它是一个只读值:不允许修改get方法获得的值。因此,get方法的返回类型是Provider而不是Property。此Provider只能用作其他任务的输入。
    例如,前面获取apk文件的示例代码中使用了variant.artifacts.get(SingleArtifact.APK)
androidComponents {
    onVariants { variant ->
        project.tasks.register<DisplayApksTask>("${variant.name}DisplayApks") {
            apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
            builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
        }
    }
} 
  • Transformation : 该操作以某种方式修改中间件。原始中间件将作为给定任务的输入提供,并提供用于输出转换后的中间件的新位置。
    例如,前面修改 Manifest 文件内容的示例代码中使用了toTransform
variant.artifacts.use(manifestUpdater)
      .wiredWithFiles(
          ManifestTransformerTask::mergedManifest,
          ManifestTransformerTask::updatedManifest)
      .toTransform(SingleArtifact.MERGED_MANIFEST) 
  • Append : Append只与使用MultipleArtifact修饰的中间件类型相关。因为这些类型表示一个DirectoryRegularFileList。一个任务可以声明一个追加到该列表的output输出。
    例如,前面添加Class文件的示例代码中使用了toAppendTo
variant.artifacts.use(taskProvider)
     .wiredWith(AddClassesTask::output)
     .toAppendTo(MultipleArtifact.ALL_CLASSES_DIRS)
  • Creation : 此操作将一个Artifact当前的provider替换为一个新的对象,丢弃所有先前的provider。这是一个“out”操作,其中任务将自己声明为该中间件的唯一provider。如果有多个任务将自己声明为该中间件的provider,则最后一个任务胜出。
    例如,前面重写 Manifest 文件的示例代码中使用了toCreate
variant.artifacts.use(manifestProducer)
     .wiredWith(ManifestProducerTask::outputManifest)
     .toCreate(SingleArtifact.MERGED_MANIFEST) 

总结来说就是中间件支持三种操作:查询、替换、新增。

任务依赖顺序

新版 AGP API 的一大优势就是定义 Task 任务时,不用显示的指定依赖关系(即不再需要使用 dependsOnmustRunAfter这些来指定),AGP 唯一指定任务之间依赖关系的方式就是通过输入输出文件来指定。这种依赖顺序是AGP自动决定的,开发者只需要关心针对哪个variant 提供什么样的任务,任务的输入输出是什么,输入从哪里来,输出要到哪里去。

总的来说:如果 A 任务的输出文件作为 B 任务的输入文件,那么 B 任务依赖 A 任务, B --> A,在执行 B 任务前会先执行 A 任务。

如果上一个Task的输出文件是下一个Task的输入文件,则两个Task会自动建立依赖关系。
学新通

任务之间的依赖关系

首先要明确的一点是AGP中 project.tasks.register() 返回的是一个 TaskProvider 对象,而非 Task 引用对象。AGP不希望用户持有 Task 引用进行操作。

TaskProvider 对象只能作为其他任务的输入,从而建立一种依赖关系。

例如,前面的修改 Manifest 文件内容的示例代码:

androidComponents {
    onVariants { variant ->
        val gitVersionProvider = tasks.register<GitVersionTask>("${variant.name}GitVersionProvider") {
            gitVersionOutputFile.set(
                File(project.buildDir, "intermediates/gitVersionProvider/output"))
            outputs.upToDateWhen { false }
        }

        val manifestUpdater = tasks.register<ManifestTransformerTask>("${variant.name}ManifestUpdater") {
            gitInfoFile.set(gitVersionProvider.flatMap(GitVersionTask::gitVersionOutputFile))
        }

        variant.artifacts.use(manifestUpdater)
            .wiredWithFiles(
                ManifestTransformerTask::mergedManifest,
                ManifestTransformerTask::updatedManifest)
            .toTransform(SingleArtifact.MERGED_MANIFEST)
    }
}
学新通

这个例子中,GitVersionTask 注册任务返回的 TaskProvider 对象为 ManifestTransformerTask 任务的输入提供来源,而 ManifestTransformerTask 注册任务返回的 TaskProvider 对象又为当前 variant 变体的Artifact中间件(这里是MERGED_MANIFEST)提供来源,因此这种上一个任务输出作为下一个任务输入的关系,自动建立了一种依赖顺序
学新通

通过Artifact挂接到系统任务

Artifact中间件的get操作输出作为自定义任务的输入文件属性,就可以使自定义任务挂接到系统任务中。

例如,前面获取 apk 文件的示例代码:

androidComponents {
    onVariants { variant ->
        project.tasks.register<DisplayApksTask>("${variant.name}DisplayApks") {
            apkFolder.set(variant.artifacts.get(SingleArtifact.APK))
            builtArtifactsLoader.set(variant.artifacts.getBuiltArtifactsLoader())
        }
    }
}

这里将系统 APK文件的输出位置设置为DisplayApksTask的输入文件apkFolder , 因此系统构建APK的任务和DisplayApksTask之间自动建立依赖关系,如果执行DisplayApksTask会先执行系统构建生成APK。

学新通
当然系统构建生成APK的任务不止一个,可能有很多一些列的Task,但不管它们之间的执行顺序如何,最终都会保证在DisplayApksTask之前完成。

通过taskProvider挂接到系统任务

一种方法是将 taskProvider 作为 Artifact 中间件的输入,自动挂接到系统任务。

例如,前面添加Class文件的示例:

androidComponents {
    onVariants { variant ->
        val taskProvider = project.tasks.register<AddClassesTask>("${variant.name}AddClasses")
        variant.artifacts.use(taskProvider)
            .wiredWith(AddClassesTask::output)
            .toAppendTo(MultipleArtifact.ALL_CLASSES_DIRS)
    }
} 

学新通

还有一种方法就是将 taskProvider.flatMapmap操作输出设置到variant相关属性的输入即可自动挂接到系统任务。

例如,前面修改 ApplicationId 的示例:

androidComponents {
    onVariants(selector().withBuildType("debug")) { variant ->
        val appIdProducer = tasks.register<ApplicationIdProducerTask>("${variant.name}AppIdProducerTask") {
            File(buildDir, name).also {
                outputFile.set(File(it, "appId.txt"))
            } 
        } 
        variant.applicationId.set(appIdProducer.flatMap { task ->
            task.outputFile.map { it.asFile.readText() }
        })
    }
} 

taskProvider.flatMap操作会先依赖其task执行完,接下来再执行map操作拿到outputFile的文件内容作为applicationId的输入。

对于前面的其他示例,情况是类似的,只要搞清楚谁是输入谁是输出,就能确定依赖关系,可自行分析。

wiredWith:奇怪又智能的API

对于前面的很多示例中,发现有些任务的输入输出文件设置的不是那么的明显。。比如前面修改Manifest文件内容的示例中:

variant.artifacts.use(manifestUpdater)
    .wiredWithFiles(
        ManifestTransformerTask::mergedManifest,
        ManifestTransformerTask::updatedManifest)
    .toTransform(SingleArtifact.MERGED_MANIFEST)

这里发现使用了一个奇怪的API,wiredWithFiles,将ManifestTransformerTask的输入输出属性的引用传入其中,然后执行Transform操作。这一顿骚操作看的我着实有些迷糊,不管怎样,这一顿操作下来的结果就是ManifestTransformerTask的输入输出属性值会被自动设置为Artifact中间件的输入输出文件,至于怎么设置的,我们也无需深究了,一般这种中间件产物的目录都是固定的,可以通过在 Task 中打印来查看。

查看API发现 wiredWith开头的还有几个,但是它们的用途不同,都需要结合一些特定的Artifact的操作符来使用。

  • wiredWith(taskOutput: (TaskT) -> FileSystemLocationProperty<FileTypeT>) 为Task设置输出文件,主要用于两种情况:1)output用于创建一个新版本的中间件,2)output用于附加到已有的中间件。如添加Class文件和重写Manifest文件的例子:
variant.artifacts.use(taskProvider)
     .wiredWith(AddClassesTask::output)
     .toAppendTo(MultipleArtifact.ALL_CLASSES_DIRS)
variant.artifacts.use(manifestProducer)
    .wiredWith(ManifestProducerTask::outputManifest)
    .toCreate(SingleArtifact.MERGED_MANIFEST)
  • wiredWith(taskInput: (TaskT) -> ListProperty<FileTypeT>, taskOutput: (TaskT) -> FileSystemLocationProperty<FileTypeT>) 为Task设置一组输入输出,输入是一个文件列表,输出是一个文件类型,比较适合用于根据所有中间件列表来组合生成一个中间件的情况。例如,前面修改Class文件的示例:
variant.artifacts.use(taskProvider)
    .wiredWith(ModifyClassesTask::allClasses, ModifyClassesTask::output)
    .toTransform(MultipleArtifact.ALL_CLASSES_DIRS) 
  • wiredWithDirectories(taskInput: (TaskT) -> DirectoryProperty, taskOutput: (TaskT) -> DirectoryProperty) 为Task设置一组输入输出目录,适合用于将SingleArtifact中间件从当前版本转换为新版本(且中间件的类型是Artifact.DIRECTORY)例如,前面拷贝apk文件的示例:
variant.artifacts.use(copyApksProvider)
     .wiredWithDirectories(
          CopyApksTask::apkFolder,
          CopyApksTask::outFolder)
      .toTransformMany(SingleArtifact.APK) 
  • wiredWithFiles(taskInput: (TaskT) -> RegularFileProperty, taskOutput: (TaskT) -> RegularFileProperty) 为Task设置一组输入输出文件,适合用于将SingleArtifact中间件从当前版本转换为新版本(且中间件的类型是Artifact.FILE)例如开头提到的修改Manifest文件内容的示例代码。

参考资料:

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgagakb
系列文章
更多 icon
同类精品
更多 icon
继续加载