自定义Gradle Plugin踩坑记

Gradle 版本3.5.3

插件编译Gradle环境配置

Android Studio 3.6.3
com.android.tools.build:gradle:3.5.3
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

首先我们写一个打印项目class的插件代码如下:

package com.asm.fix

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import com.android.utils.FileUtils

public class ClassTransform extends Transform {

    private String TAG = "ClassTransform"

    /**
     * 设置我们自定义的 Transform 对应的 Task 名称。
     * Gradle 在编译的时候,会将这个名称显示在控制台上。
     * 比如:Task :app:transformClassesWithXXXForDebug。
     * @return
     */
    @Override
    String getName() {
        return "ClassTransform"
    }

    /**
     * 在项目中会有各种各样格式的文件,通过 getInputType 可以设置
     * ClassTransform 接收的文件类型,
     * 此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 这个方法规定自定义 Transform 检索的范围
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.PROJECT_ONLY
    }

    //表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * 自定义Transform 中最重要的方法就是 transform()。在这个方法中,可以获取到两个数据的流向。
     * @param transformInvocation
     * @throws TransformException* @throws InterruptedException* @throws IOException
     */
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        System.out.println(TAG + " start")

        Collection<TransformInput> inputCollection = transformInvocation.inputs
        if (inputCollection == null) {
            throw new IllegalArgumentException("TransformInput is null !!!")
        }
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider == null) {
            throw new IllegalArgumentException("TransformOutputProvider is null !!!")
        }
      
        inputCollection.each { TransformInput transformInput ->
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        System.out.println("find class: " + file.name)
                    }
                }
            }
        }
    }
}

构建好插件并上传到maven仓库,然后再主项目的build.gradle文件中引用插件

先 clean一下项目,然后执行 ./gradlew build命令或者 Gradle窗口->app->build->build任务。这时可以看到Run窗口打印的构建过程和构建日志如下:

gradle渠道修改java_gradle渠道修改java

可以看到我们自定义的插件生效了并打印出对应的class文件名称。但是当我们在真机器上打包run时会发现APP启动崩溃报错:

gradle渠道修改java_android_02

这是因为我们上面写的插件只有输入没有输出,transform方法是重点,它接收上一个Transform的输出,并把处理后的结果作为下一个Transform的输入,如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lLja1TCs-1594365109011)(http://note.youdao.com/yws/res/1998/WEBRESOURCEeaa9379d7c192ba25b4ccd1b2c5ede25)]

修改上面transform方法:

@Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        System.out.println(TAG + " start")

        Collection<TransformInput> inputCollection = transformInvocation.inputs
        if (inputCollection == null) {
            throw new IllegalArgumentException("TransformInput is null !!!")
        }
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider == null) {
            throw new IllegalArgumentException("TransformOutputProvider is null !!!")
        }
      
        inputCollection.each { TransformInput transformInput ->
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        System.out.println("find class: " + file.name)
                    }
                }
                //将文件输出到指定目录
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }

这时我们重新生成插件,并运行项目就可以正真机上正常运行了。



Gradle 版本3.6.3

插件编译Gradle环境配置

Android Studio 3.6.3
com.android.tools.build:gradle:3.6.3
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip

经过我们一番折腾上面的插件我们终于可以只用了,这时你兴致勃勃的把你的插件分享给你同事,结果在你同事在他项目上一运行发现直接报错,细心的你发现你同事项目用的Gradle版本是3.6.3的。不禁感慨到“一入Gradle深似海啊”

感慨过后问题还是要解决的,我们将项目的gradle版本改成3.6.3后编译运行。老步骤编译插件->clean->build这时我们发现Build窗口打印的日志有变化:

gradle渠道修改java_android_03

跟在Gradle 3.5.3上编译运行时日志少了资源文件的class,比如R.class。这时直接直接再真机上run,会发现项目安装失败,报错如下:

gradle渠道修改java_ide_04

查询资料可知3.6.x版本后google简化了R类的生成

简化了 R 类的生成过程
Android Gradle 插件通过仅为项目中的每个库模块生成一个 R 类并与其他模块依赖项共享这些 R 类,简化了编译类路径。这项优化应该会加快构建速度,但您需要注意以下事项:

  • 由于编译器与上游模块依赖项共享 R 类,因此项目中的每个模块都必须使用独一无二的软件包名称。
  • 库的 R 类对其他项目依赖项的可见性取决于用于将库添加为依赖项的配置例如,如果库 A 将库 B 添加为“api”依赖项,则库 A 和其他依赖于库 A 的库都可以访问库 B 的 R 类。不过,如果库 A 使用 implementation 依赖项配置,则其他库可能无权访问库 B 的 R 类。如需了解详情,请参阅依赖项配置。
    所以我们要修改插件的输入输出代码:
package com.asm.fix

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import com.android.utils.FileUtils

public class ClassTransform extends Transform {

    private String TAG = "ClassTransform"

    /**
     * 设置我们自定义的 Transform 对应的 Task 名称。
     * Gradle 在编译的时候,会将这个名称显示在控制台上。
     * 比如:Task :app:transformClassesWithXXXForDebug。
     * @return
     */
    @Override
    String getName() {
        return "ClassTransform"
    }

    /**
     * 在项目中会有各种各样格式的文件,通过 getInputType 可以设置
     * ClassTransform 接收的文件类型,
     * 此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_JARS
    }

    /**
     * 这个方法规定自定义 Transform 检索的范围
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    //表示当前 Transform 是否支持增量编译,我们不需要增量编译,所以直接返回 false 即可。
    @Override
    boolean isIncremental() {
        return false
    }

    /**
     * 自定义Transform 中最重要的方法就是 transform()。在这个方法中,可以获取到两个数据的流向。
     * @param transformInvocation
     * @throws TransformException* @throws InterruptedException* @throws IOException
     */
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        System.out.println(TAG + " start")

        Collection<TransformInput> inputCollection = transformInvocation.inputs
        if (inputCollection == null) {
            throw new IllegalArgumentException("TransformInput is null !!!")
        }
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider == null) {
            throw new IllegalArgumentException("TransformOutputProvider is null !!!")
        }

        inputCollection.each { TransformInput transformInput ->

            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
               
                File dir = directoryInput.file
                
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        System.out.println("find class: " + file.name)
                    }
                }

                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            transformInput.jarInputs.each { JarInput jarInput ->
                File jar = jarInput.file
                def dest = outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

修改后我们重新编译运行,发现可以正常在真机上运行。

其实在写gradle插件的时候应该要先搞明白Transform的输入输出方式和类型。要验证自己的插件再不同的gradle版本上的兼容性

/**
     * 在项目中会有各种各样格式的文件,通过 getInputType 可以设置
     * ClassTransform 接收的文件类型,
     * 此方法返回的类型是 Set<QualifiedContent.ContentType> 集合。
     * @return
     */
    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_JARS
    }

    /**
     * 这个方法规定自定义 Transform 检索的范围
     * @return
     */
    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }