公司项目里有编译期代码和资源生成的需求, 之前一直是使用python(甚至还有bat)脚本去处理的, 为了让动态生成的资源参与编译, 先后使用过下面的方法:

  • 生成到常规资源目录(默认src/main/java和src/main/res). 这样处理的话动态生成的资源会被版本管理系统检测到, 需要一一指定忽略, 或者提交时手动进行排除, 生成的东西多了很难管控, 经常有误提的情况发生.
  • 生成到build下的自定义目录, 通过android gradle plugin提供的sourceSets配置, 将生成的资源目录进行添加. build目录是统一加了忽略配置的, 不会出现上面版本控制的问题, 但是在IDE里的项目结构中会将生成的代码当做普通代码对待, 与手写的代码混在一起, 看起来很难受.

恰好android编译的流程中是有动态生成资源的逻辑的, 比如BuildConfig.java, 生成的资源也会被IDE正确识别为generated resources, 与手写的代码是隔离开的, 于是抽了点时间对这部分的逻辑进行了下研究.

代码生成

这部分没什么好说的, 可以通过自定义的gradle task直接进行处理, 举个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
task genJavaCode() {
    doFirst {
        String fileContent = """// auto-generated code. DO NOT MODIFY!
package me.jayl.demo.resgen;

public class GeneratedClass {
    public static String getMessage() {
       return "message from GeneratedClass!";
    }
}
"""
        File genDir = new File(
                "${project.buildDir.absolutePath}/gen/me/jayl/demo/resgen")
        genDir.mkdirs()
        FileWriter fw = new FileWriter(new File("${genDir.absolutePath}/GeneratedClass.java"))
        fw.write(fileContent)
        fw.flush()
    }
}

实际开发中生成的代码可能会比较复杂, 可以使用javapoet或者其他库, 这里只是演示所以就直接写死了.

上面的例子在build/gen/目录下生成了一个简单的java class代码, 但是直接生成的代码是不会被IDE识别的, 也不会参与编译, 需要我们将对应的路径进行注册:

代码生成task的注册

android gradle plugin已经提供了将对应的task注册为java代码生成task的接口, 具体可以参考BaseVariant的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    /**
     * Adds to the variant a task that generates Java source code.
     *
     * This will make the generate[Variant]Sources task depend on this task and add the
     * new source folders as compilation inputs.
     *
     * The new source folders are also added to the model.
     *
     * @param task the task
     * @param sourceFolders the source folders where the generated source code is.
     */
    void registerJavaGeneratingTask(@NonNull Task task, @NonNull File... sourceFolders);

    /**
     * Adds to the variant a task that generates Java source code.
     *
     * This will make the generate[Variant]Sources task depend on this task and add the
     * new source folders as compilation inputs.
     *
     * The new source folders are also added to the model.
     *
     * @param task the task
     * @param sourceFolders the source folders where the generated source code is.
     */
    void registerJavaGeneratingTask(@NonNull Task task, @NonNull Collection<File> sourceFolders);

关于Variant的信息这里就不详细说明了, 可以简单的理解成每个Variant对应了android编译时的一个完整构建, 我们在build.gradle指定的那些flavor和其他编译类型定义(debug/release/test等)的组合都对应着一个Variant实例.

有了对应的接口就好说了, 我们可以遍历编译期的每个Variant, 将代码生成对应的task和生成路径进行注册:

1
2
3
4
android.applicationVariants.all {
        it.registerJavaGeneratingTask(
          genJavaCode, new File("${project.buildDir.absolutePath}/gen"))
}

搞定之后, 重新跑app的编译, 看下task执行流程, 注册的代码生成task已经被添加到task图里执行了:

gradle task execute log

Android Studio的项目结构中也可以看到刚刚生成的代码文件已经被正确识别为generated java code, 与BuildConfig一样:

generated code

资源生成的一些不同点

