美文网首页
Gradle Plugin: Transform + Javas

Gradle Plugin: Transform + Javas

作者: Coralline_xss | 来源:发表于2018-11-21 12:07 被阅读78次
    一、Gradle 自定义插件步骤

    参考:https://www.jianshu.com/p/03eb55536298

    在Gradle中自定义插件,有三种方式:

    • 在 build.gradle 脚本中直接创建使用
    • 在 buildSrc 模块中使用
    • 在独立 Module 中使用

    对比这三种方式,各自优缺点如下:

    • 方式一比较快捷,但是可能不能为其它项目使用;
    • 方式二创建了 buildSrc 模块后,Android Studio 会直接识别其为插件模块,在主工程 .gradle 文件中可以直接 apply 插件,而不用引入 maven 或 jcenter 等仓库才能使用插件;
    • 方式三就需要使用引入 maven 或 jcenter 等仓库才能使用插件。

    另外,在 IDEA 中也能开发 Gradle 插件,但是在 Android Studio 中更利于进行插件依赖和调试。所以建议直接在 Android Studio 中创建插件,若提供给其他项目使用,则创建 maven 、jcenter 仓库上传脚本上传到远程仓库后进行远程依赖就行。

    以下所有的插件实现都是通过在 Android Studio 中创建 buildSrc 模块实现的。

    通过 buildSrc 方式自定义插件过程中遇见的问题:
    Q:定义了多个插件如何声明和使用?
    A:gradle-plugins 为声明插件的目录,项目中创建了多个 Plugin.groovy 文件,可以在这里创建多个 youPluginName.properties 文件,内容为:

    implementation-class=包名.插件类名
    

    使用时,直接在 app.gradle 中进行依赖:

    apply plugin: 'youPluginName'
    

    如果是远程maven等仓库依赖,则需要添加仓库地址,并且需要在项目根目录添加插件版本 classPath 。

    Q:buildSrc 插件模块,在定义插件时,如何使用第三方依赖?
    A:同一般依赖引用,比如下面要使用 Transform + Javassist 进行操作字节码,则需要同时加入 gradle 和 javassist 远程依赖,在 buildSrc 模块下的 .gradle 文件配置如下:

    apply plugin: 'groovy'
    
    sourceCompatibility = 1.8
    targetCompatibility = 1.8
    
    dependencies {
        repositories {
            google()
            mavenLocal()
            jcenter()
        }
    
        implementation gradleApi()    // gradle sdk
        implementation localGroovy()  // groovy sdk
        
        // transform 时需要用到gradle tool的api,需要单独引入
        implementation 'com.android.tools.build:gradle:3.1.3'
        implementation 'com.android.tools.build:gradle-api:3.1.3'
        implementation 'org.javassist:javassist:3.20.0-GA'
    }
    
    二、Javassist + Task 自动生成 .java 文件

    按照上面的配置,先来试一下如何使用 javassit 在编译期自动生成 java 代码。
    具体场景:在系统自动生成 BuildConfig.java 文件后(也就是系统内置任务 generateDebugBuildConfig 之后新建任务执行),自动生成我们自定义的 java 代码文件。

    具体 .groovy 代码如下:

    package com.coral.plugin
    
    import com.android.build.gradle.AppPlugin
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import com.android.build.gradle.AppExtension
    
    /**
     * desc: 利用 Javassist,在系统自动生成BuildConfig.java文件后,自动生成我们的java文件
     */
    public class CreateJavaPlugin implements Plugin<Project> {
    
        @Override
        void apply(Project project) {
            System.out.println("----------------Begin----------------")
            System.out.println("This is out custom plugin.")
    
            def android = project.extensions.getByType(AppExtension)
    
            // 注册一个Transform
            def classTransform = new MyTransform(project)
            android.registerTransform(classTransform)
    
            // 创建一个 Extension
            project.extensions.create("testCreateJavaConfig", CreateJavaExtension)
    
            // 生产一个类
            if (project.plugins.hasPlugin(AppPlugin)) {
                // 获取到 Extension,也即是 .gradle 文件中的闭包
                android.applicationVariants.all { variant ->
                    // 获取到 scope 作用域
                    def variantData = variant.variantData
                    def scope = variantData.scope
    
                    // 拿到 .gradle 中配置的 Extension 值
                    def config = project.extensions.getByName("testCreateJavaConfig")
    
                    // 创建一个 Task(名称为:coralDebugCreateJavaPlugin 或 coralReleaseCreateJavaPlugin)
                    def createTaskName = scope.getTaskName("coral", "CreateJavaPlugin")
                    def createTask = project.task(createTaskName)
    
                    // 设置 task 要执行的任务
                    createTask.doLast {
                        // 生成 java 类
                        createJavaTest(variant, config)
                    }
    
                    // 设置 task 依赖于生成 BuildConfig 的 task,然后在生成 BuildConfig 后生成我们的类
                    String generateBuildConfigTaskName = variant.getVariantData()
                            .getScope().getGenerateBuildConfigTask().name
                    // 任务名称:generateDebugBuildConfig
                    println("generateBuildConfigTaskName = " + generateBuildConfigTaskName)
    
                    def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                    if (generateBuildConfigTask) {
                        createTask.dependsOn generateBuildConfigTask
                        generateBuildConfigTask.finalizedBy createTask
                    }
                }
            }
    
            System.out.println("----------------Has it finished?----------------")
        }
    
        static void createJavaTest(variant, config) {
            println("---begin create: " + variant + ", " + config.str)
            // 要生成的内容
            def content = """package com.coral.demo;
    /**
    * Created by xss on 2018/11/20.
    */
    public class TestClass {
        public static final String str = "${config.str}";
    }
                          """
            // 获取到 BuildConfig 类的路径
            File outputDir = variant.getVariantData().getScope().getBuildConfigSourceOutputDir()
            // app/build/generated/source/buildConfig/debug
            println("outputDir = " + outputDir.absolutePath)
            def javaFile = new File(outputDir, "TestClass.java")
            javaFile.write(content, 'UTF-8')
            println("---create finished---")
        }
    }
    
    public class CreateJavaExtension {
        def str = "动态生成Java类的字符串"
    }
    

    在 app.gradle 文件中配置如下:

    // 自动生成 Java 类插件 
    apply plugin: 'myPluginCreateJava'
    
    testCreateJavaConfig {
        str = '动态生成Java类'
    }
    

    同步gradle 后,在Studio右侧 app -> Tasks -> other 可以看到自定义的任务:


    自定义任务名称

    双击执行任务编译成功后,在 app/build/generated/source/buildConfig/debug 目录下可以看到自动生成的 java 文件,在项目中可以进行直接引用该类。

    参考:http://www.10tiao.com/html/227/201709/2650241354/1.html

    三、Transform + Javassist 编译期注入代码到 .class文件

    使用 Transform + Javassit 操作字节码,需要在 .gradle 中添加 Transform 和 Javassist 的 API ,配置按上面的 .gradle 配置就行。

    具体场景:在项目的 MainActivity 的 onCreate() 方法内部插入一行代码。

    MyTransform.groovy 文件代码如下:

    package com.coral.plugin
    
    import com.android.build.api.transform.Context
    import com.android.build.api.transform.DirectoryInput
    import com.android.build.api.transform.Format
    import com.android.build.api.transform.JarInput
    import com.android.build.api.transform.Transform
    import com.android.build.api.transform.QualifiedContent
    import com.android.build.api.transform.TransformException
    import com.android.build.api.transform.TransformInput
    import com.android.build.api.transform.TransformOutputProvider
    import com.android.build.gradle.internal.pipeline.TransformManager
    import com.android.utils.FileUtils
    import org.apache.commons.codec.digest.DigestUtils
    import org.gradle.api.Project
    
    public class MyTransform extends Transform {
    
        Project project
    
        /**
         * 构造方法,保留原project备用
         */
        MyTransform(Project project) {
            this.project = project
        }
    
        /**
         * 设置自定义 Transform 对应的 Task 名称
         * 类似:TransformClassesWithPreDexForXXX,对应的 task 名称为:transformClassesWithMyTransformForDebug
         * 会生成目录 build/intermediates/transforms/MyTransform/
         */
        @Override
        String getName() {
            return "MyTransform"
        }
    
        /**
         * 指定输入的类型,可指定我们要处理的文件类型(保证其他类型文件不会传入)
         * CLASSES - 表示处理java的class文件
         * RESOURCES - 表示处理java的资源
         */
        @Override
        Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS
        }
    
        /**
         * 指定 Transform 的作用范围
         */
        @Override
        Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT
        }
    
        /**
         * 是否支持增量编译
         */
        @Override
        boolean isIncremental() {
            return false
        }
    
        /**
         * 核心方法,具体如何处理输入和输出
         * @param inputs          为传过来的输入流,两种格式,一种jar包格式,一种目录格式
         * @param outputProvider  获取到输出目录,最后将修改的文件复制到输出目录,这一步必须执行,不让编译会报错
         */
        @Override
        void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs,
                       TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
    
            System.out.println("----------开始Transform-----------")
            // Transform 的 inputs 分为两种类型,一直是目录,一种是 jar 包。需要分开遍历
    
            inputs.each { TransformInput input ->
                // 1) 对类型为"目录"的 input 进行遍历
                input.directoryInputs.each { DirectoryInput dirInput ->
                    // demo1. 在MainActivity的onCreate()方法之前注入代码
                    MyInject.injectOnCreate(dirInput.file.absolutePath, project)
                    // 获取 output 目录
                    def dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes,
                        dirInput.scopes, Format.DIRECTORY)
                    // 将 input 的目录复制到 output 指定目录
                    FileUtils.copyDirectory(dirInput.file, dest)
                }
    
                // 2) 对类型为 jar 文件的 input 进行遍历
                input.jarInputs.each { JarInput jarInput ->
                    // jar 文件一般是第三方依赖库jar包
    
                    // 重命名输出文件(同目录 copyFile 会冲突)
                    def jarName = jarInput.name
                    def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
    
                    if (jarName.endsWith(".jar")) {
                        jarName = jarName.substring(0, jarName.length() - 4)
                    }
                    // 生成输出路径
                    def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                    // 将输入内容复制到输出
                    FileUtils.copyFile(jarInput.file, dest)
                }
            }
    
            System.out.println("----------结束Transform-----------")
        }
    }
    
    

    MyInject.groovy 文件操作字节码代码如下:

    package com.coral.plugin
    
    import javassist.ClassPool
    import javassist.CtClass
    import javassist.CtConstructor
    import javassist.CtMethod
    import org.gradle.api.Project
    
    public class MyInject {
        private static ClassPool classPool = ClassPool.getDefault()
    
        public static void injectOnCreate(String path, Project project) {
            classPool.appendClassPath(path)
            classPool.appendClassPath(project.android.bootClasspath[0].toString())
            classPool.importPackage("android.os.Bundle")
    
            File dir = new File(path)
            if (dir.isDirectory()) {
                dir.eachFileRecurse { File file ->
                    String filePath = file.absolutePath
                    if (file.getName().equals("MainActivity.class")) {
                        // 获取 MainActivity
                        CtClass ctClass = classPool.getCtClass("com.coral.demo.MainActivity")
                        println("ctClass = " + ctClass)
    
                        // 解冻
                        if (ctClass.isFrozen()) {
                            ctClass.defrost()
                        }
    
                        // 获取到 onCreate() 方法
                        CtMethod ctMethod = ctClass.getDeclaredMethod("onCreate")
                        println("ctMethod = " + ctMethod)
                        // 插入日志打印代码
                        String insertBeforeStr = """android.util.Log.e("--->", "Hello");"""
    
                        ctMethod.insertBefore(insertBeforeStr)
                        ctClass.writeFile(path)
                        ctClass.detach()
                    }
                }
            }
        }
    }
    
    

    如何使自定义的 Transform 有作用?需要定义插件进行注册,MyTransformPlugin.groovy 代码如下:

    import com.android.build.gradle.AppPlugin
    import org.gradle.api.Plugin
    import org.gradle.api.Project
    import com.android.build.gradle.AppExtension
    
    public class MyTransformPlugin implements Plugin<Project> {
    
        @Override
        public void apply(Project project) {
            def android = project.extensions.getByType(AppExtension)
            // 注册Transform
            def classTransform = new MyTransform(project)
            android.registerTransform(classTransform)
        }
    }
    

    在 app.gradle 使用时也需要 apply plugin ,依赖脚本同上。

    说明:

    • 自定义的 Transform 在编译的时候并不会被触发执行,在安装 apk 时会触发执行;

    • 自定义的 Transform 会自动生成几种不同 gradle task,任务名称规则为:transformClassWith$${getName}For${variant}


      自定义Transform任务名称
    • 双击上述自定义的 transform 任务会去执行 Transform 中的 transform() 方法,进行字节码操作代码。这一步可以看到我们再 groovy 中的打印日志,很方便调试。

    • 在自定义的 MyTransform 中,使用 transform() 方法处理字节码,除了调用 MyInject 类的方法处理不同,其他的处理步骤都是统一的。

    • transform() 处理步骤大致可以分为:1)对类型为目录的 input 遍历;2)调用 javassist api 处理字节码;3)生成输出路径,将操作后的 input 目录复制到 output 指定目录;4)对类型为 jar 的 input 遍历;5)重命名输出文件(防止复制文件冲突);5)生成输出路径 & 将输入内容复制到输出。

    相关文章

      网友评论

          本文标题:Gradle Plugin: Transform + Javas

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