本文所有代码是基于 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:
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))
}
})
}
在根目录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文件中实现了该接口:
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.
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())
}
})
}
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。
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 = """
${new String(getGitInfoFile().get().asFile.readBytes())} "
android:versionCode="1" >
"""
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 文件中生成的版本号:
<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>
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 文件中生成的版本号:
<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>
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文件部分)。
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")
}
})
}
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示例。
以下是官方sample中的Kotlin示例。
首先将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/google")
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/google")
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
...
}
...
}
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 任务,输出:
新建一个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 任务,输出:
androidComponents.finalizeDsl { extension ->
extension.buildTypes.create("extra").let {
it.isJniDebuggable = true
}
}
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";
}
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";
}
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内容如下:
<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:
<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>
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:
<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>
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资源:
<resources>
<string name="VariantName" translatable="false">appstring>
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)
}
}
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资源:
<resources>
<string name="VariantName" translatable="false">appstring>
<string name="GitVersion" translatable="false">1234string>
resources>
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资源目录中。
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类型文件:
就是将前面两种结合起来一起使用
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)
}
}
}
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操作。不知道有什么用。。。
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源码文件目录
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目录
import com.android.build.api.variant.AndroidVersion
androidComponents {
beforeVariants(selector().withName("release")) { variantBuilder ->
variantBuilder.minSdk = 23
}
}
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。
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 任务,输出:
根目录下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文件。
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文件内容被修改:
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 = """
${gitVersion} "
android:versionCode="1" >
"""
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 文件中生成的版本号:
<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>
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 文件中生成的版本号:
<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 module,其 AndroidManifest.xml 内容为:
<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(" ,
"\n )
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文件:
<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>
新建一个 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文件中,千万不要把上面代码直接拷贝用在项目中。
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"))
}
}
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文件部分)。
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 目录,其中新建 build.gradle.kts,添加配置:
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.5.31"
}
repositories {
// google()
// 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/google")
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文件:
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 的构建变体。
修改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 跟前文中提到的一样,同步项目后,运行结果跟之前一样。
修改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 跟前文中提到的一样,同步项目后,运行结果跟之前一样。
修改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:
修改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 任务,输出如下:
修改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
在上一个例子的基础上,修改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)
}
}
}
}
}
通过前面的自定义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().content = "Toy"
// }
// }
}
androidComponents {
onVariants { variant ->
variant.getExtension(ToyVariantExtension::class.java)?.content?.set("Hello World")
}
}
在 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 任务,输出:
在 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
.
修改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目录中:
首先所有的 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
二者之一中使用,可以不用做兼容处理,选择对应的类即可。如果插件是想在二者中都应用,要保证不报错,可以把 getByType
、getByName
换成 findByType
、findByName
, 区别是前者找不到时会直接抛出异常,而后者会返回可空类型,找不到时会返回null
, 方便判断。
从目前官方的所有示例代码来看,定义 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
注解来标注执行任务的具体方法。
从目前官方的所有示例代码来看,注册任务的格式也比较固定,基本上都是类似下面的模板:
androidComponents {
onVariants { variant ->
val taskProvider = tasks.register<MyTask>("MyTaskName") {
inputFile.set(xxx)
outputFile.set(xxx)
}
}
}
这里设置任务的输入输出文件属性有两种方式,一种是在register()
方法后面的lambda块中调用属性进行设置,另一种则是需要将register()
方法返回的taskProvider
对象连接到variant
的Artifact
上面,由系统构建输出Artifact
产物时自动设置Task中的输入输出文件属性,例如:
val manifestUpdater = tasks.register<MyTask>("MyTaskName")
variant.artifacts.use(manifestUpdater)
.wiredWithFiles(
ManifestTransformerTask::mergedManifest,
ManifestTransformerTask::updatedManifest)
.toTransform(SingleArtifact.MERGED_MANIFEST)
Gradle工程的整个生命周期可以分为三个阶段:初始化阶段、配置阶段、执行阶段。它们的作用如下:
因此以前主要使用 beforeEvaluate
、afterEvaluate
、buildFinished
这三个方法来寻找切入点:
/**
* 配置阶段开始前的监听回调
*/
this.beforeEvaluate {
println "配置阶段开始前--->"
}
/**
* 配置阶段完成以后的监听回调
*/
this.afterEvaluate {
println "配置阶段执行完毕--->"
}
/**
* gradle执行完毕后的监听回调
*/
this.gradle.buildFinished {
println "执行阶段执行完毕--->"
}
而现在的 beforeVariants
、onVariants
、finalizeDsl
这三个只能说可以类比之前的阶段,但是又不完全一样,因为它们是以 Variant 变体作为单位来执行的, Variant 变体 就是指构建类型 buildType
和 Flavor
的组合,一般没有 Flavor
的话,Variant默认就只有release
和debug
两种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
是对等的。
而 beforeVariants
和onVariants
则是在 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 翻译过来意思是手工艺品、工件、手工制品,这里我觉得使用中间件来表达比较好一些,因为它就是代表着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.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的实例。中间件是文件还是目录都表示为其类型的一部分。当编写自定义任务时,使用正确的对应属性是很重要的(如DirectoryProperty
或RegularFileProperty
)。中间件的基数也在其类型中编码,在处理MultipleArtifact
类型时,必须使用特定的api
与ListProperty
交互。
目前对 Artifact
支持的操作有:
get
(或MultipleArtifacts
的getAll
) 方法将始终确保提供中间件的最终版本。它是一个只读值:不允许修改get
方法获得的值。因此,get
方法的返回类型是Provider
而不是Property
。此Provider
只能用作其他任务的输入。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())
}
}
}
toTransform
:variant.artifacts.use(manifestUpdater)
.wiredWithFiles(
ManifestTransformerTask::mergedManifest,
ManifestTransformerTask::updatedManifest)
.toTransform(SingleArtifact.MERGED_MANIFEST)
MultipleArtifact
修饰的中间件类型相关。因为这些类型表示一个Directory
或RegularFile
的List
。一个任务可以声明一个追加到该列表的output
输出。toAppendTo
:variant.artifacts.use(taskProvider)
.wiredWith(AddClassesTask::output)
.toAppendTo(MultipleArtifact.ALL_CLASSES_DIRS)
Artifact
当前的provider
替换为一个新的对象,丢弃所有先前的provider
。这是一个“out”操作,其中任务将自己声明为该中间件的唯一provider
。如果有多个任务将自己声明为该中间件的provider
,则最后一个任务胜出。toCreate
:variant.artifacts.use(manifestProducer)
.wiredWith(ManifestProducerTask::outputManifest)
.toCreate(SingleArtifact.MERGED_MANIFEST)
总结来说就是中间件支持三种操作:查询、替换、新增。
新版 AGP API 的一大优势就是定义 Task 任务时,不用显示的指定依赖关系(即不再需要使用 dependsOn
、mustRunAfter
这些来指定),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
中间件的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 作为 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.flatMap
或map
操作输出设置到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
的输入。
对于前面的其他示例,情况是类似的,只要搞清楚谁是输入谁是输出,就能确定依赖关系,可自行分析。
对于前面的很多示例中,发现有些任务的输入输出文件设置的不是那么的明显。。比如前面修改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)
为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, taskOutput: (TaskT) -> FileSystemLocationProperty)
为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文件内容的示例代码。参考资料: