公司项目里有编译期代码和资源生成的需求, 之前一直是使用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图里执行了:
Android Studio的项目结构中也可以看到刚刚生成的代码文件已经被正确识别为generated java code, 与BuildConfig一样:
资源生成的一些不同点
其实再仔细看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))
}
}
}
|