Kotlin版Gradle插件开发

Gradle 是一个开源的构建自动化系统,可以帮助我们操作构建的过程及其逻辑。例如,当我们构建一个Android 应用时,Gradle 会编译所有代码并为我们创建一个 APK。

Gradle 分三个阶段评估和运行构建,分别是 Initialization (初始化)、Configuration (配置) 和 Execution (执行)

在 Initialization (初始化) 阶段,Gradle 会决定构建中包含哪些项目,并会为每个项目创建 Project 实例。为了决定构建中会包含哪些项目,Gradle 首先会寻找 settings.gradle 来决定此次为单项目构建还是多项目构建。

在 Configuration (配置) 阶段,Gradle 会评估构建项目中包含的所有构建脚本,随后应用插件、使用 DSL 配置构建,并在最后注册 Task,同时惰性注册它们的输入。

需要注意的是,无论请求执行哪个 Task,配置阶段都会执行。为了保持构建简洁高效,请避免在配置阶段执行任何耗时操作。

最后,在 Execution (执行) 阶段,Gradle 会执行构建所需的 Task 集合。

Gradle Task

Gradle 中有 Task,每个 Task 都代表构建的单个原子工作。我们可以创建自定义 Task ,也可以创建包含多个 Task 的插件。

要查看所有 Task 的列表,可以打开项目的根目录在终端中运行./gradlew tasks 或者 gradle tasks 打印所有任务。

gradlew 是 Gradle Wrapper,它可以让我们无需在计算机上安装 Gradle,Wrapper 会为我们完成,并且它允许我们使用不同版本的 Gradle 构建不同的项目。

一个 Task 由一系列的 Action 对象组成,当 Task 执行时,通过调用 Action.execute(),每个动作依次执行。我们可以通过调用 doFirst(Action) 或 doLast(Action) 向任务中添加 Action 。

Task 可能与其他 Task 有依赖关系,也可能被调度为总是在另一个 Task 之后运行。Gradle 确保在执行 Task 时,所有的 Task 依赖项和排序规则都被尊重,所以 Task 会在所有的依赖项和任何“必须在” Task 执行完之后才执行。

我们可以通过在模块的 build.gradle 文件来编写自己的 Task :

task myTask {
  println("Hello " + project.parent?.name)
  doLast {
    println("Hello " + project.name)
  }
}

还有一种写法:

tasks.register("hello") {
  println("Hello " + project.parent?.name)
  doLast {
    println("Hello " + project.name)
  }
}

Gradle Plugin

那什么是 Gradle 插件呢 ? 其实 Gradle 插件就是我们希望一起执行的一组 Task。

一般来说有3种方法可以编写 Gradle 插件:

Build script

将插件的源代码直接放入 build.gradle 文件中,这种方法的优点是该类会自动编译并添加到构建脚本的类路径中,而无需我们进行任何配置,缺点是插件不能在另一个模块中使用。

buildSrc project

另一种方法是将插件的源代码放在 rootProjectDir/buildSrc/src/main/java 目录中,这样做的好处是该插件可以在整个项目中使用,而不仅限于一个模块。

buildSrc 是 Gradle 在编译时查看的目录。运行 Gradle 时,它会检查是否存在名为 buildSrc 的目录。然后,Gradle 会自动编译和测试此代码,并将其放在构建脚本的类路径中,无需提供任何说明。而且它还支持 Kotlin DSL 。

独立 project

我们还可以为插件创建一个单独的项目。这样做的好处是,在构建项目之后,会生成一个 JAR 文件,它可以在我们的任何 Android Studio 项目中使用。

在本文中我们将使用第3种方式来编写 Gradle 插件。首先在 Android 项目中创建一个单独的模块,用于开发我们的插件,这样就可以发布插件与并其他项目共享。

选择 File ▸ New ▸ New Module,然后选择 Java or Kotlin library ,模块名称为:plugin,类名:RouterPlugin,语言记得选择 Kotlin,填写信息后完成创建。
在这里插入图片描述
打开 plugin 模块的 build.gradle,在 plugins 任务中添加插件:java-gradle-plugin 和 maven 。

使用 Java Gradle 插件将自动添加 gradleApi() 依赖项,在生成的 JAR 文件中生成所需的插件描述符。

plugins {
    id 'java'
    id 'java-gradle-plugin'
    id 'maven'
    id 'org.jetbrains.kotlin.jvm'
}

dependencies {
    implementation "com.android.tools.build:gradle:$gradle_version"
}

然后打开 RouterPlugin 实现 org.gradle.api.Plugin 接口,泛型选择org.gradle.api.Project,在apply函数中,注册我们创建的Task,代码如下所示:

import org.gradle.api.Plugin
import org.gradle.api.Project

class RouterPlugin : Plugin<Project> {

    lateinit var project: Project

    override fun apply(project: Project) {
        this.project = project
        // TODO
    }
}

接下来我们将给插件的 apply() 函数中创建 Task:

project.task("renameApk") { task ->
  task.doLast {
    val apkPath = "/outputs/apk/release/app-release-unsigned.apk"
    val renamedApkPath = "/outputs/apk/release/RenamedAPK.apk"
    val previousApkPath = "${project.buildDir.absoluteFile}$apkPath"
    val newPath = File(previousApkPath)

    if (newPath.exists()) {
      val newApkName = "${project.buildDir.absoluteFile}$renamedApkPath"
      newPath.renameTo(File(newApkName))
    } else {
      println("Path does not exist!")
    }
  }
}.dependsOn("build")

创建一个名为 renameApk 的 Task,找到 APK 所在的位置,然后对其进行重命名。最后一行 .dependsOn("build") 的作用是如果 APK 还没有创建或删除了APK文件,此函数将创建对将创建 APK 的构建任务的依赖项。

我们可以继续像创建 renameApk 一样在 apply 函数中创建更多的Task,但更好的办法是将 Task 封装成类以保持功能单一和可重用。

在 plugin 模块中创建一个名为 RenameApk 的 Kotlin 类,代码如下:

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
import java.io.File

open class RenameApk : DefaultTask() {
    
    @TaskAction
    fun renameApk() {
        val apkPath = "/outputs/apk/release/app-release-unsigned.apk"
        val renamedApkPath = "/outputs/apk/release/RenamedAPK.apk"
        val previousApkPath = "${project.buildDir.absoluteFile}$apkPath"
        val newPath = File(previousApkPath)

        if (newPath.exists()) {
            val newApkName = "${project.buildDir.absoluteFile}$renamedApkPath"
            newPath.renameTo(File(newApkName))
        } else {
            println("Path does not exist!")
        }
    }
    
}

要创建自己的 Task,需要继承 DefaultTask 类,并使用 @TaskAction 注解函数,然后在其中编写逻辑。

要在插件中使用这个 Task ,请打开 RouterPlugin 类,然后在 apply() 中,将现有代码替换为:

import org.gradle.api.Plugin
import org.gradle.api.Project

class RouterPlugin : Plugin<Project> {

    lateinit var project: Project

    override fun apply(project: Project) {
        this.project = project
        project.tasks.register("renameApk", RenameApk::class.java) {
            it.dependsOn("build")
        }
    }
}

这样我们就将 RenameApk 类中创建的 Task 注册到了 RouterPlugin 插件类中。同步项目并打开终端,运行 gradle clean 来清理项目,然后运行 gradle -q renameApk

注册和发布插件

接下来我们将注册这个插件,为此,我们必须创建一个特殊文件:

展开 plugin ▸ src ▸ main,添加一个名为 resources 的新目录。然后在资源中添加一个子目录并将其命名为 META-INF(必须大写字母)。最后,在META-INF添加一个命名为 gradle-plugins 的子目录。

在 gradle-plugins 添加一个空文件并将其命名为:com.guiying712.router.properties,其中com.guiying712.router 就是插件的名称,根据前面的规则插件的使用方式如下:

apply plugin: 'com.guiying712.router'

打开文件 com.guiying712.router.properties 并将我们创建的插件的完全限定名称作为其内容。如下所示:

implementation-class=com.guiying712.plugin.RouterPlugin

项目结构将如下所示:
在这里插入图片描述
一切准备继续,然后我们将插件发布到本地仓库。打开 plugin 的 build.gradle 文件,添加以下内容:

install {
    repositories.mavenInstaller {
        pom.version = '0.0.1' // 配置插件版本号
        pom.artifactId = 'router-plugin' // 配置插件标识
        pom.groupId = 'com.guiying712.plugin' // 配置插件组织
    }
}

添加代码后同步Gradle更改。 此任务会将插件发布到本地仓库中。可以通过单击任务左侧栏的绿色播放按钮或在终端中执行命令来执行任务,命令如下:

./gradlew install 或者 gradle install

也可以直接打开 AndroidStudio 右侧的 Gradle 栏,按照下图来执行任务:
安装插件
为了验证插件是否发布到本地仓库,可以打开本地仓库目录进行查看,路径如下:

C:\Users\用户名\.m2\repository

然后打开 Project 的 build.gradle 文件,在 buildscript 块中的 repositories 和 dependencies 中添加以下内容:

打开 Module 的 build.gradle 文件,然后在Gradle文件的顶部添加以下内容:

apply plugin: 'com.android.application'

apply plugin: 'com.guiying712.router'

最后运行 Gradle Clean命令,然后从新编译项目。

调试Gradle Plugin

1、在AndroidStudio 中选择 Edit Configurations :
1
2、然后新建 Remote 配置,命名并保存:
2
3、打开 AndroidStudio 的 Terminal ,执行以下命令:

gradle app:mytask -Dorg.gradle.debug=true

mytask 就是要调试的任务名称。

4、当命令行执行到 Starting Daemon 时,在需要调试的地方打上断点,然后运行 debug 按钮,稍等一下(速度比较慢耐心点) 就会进入到断点所在位置,然后就可以一步步进行调试了。
在这里插入图片描述

什么是Transform

Transform 是由 Android Gradle Plugin 提供的API,用于在 .class to dex 过程中对 class 进行处理。开发者可以在将代码转换为 dex 文件之前,使用 Transform 做一些额外的工作。

对于每个添加的 Transform ,都会创建一个新 Task。添加 Transform 的操作负责处理 Task 之间的依赖关系,这是根据转换过程完成的。Transform 的输出可以被其他 Transform 使用,并且这些 Task 会自动链接在一起。

以下是 Transform 的特性:

  • Transform 指示它适用于什么(内容-content、范围-scope)以及它生成什么(内容-content)。

  • Transform 接收一个 TransformInput 的集合作为输入,该集合由 JarInputs 和 DirectoryInputs 组成。两者都提供有关与其特定内容关联的 QualifiedContent.Scopes 和 QualifiedContent.ContentTypes 的信息。

  • Transform 的输出由 TransformOutputProvider 处理,它允许创建新的自包含内容,每个都与自己的 Scopes 和 Content Types 相关联。Transform输入/输出 处理的内容由转换系统管理,它们的位置是不可配置的。

  • 最佳做法是写入与 Transform 接收到的 Jar/Folder 输入一样多的输出。将所有输入合并到一个输出中,可以防止下游 Transform 处理有限的范围。

  • 虽然可以通过文件扩展名区分不同的 Content Types,但对于 Scopes 无法这样做。因此,如果 Transform 请求一个 Scope,但唯一可用的输出包含的内容超过了请求的Scope ,则构建将失败。

  • 如果 Transform 请求单一内容类型,但唯一可用的内容包含的内容多于所请求的类型,则输入文件/文件夹将包含所有类型的所有文件,但 Transform 应该只读取、处理和输出它所请求的类型。

  • 此外,Transform 可以指示辅助的输入/输出。这些辅助的输入/输出不由上游或下游 Transform 处理,并且不受 Transform 处理的类型的限制,它们可以是任何东西。由每个 Transform 来管理这些文件的位置,并确保在调用 Transform 之前生成这些文件。这是在注册 Transform 时通过附加的参数完成的。

  • 辅助的输入/输出 允许 Transform 读取但不处理任何内容。可以通过让 getScopes() 返回一个空列表,并使用 getReferencedScopes() 来指示要读取的内容来实现。
    在这里插入图片描述

注意: Android Gradle Plugin Transform API 不是最终版本,AGP 4.1.x / 4.2.0 以后版本没有公共 BaseExtension#registerTransform(...) 方法,新的 ApplicationExtension.kt 也没有导出它。

编写Transform

在 plugin 模块中创建一个名为 RouterTransform 的 Kotlin 类,代码如下:

class RouterTransform : Transform() {

    override fun getName(): String = "Router" // 1

    override fun getInputTypes(): Set<QualifiedContent.ContentType> { // 2
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> { // 3
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun isIncremental(): Boolean = false // 4

    override fun transform(transformInvocation: TransformInvocation?) { // 5
        // TODO
    }
}

解释下上面的代码:

1、返回这个 Transform 的唯一名称,在这里命名为 “Router” 。
2、返回 Transform 要消费的数据类型,可能不止一种类型。TransformManager.CONTENT_CLASS 代表此 Transform 消费被编译过的Java代码,也就是 .class 类型 。

 public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
  • 1

3、返回 Transform 的范围,用来指示 Transform 作用的范围。TransformManager.SCOPE_FULL_PROJECT 代表此 Transform 的作用范围是当前项目、子项目以及外部的依赖库。

public static final Set<ScopeType> SCOPE_FULL_PROJECT =
       ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);

4、返回 Transform 是否可以执行增量工作。如果可以增量工作,则 TransformInput 可能包含 更改/删除/添加 文件的列表,除非其他东西触发了非增量运行。
5、执行转换。

我们在 RouterTransform 类的 transform() 函数中没有添加任何转换逻辑,因此这是一个空的自定义 Transform。

为了在 Gradle 插件中注册这个 Transform,我们先在模块中创建一个命名为 GradleExt 的 Kotlin 文件,代码如下:

import com.android.build.gradle.BaseExtension
import org.gradle.api.Project

val Project.androidType: BaseExtension
    get() = extensions.getByType(BaseExtension::class.java)

val Project.hasAndroid: Boolean
    get() = extensions.findByName("android") is BaseExtension

val Project.android: BaseExtension
    get() = extensions.getByName("android") as BaseExtension

然后在 RouterPlugin 中注册这个自定义 Transform ,代码如下:

import org.gradle.api.Plugin
import org.gradle.api.Project

class RouterPlugin : Plugin<Project> {

    lateinit var project: Project

    override fun apply(project: Project) {
        this.project = project
        project.tasks.register("renameApk", RenameApk::class.java) {
            it.dependsOn("build")
        }
        if (project.hasAndroid) {
        	project.android.registerTransform(RouterTransform(project))
        }
    }
}

然后从新发布插件到本地仓库,并同步项目。

打开Gradle侧边栏,可以在 KotlinRouter/Tasks/other 路径下看到 transform 命名开头的 Task:
在这里插入图片描述
对于我们添加的每个自定义 Transform ,都会创建一个名为 transformClassesWithXxxForDebug 的 Task,相信大家都能看到这其中规律。没错,Xxx 正是 Transform#getName() 返回的名称。
transform任务名称
最后点击 Run 按钮生成 apk 文件,但是当安装 apk 并启动应用程序时,它崩溃了。错误如下:
安装失败
在执行编译的过程中会生成对应的目录,在 /app/build/intermediates/transforms 目录下生成了一个名为 Router 的目录,这个名称也是根据自定义的 Transform#getName() 方法返回的字符串来创建的。
class文件不存在
可以看到在 Router 目录下没有任何内容。找到 outputs 目录并打开生成的 apk 文件,我们发现里面缺少了 classes.dex ,这就是安装失败的原因。

当我们自定义 Transform 时,必须处理输入文件并将它们写入到输出文件夹。即使我们不处理类文件,也必须将它们复制到输出文件夹。如果不这样做,所有的类文件都会被删除。

在 transform() 方法中,我们可以获取输入和输出文件夹,代码如下:

override fun transform(transformInvocation: TransformInvocation) {
    if (!transformInvocation.isIncremental) {
        transformInvocation.outputProvider.deleteAll()
    }

    val inputs = transformInvocation.inputs.flatMap { it.directoryInputs + it.jarInputs }
    val outputs: List<File> = inputs.map { input ->
        val format = if (input is JarInput) Format.JAR else Format.DIRECTORY
        // Transforms 可以要求获取给定范围、内容类型和格式的位置
        // 如果格式为 Format.DIRECTORY,则结果为目录的文件位置
        // 如果格式是 Format.JAR,那么结果是一个代表要创建的 jar 的文件
        transformInvocation.outputProvider.getContentLocation(
            input.name,
            input.contentTypes,
            input.scopes,
            format
        )
    }
    process(inputs.map { it.file }, outputs)
}

Transform 的输入被打包成一个 TransformInvocation 的实例:

  • 返回一个 TransformInput 的输入集合。代表 Transform 要消费的输入,这些输入被转换过后的版本必须写入到输出,可以通过 getInputTypes() 和 getScopes() 来控制接收的内容。
  • 返回一个 TransformInput 的 referencedInputs 集合。仅供参考,请勿转换,接收到的内容是通过 getReferencedScopes() 控制的。

如果 Transform 不想消费任何东西只想查看某些输入内容,应该在 getScopes() 中返回一个空集,并在 getReferencedScopes() 中返回它想要查看的内容。

最后,我们必须将所有文件从输入复制到输出,代码如下:

fun process(inputs: List<File>, outputs: List<File>) {
    inputs.zip(outputs) { input, output ->
        when {
            input.isDirectory -> {
                FileUtils.copyDirectory(input,output)
            }
            input.name.endsWith(SdkConstants.DOT_JAR) -> {
                FileUtils.copyFile(input,output)
            }
        }
    }
}

再次打开 /app/build/intermediates/transforms 目录,可以看到 Router 目录下的全部内容:
在这里插入图片描述
此外 Router 目录下还会生成一个名为 __content__.json 的文件,该文件中描述了 Router 目录下的内容:

[
  {
    "name": "androidx.databinding:viewbinding:3.6.0",
    "index": 0,
    "scopes": [
      "EXTERNAL_LIBRARIES"
    ],
    "types": [
      "CLASSES"
    ],
    "format": "JAR",
    "present": true
  },
  ... // 精简了序号 1-37 的 JAR 描述
  {
    "name": "ARouter",
    "index": 38,
    "scopes": [
      "PROJECT"
    ],
    "types": [
      "CLASSES"
    ],
    "format": "DIRECTORY",
    "present": true
  }
]

Transform 和 Task 之间的关系

一般查看源码最佳的姿势是从使用上入手。Transform 有两个地方入手,一个是 Plugin注册 Transform 的方法。一个是 Transform#transform()调用。

首先我们看下 BaseExtension 类中 Transform 相关的内容,源码如下:

private final List<Transform> transforms = Lists.newArrayList();

public void registerTransform(@NonNull Transform transform, Object... dependencies) {
    transforms.add(transform);
    transformDependencies.add(Arrays.asList(dependencies));
}

@Override
@NonNull
public List<Transform> getTransforms() {
    return ImmutableList.copyOf(transforms);
}

从上面的源码可以看出,registerTransform() 是将 Transform 添加到一个列表中,然后对外暴露了这个列表。接下来只要找到哪里调用了 getTransforms() 即可。

通过追踪代码,发现 TaskManager#createPostCompilationTasks() 中调用了 BaseExtension#getTransforms() ,源码如下:

public void createPostCompilationTasks(
@NonNull final VariantScope variantScope) {
    ...
    // ----- External Transforms -----
    // apply all the external transforms.
    List<Transform> customTransforms = extension.getTransforms();
    List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();

    for (int i = 0, count = customTransforms.size(); i < count; i++) {
        Transform transform = customTransforms.get(i);
        List<Object> deps = customTransformsDependencies.get(i);
        transformManager.addTransform(
            taskFactory,
            variantScope,
            transform,
            null,
            task -> {
        if (!deps.isEmpty()) {
            task.dependsOn(deps);
        }
    },
        taskProvider -> {
        // if the task is a no-op then we make assemble task depend on it.
        if (transform.getScopes().isEmpty()) {
            TaskFactoryUtils.dependsOn(
                variantScope.getTaskContainer().getAssembleTask(),
                taskProvider);
        }
    });
    }
    ...
}

从上面源码可以看出,创建编译任务时会应用所有的外部转换,也就是我们自定义的 Transform 。具体方法是通过 for 循环遍历所有的外部转换,然后使用 TransformManager 添加 Transform 。

在创建Transform 时我们使用过 TransformManager 中定义的常量。这里

@NonNull
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
@NonNull TaskFactory taskFactory,
@NonNull VariantScope scope,
@NonNull T transform,
@Nullable PreConfigAction preConfigAction,
@Nullable TaskConfigAction<TransformTask> configAction,
@Nullable TaskProviderCallback<TransformTask> providerCallback) {
    ...
    List<TransformStream> inputStreams = Lists.newArrayList();
    String taskName = scope.getTaskName(getTaskNamePrefix(transform));

    // get referenced-only streams
    List<TransformStream> referencedStreams = grabReferencedStreams(transform);

    // find input streams, and compute output streams for the transform.
    IntermediateStream outputStream = findTransformStreams(
            transform,
    		scope,
    		inputStreams,
   			taskName,
    		scope.getGlobalScope().getBuildDir());
    ...
    transforms.add(transform);
    // create the task...
    return Optional.of(
        taskFactory.register(
            new TransformTask.CreationAction<>(
                    scope.getFullVariantName(),
            taskName,
            transform,
            inputStreams,
            referencedStreams,
            outputStream,
            recorder),
        preConfigAction,
        configAction,
        providerCallback));
}

TaskFactory 是一个接口,它的唯一实现是TaskFactoryImpl ,因此只需要查看TaskFactoryImpl#register() 方法,源码如下:

override fun <T : Task> register(
    creationAction: TaskCreationAction<T>,
    secondaryPreConfigAction: PreConfigAction?,
    secondaryAction: TaskConfigAction<in T>?,
    secondaryProviderCallback: TaskProviderCallback<T>?
): TaskProvider<T> =
    taskContainer.registerTask(creationAction, secondaryPreConfigAction, secondaryAction, secondaryProviderCallback)

在 register() 方法我们又看到了熟悉的 registerTask() ,没错,跟 Task 章节中 Plugin#apply() 中 注册 Task的方法一样。

这也就证明了 每添加一个 Transform 就会创建 一个 Task 这句话。

本文中的源代码如下:

https://github.com/guiying712/KotlinRouter

Android Gradle 插件 API 更新:

https://developer.android.google.cn/studio/releases/gradle-plugin-api-updates?hl=zh-cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值