美文网首页
Gradle插件开发

Gradle插件开发

作者: 慕尼黑凌晨四点 | 来源:发表于2023-06-03 18:05 被阅读0次

    gradle 生命周期

    任务图(Task Graph)

    首先要明白Gradle 核心是基于依赖的编程。具体来说是当你定义了任务和任务之间的依赖,gradle得保证这些任务按照他们的依赖顺序执行,所以gradle在执行任务之前,会构建一个任务图(Task Graph)。通过配置,Gradle 会跳过不属于当前构建的任务的配置。

    在每个项目中,任务图最终会形成一个有向无环图(DAG)。

    gradle-task-graph.png

    构建阶段

    Gradle 构建具有三个不同的阶段。Gradle 按顺序运行这些阶段:首先是初始化,然后是配置,最后是执行。

    • 初始化

      • 检测settting.gradle文件。
      • 评估settting.gradle文件以确定哪些项目和包含的构建参与构建。
      • 为每个项目创建一个Project实例。
    • 配置

      • 评估参与构建的每个项目的构建脚本。
      • 为请求的任务创建任务图。
    • 执行

      • 按照依赖关系的顺序安排和执行每个选定的任务。
    // setting.gradle
    rootProject.name = 'basic'
    println 'This is executed during the initialization phase.'
    
    // build.gradle
    println 'This is executed during the configuration phase.'
    
    tasks.register('configured') {
        println 'This is also executed during the configuration phase, because :configured is used in the build.'
    }
    
    tasks.register('test') {
        doLast {
            println 'This is executed during the execution phase.'
        }
    }
    
    tasks.register('testBoth') {
        doFirst {
          println 'This is executed first during the execution phase.'
        }
        doLast {
          println 'This is executed last during the execution phase.'
        }
        println 'This is executed during the configuration phase as well, because :testBoth is used in the build.'
    }
    

    具体来说,当以上gradle文件执行任务时,会先运行setting.gradle,比如Android项目中会有的include ':app',也会在这时注册app项目,创建其project实例。

    然后在怕配置阶段会执行build.gradle,在这里会创建三个任务,但不会立即执行。

    所以当执行以下命令时显示如下:

    > gradle test testBoth
    This is executed during the initialization phase.
    
    > Configure project :
    This is executed during the configuration phase.
    This is executed during the configuration phase as well, because :testBoth is used in the build.
    
    > Task :test
    This is executed during the execution phase.
    
    > Task :testBoth
    This is executed first during the execution phase.
    This is executed last during the execution phase.
    
    BUILD SUCCESSFUL in 0s
    2 actionable tasks: 2 executed
    

    而gradle也为我们准备了一些钩子函数对其生命周期的各个阶段进行监听:

    // 项目评估监听
    gradle.beforeProject { project ->
        project.ext.set("hasTests", false)
    }
    
    gradle.afterProject { project ->
        // ...
    }
    
    // 任务监听
    gradle.taskGraph.beforeTask { Task task ->
        println "executing $task ..."
    }
    
    gradle.taskGraph.afterTask { Task task, TaskState state ->
        if (state.failure) {
            println "FAILED"
        }
        else {
            println "done"
        }
    }
    

    gradle插件

    gradle的核心是上述的任务自动化,但其所有的功能(如编译 Java 代码的能力)都是由插件提供的。gradle插件有点类似于代码中封装的方法,可以通过特定的配置,封装并且扩展项目的功能,减少多个项目维护相似的逻辑的开销。

    插件类型分为两种:二进制插件和脚本插件。因为二进制插件可以以外部jar包的形式提供,所以通常运用的更普遍,这里以二进制插件为例。

    声明插件

    // build.gradle
    plugins {
        id 'java' // 核心插件
        id 'com.jfrog.bintray' version '1.8.5' // 第三方社区插件
        // id «plugin id» version «plugin version» [apply «false»]
    }
    

    plugins块有如下限制:

    • 该plugins {}块还必须是构建脚本中的顶级语句。它不能嵌套在另一个构造中(例如 if 语句或 for 循环)。
    • 该plugins {}块目前只能在项目的构建脚本和 settings.gradle 文件中使用。它不能用于脚本插件或初始化脚本。

    管理插件

    可以在setting.gradle中配置pluginManagement {}块管理插件。它必须是文件中的第一个块

    // setting.gradle
    pluginManagement {
        plugins {
            id 'com.example.hello' version "${helloPluginVersion}"
        }
        repositories {
             maven {
                url './maven-repo'
            }
            gradlePluginPortal()
        }
    }
    
    // build.gradle
    plugins {
        id 'com.example.hello'
    }
    

    约定插件

    如果想定义一个自己的插件,可以新建一个module,或者在项目的buildSrc目录中新建build.gradle,并配置如下:

    plugins {
        id 'java-gradle-plugin'
    }
    
    // 
    gradlePlugin {
        plugins {
            myPlugins {
                id = 'my-plugin'
                implementationClass = 'my.MyPlugin'
            }
        }
    }
    

    这其实是 Java Gradle Plugin 提供的一个简化 API,其背后会自动帮我们创建一个 [插件ID].properties 配置文件,Gradle 就是通过这个文件类进行匹配的。如果你不使用 gradlePlugin API,直接手动创建 [插件ID].properties 文件,作用是完全一样的。

    // my-plugin.properties
    implementation-class=my.MyPlugin
    

    然后在module中新建java/groovy/kotlin文件MyPlugin,继承自Plugin<Project>,并重写apply方法实现插件的逻辑。

    class CyPlugin : Plugin<Project> {
        override fun apply(project: Project) {
            // apply
        }
    }
    

    发布插件

    通过 maven-publish 或 ivy-publish 发布:

    // build.gradle
    plugins {
        id 'java-gradle-plugin'
        id 'maven-publish'
        id 'ivy-publish'
    }
    
    group 'com.example'
    version '1.0.0'
    
    gradlePlugin {
        plugins {
            hello {
                id = 'com.example.hello'
                implementationClass = 'com.example.hello.HelloPlugin'
            }
            goodbye {
                id = 'com.example.goodbye'
                implementationClass = 'com.example.goodbye.GoodbyePlugin'
            }
        }
    }
    
    publishing {
        repositories {
            maven {
                url layout.buildDirectory.dir("maven-repo")
            }
            ivy {
                url layout.buildDirectory.dir("ivy-repo")
            }
        }
    }
    

    应用插件

    在Gradle构建工具中,可以用buildScript{} 块来定义构建脚本自身的依赖关系,将已作为外部 jar 文件发布的二进制插件添加到项目中。

    buildscript {
        repositories {
            gradlePluginPortal()
        }
        dependencies {
            classpath 'com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.5'
        }
    }
    
    apply plugin: 'com.jfrog.bintray'
    

    限制:buildscript {}块必须放在plugin {}块之前。

    TASK Transform ASM

    TASK

    通用task

    // android项目的clean task
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    // 自定义task
    tasks.register('hello')
    // 自定义Copy类型的task
    tasks.register('copy', Copy)
    

    自定义task

    首先继承defaultTask:

    abstract class MyTask : DefaultTask() {
        @get:OutputFile
        abstract val outputFile: RegularFileProperty
    
        @get:Input
        abstract val inputCount: Property<Int>
    
        @TaskAction
        fun action() {
            // task执行的代码
            val outputFile = outputFile.get().asFile
            outputFile.delete()
            outputFile.parentFile.mkdirs()
            Files.write(outputFile.toPath(), ("Count is: " + inputCount.get()).toByteArray())
            println("MyTask Output file is: " + outputFile.toPath())
        }
    }
    

    注册:

    project.tasks.register("myTask", MyTask::class.java) {
                    it.inputCount.set(10)
                    it.outputFile.set(File("build/myTask/output/file.txt"))
                }
    

    上述任务执行的结果是生成app/build/myTask/output/file.txt,内容是

    Count is: 10
    

    Action

    其实Task的本质是一组被顺序的Action对象构成。可以把Action理解为一段代码块。可通过在Task中添加doFirst{}和doLast{}来为Task执行Action的开始和结束添加Action。

    task clean(type: Delete) {
       delete rootProject.buildDir
       doLast {
           println(prefix + "Android Studio auto add clean task do last")
       }
       doFirst {
           println(prefix + "Android Studio auto add clean task do first")
       }
    }
    

    task执行顺序

    1. B.dependsOn A:先执行完ATask,在执行BTask;
    2. B.mushRunAfter A:先执行完ATask,在执行BTask
    3. B.mushRunAfter A C.mushRunAfter A:按照ATask、BTask、CTask顺序执行
    4. B.shouldRunAfter A:先执行完ATask,在执行BTask

    Transform

    Transform API 是 AGP1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class转Dex的过程中修改 Class 字节码。

    Android 打包.png

    自定义Transform流程:

    public class DemoTransform extends Transform {
        Project project;
    
        public DemoTransform(Project project) {
            this.project = project;
        }
    
        // transform任务名字(用于尾部拼接)
        // 最终会生成 transformClassesWithDemoTransformForDebug 的Task
        @Override
        public String getName() {
            return "DemoTransform";
        }
    
        // Transform需要处理的类型
        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS;
        }
    
        // transform作用域,要处理所有class字节码,Scope我们一般使用TransformManager.SCOPE_FULL_PROJECT
        @Override
        public Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT;
        }
    
        // 增量编译开关,true只有增量编译时才回生效
        @Override
        public boolean isIncremental() {
            return true;
        }
    
        @Override
        public void transform(TransformInvocation transformInvocation) throws IOException {
            System.out.println("============ DemoTransform 开始执行============");
            //消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
            final Collection<TransformInput> inputs = transformInvocation.getInputs();
            //引用型输入,无需输出。
            final Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs();
            //OutputProvider管理输出路径
            final TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    
            for (TransformInput input : inputs) {
                // 处理jar文件
                for (JarInput jarInput : input.getJarInputs()) {
                    System.out.println("jar= " + jarInput.getName());
                    File dest = outputProvider.getContentLocation(
                            jarInput.getFile().getAbsolutePath(),
                            jarInput.getContentTypes(),
                            jarInput.getScopes(),
                            Format.JAR);
                    // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
                    FileUtils.copyFile(jarInput.getFile(), dest);
                }
    
                // 处理class
                for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                    if (directoryInput.getFile().isDirectory()) {
                        for (File file : FileUtils.getAllFiles(directoryInput.getFile())) {
                            System.out.println("directoryInput--" + file.getName());
                        }
                    }
                    File dest = outputProvider.getContentLocation(
                            directoryInput.getName(),
                            directoryInput.getContentTypes(),
                            directoryInput.getScopes(),
                            Format.DIRECTORY);
                    //建立文件夹
                    FileUtils.mkdirs(dest);
                    //将class文件及目录复制到dest路径
                    FileUtils.copyDirectory(directoryInput.getFile(), dest);
                }
    
            }
    
        }
    }
    

    这里的关键是重写transform方法,里面可以从TransformInvocation中拿到所有打包到apk过程中需要的jar文件和class文件,所以这里我们就有机会对class文件进行干预,比如说分析或者改变class字节码。然后我们拿到dest目录(也就是我们拿到了这些文件,处理后需要将其移交给下一个transform),并将干预后的jar/class文件放到dest目录下。

    需要注意的是这里我们就算什么文件都不想干预,也必须要将文件复制到dest目录下,否则打包会失败。

    注册transform:

    val appExtension = project.extensions.getByType(AppExtension::class.java)
    appExtension.registerTransform(DemoTransform(project))
    

    由此可见transform的效率并不高,因为每一个transform都会遍历整个打包过程中的jar/class文件,并且每一个transform我们都需要写上一堆重复的代码(获取jar - 遍历jar - 拿到class - 处理class - 打包jar - 复制/移动到dest目录)。

    所以在AGP 7.0以后,transform被标记为弃用,并在AGP 8.0中被移除。取而代之的是 TransformActionAsmClassVisitorFactory

    AGP 8.0 变化

    以下是 AGP 8.0 的重要 API 更新

    移除了 Transform API

    从 AGP 8.0 开始,Transform API 将被移除。这意味着,软件包 com.android.build.api.transform 中的所有类都会被移除。

    Transform API 即将被移除,以提高 build 的性能。使用 Transform API 的项目会强制 AGP 对 build 使用优化程度不够的流程,从而导致构建时间大幅增加。同时也很难使用 Transform API 以及将其与其他 Gradle 功能结合使用;这些替代 API 可让您更轻松地扩展 AGP,而不会引起性能问题或 build 正确性问题。

    替代 API

    Transform API 没有单一的替代 API,每个用例都会有新的针对性 API。所有替代 API 都位于 androidComponents {} 代码块中,在 AGP 7.2 中均有提供。

    支持转换字节码

    如需转换字节码,请使用 Instrumentation API。对于库,您只能为本地项目类注册插桩;对于应用和测试,您既可以选择仅为本地类注册插桩,也可以选择为所有类(包括本地和远程依赖项)注册插桩。为了使用此 API,每个类上的插桩都是独立运行的,并且对类路径中其他类的访问会受到限制(如需了解详情,请参见 createClassVisitor())。此限制提高了完整 build 和增量 build 的性能,并使得 API Surface 变得简单。每个库一旦准备就绪,即会进行并行插桩;而不是在所有编译完成后进行插桩。此外,如果是在单个类中做出更改,则意味着只有受影响的类必须在增量 build 中重新进行插桩。如需查看 Instrumentation API 使用方法的示例,请参阅使用 ASM 转换类 AGP 配方。

    TransformAction

    参考:Transform 被废弃,TransformAction 了解一下~

    Transform API是由AGP提供的,而Transform Action则是由Gradle提供。不光是 AGP 需要 Transform,Java 也需要,所以由 Gradle 来提供统一的 Transform API。

    关于 TransformAction 如何使用,Gradle 官方已经提供了很详细的文档–Transforming dependency artifacts on resolution,与 AGP 类似,也是需要先注册,只不过 AGP 是通过 Android Extension 来注册 Transform ,Gradle 是通过 DependencyHandler 来注册 TransformAction ,差异并不算很大。

    // Plugin#apply()
    val artifactType = Attribute.of("artifactType", String::class.java)
    project.dependencies.registerTransform(MyTransformAction::class.java) {
        it.from.attribute(artifactType, "jar")
        it.to.attribute(artifactType, "my-custom-type")
    }
    
    abstract class MyTransformAction : TransformAction<TransformParameters.None> {
        @get:PathSensitive(PathSensitivity.NAME_ONLY)
        @get:InputArtifact
        abstract val inputArtifact: Provider<FileSystemLocation>
    
        override fun transform(outputs: TransformOutputs) {
            val file = inputArtifact.get().asFile;
            println("Processing $file. File exists = ${file.exists()}")
            if (file.exists()) {
                val outputFile = outputs.file("copy");
                Files.copy(file.toPath(), outputFile.toPath())
            } else {
                throw RuntimeException("File does not exist: " + file.canonicalPath);
            }
        }
    }
    

    具体的使用也可以看看AGP中自带的JetifyTransformAarTransform

    AsmClassVisitorFactory

    AGP 8.0文档中也提到了对字节码转换的支持,具体来说,就是AGP为我们又做了一层封装,提供了AsmClassVisitorFactory来方便我们使用Transform Action进行ASM操作。

    ASM(全称:Java ASM)是一种 Java 字节码操纵框架,官网:https://asm.ow2.io/

    如果是用transform api + asm 的方式实现字节码插桩,我们需要写很多模板式的代码,具体可以看看sensor埋点的实现。

    但其实对于ASM而言,我们只需要通过提供不同的classVisitor实例,就可以实现我们特定的需求,至于怎么找到class,怎么通过classVisitor访问class就全是模板代码了,所以AsmClassVisitorFactory的发布就是为了解决这个痛点。

    val androidComponents = project.extensions.getByType(AndroidComponentsExtension::class.java)
    androidComponents.onVariants {
            it.transformClassesWith(LogAsmTransform::class.java, InstrumentationScope.ALL) {
                // it -> InstrumentationParameters 携带参数
            }
        }
    
    abstract class LogAsmTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {
    
        override fun createClassVisitor(
            classContext: ClassContext,
            nextClassVisitor: ClassVisitor
        ): ClassVisitor {
            // 返回一个 ClassVisitor 对象,其内部实现了我们修改 class 文件的逻辑
            return object : ClassVisitor(Opcodes.ASM9, nextClassVisitor) {
                val className = classContext.currentClassData.className
    
                // 这里,由于只需要修改方法,故而只重载了 visitMethod 找个方法
                override fun visitMethod(
                    access: Int,
                    name: String?,
                    descriptor: String?,
                    signature: String?,
                    exceptions: Array<out String>?
                ): MethodVisitor {
                    val oldMethodVisitor =
                        super.visitMethod(access, name, descriptor, signature, exceptions)
                    // 返回一个 MethodVisitor 对象,其内部实现了我们修改方法的逻辑
                    return LogMethodVisitor(className, oldMethodVisitor, access, name, descriptor)
                }
            }
        }
    
        override fun isInstrumentable(classData: ClassData): Boolean {
            return true
        }
    }
    

    至于ASM的使用,又是一个大的范畴,故不在此篇做讲解。

    相关文章

      网友评论

          本文标题:Gradle插件开发

          本文链接:https://www.haomeiwen.com/subject/vzsvedtx.html