其实再仔细看BaseVariant.java的代码, 会发现plugin中同样提供了资源生成task注册的接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
    /**
     * Adds to the variant a task that generates Resources.
     *
     * This will make the generate[Variant]Resources task depend on this task and add the
     * new Resource folders as Resource merge inputs.
     *
     * The Resource folders are also added to the model.
     *
     * @param task the task
     * @param resFolders the folders where the generated resources are.
     *
     * @deprecated Use {@link #registerGeneratedResFolders(FileCollection)}
     */
    @Deprecated
    void registerResGeneratingTask(@NonNull Task task, @NonNull File... resFolders);

    /**
     * Adds to the variant a task that generates Resources.
     *
     * This will make the generate[Variant]Resources task depend on this task and add the
     * new Resource folders as Resource merge inputs.
     *
     * The Resource folders are also added to the model.
     *
     * @param task the task
     * @param resFolders the folders where the generated resources are.
     *
     * @deprecated Use {@link #registerGeneratedResFolders(FileCollection)}
     */
    @Deprecated
    void registerResGeneratingTask(@NonNull Task task, @NonNull Collection<File> resFolders);

使用方式与java代码生成的task注册是一样的, 但是这两个方法已经被标记为Deprecated了, 建议我们使用新的接口:

1
2
3
4
5
6
7
8
9
    /**
     * Adds to the variant new generated resource folders.
     *
     * <p>In order to properly wire up tasks, the FileCollection object must include dependency
     * information about the task that generates the content of this folders.
     *
     * @param folders a FileCollection that contains the folders and the task dependency information
     */
    void registerGeneratedResFolders(@NonNull FileCollection folders);

新接口中不再接收task作为参数, 只接收了一个FileCollection类型的参数, 同时方法注释里的这句”the FileCollection object must include dependency information about the task that generates the content of this folders”也是让人一头雾水, 研究了好久终于找到了所谓的”dependency information about the task”的指定方法:

1
ConfigurationFileCollecgtion#builtBy(Object...)

通过ConfigurableFileCollection的builtBy属性, 可以指定这个路径集合对应的task信息, 剩下的就是正常注册task就好了:

1
2
3
4
5
def genPath = "${project.buildDir.absolutePath}/gen_res"
// task初始化时对自己进行注册, 比如生成资源的task为resGenTask
android.applicationVariants.all {
    it.registerGeneratedResFolders(project.files(new File(genPath)).builtBy(resGenTask))
}

重新执行编译, 就可以看到资源生成的task和对应路径也被正确处理了.

最后一步, 插件编写

上面一直是通过修改module的build.gradle来进行task的定义和注册的, 规范的做法还是要通过定义gradle plugin的方式对我们的资源生成逻辑进行封装, 以便维护和在多个项目中进行复用.

关于gradle plugin的开发, 有很多教程可以参考, 这里就不做详细叙述了. 最后的gradle plugin和上面的逻辑整体是差异不大的, 记录一下关键逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ResgenPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.plugins.all {
            // 我们的插件允许被app和library使用, 所以针对application和library类型的module
            // 需要进行一些区分处理
            if (it instanceof AppPlugin) {
                registerGenerateResTask(project,
                        project.extensions.getByType(AppExtension).applicationVariants)
            } else if (it instanceof LibraryPlugin) {
                registerGenerateResTask(project,
                        project.extensions.getByType(LibraryExtension).libraryVariants)
            }
        }
    }

    void registerGenerateResTask(Project project, DomainObjectSet<BaseVariant> variants) {
        variants.all {BaseVariant variant ->
            def resGeneratePath = FileUtil.mergePaths(project.buildDir.absolutePath,
                    "generated/res/resolution", variant.getDirName())

            Task task = project.task("generateResolutionResources${variant.name.capitalize()}") {
                doFirst {
                    // 对应的资源生成逻辑, 此处省略
                    ...
                }
            }

            variant.registerGeneratedResFolders(project.files(new File(resGeneratePath))
                    .builtBy(task))
        }
    }
}