gradle用于构建项目,其plugin插件用于完成一特定功能,而有些时候我们希望在插件中完成对项目内容的一些更改,这就需要我们在gradle构建过程中,获取到源文件才能进行,所幸的是,gradle plugin从1.5.0版本开始,为我们提供了Transform功能,它可以以输入输出流的链式方式,供我们对源文件进行处理
自gradle plugin3.0开始,对于引入gradle plugin有一些问题,需要进行兼容,下面会有说到
一般我们在根项目中的build.gradle中的buildscript中引入gradle plugin脚本,而要引入Transform功能,需要在任意module的build.gradle中的dependencies引入
compile "com.android.tools.build:gradle:2.3.3"
这里需要注意的是,在3.0后,对于gradle plugin的仓库位置发生变化,需要在buildscript和module中的repositories中使用google()仓库:
repositories {
google()
...
}
引入后我们就可以看到Transform等类了
该接口定义了一个输入内容的基本实现,有name和file,此file可能有两种形式,文件夹和jar包,于是对应的分为两个实现类,DirectoryInput和JarInput
对于每一个输入流来说,内容既可能有一组文件夹,也可能有一组jar包,TransformInput类为一个输出流的标准实现
既然有了input,对应的就要有output,output的位置不能由我们私自决定,需要通过TransformOutputProvider的getContentLocation()获取
将以上输入输出流信息包装为TransformInvocation对象,这也是3.0之后的一个改变,之前只是将各个参数直接传递给Transform
对于输出内容,我们可以指定想要获取的输入内,ContentType中设定了几种类型:
CLASSES:字节码文件
RESOURCES:资源文件
除了ContentType,还可以指定整个Transform的作用域,SCOPE中设定了几种类型:
PROJECT:主项目
SUB_PROJECT:子项目
PROJECT_LOCAL_DEPS:主项目的本地依赖
SUB_PROJECT_LOCAL_DEPS:子项目的本地依赖
EXTERNAL_LIBRARIES:依赖库
在3.0后,两个LOCAL_DEPS已经废弃了,都归为EXTERNAL_LIBRARIES
3.0开始,gradle为我们提供了TransformManager类,里面的一些常量帮我们指定了一些常用的ContentType和Scope,比如:
SCOPE_FULL_PROJECT:包括了PROJECT、SUB_PROJECT、EXTERNAL_LIBRARIES,也是最常用的一个
CONTENT_CLASS:包括了CLASSES,也是最常用的一个
最后我们来说Transform,其需要重写的几个方法:
getName():为Transform定义一个名字,不过该名字最后生成的文件名,也是拼接上了flavor、buildType等等
getInputTypes():该方法返回的就是一组ContentType,用于限定接受的输入内容类型
getScopes():该方法返回的就是一组Scope,用于限定transform的作用域
isIncremental():该方法是指定该Transform是否使用增量构建模式
transform():该方法就是实际转换时候调用的方法,3.0之前将input、outputProvider等传入,3.0开始直接将一个TransformInvocation对象传入
有了Transform,需要把它加入到构建中:
project.extensions.getByType(AppExtension).registerTransform(new MyLogTransform())
AppExtension就是我们android的gradle plugin的Extension对象,即我们在build.gradle中的android{}配置模块,其中的registerTransform方法添加一个Transform对象
每个Transform的输出作为下一个Transform的输入,前一个Transform的output,会根据下一个Transform的contentTypes和scopes,将相应的内容传入到下一个Transform的inputs中
Transform在添加时,会将其包装为一个Task
public <T extends Transform> Optional<AndroidTask<TransformTask>> addTransform(TaskFactory taskFactory, TransformVariantScope scope, T transform, ConfigActionCallback<T> callback) {
...
this.transforms.add(transform);
//包装为Task
AndroidTask task1 = this.taskRegistry.create(taskFactory, new ConfigAction(scope.getFullVariantName(), taskName, transform, inputStreams, referencedStreams, outputStream, this.recorder, callback));
...
return Optional.ofNullable(task1);
}
}
}
//先加入JavaCompile的Task
setJavaCompilerTask(javacTask, tasks, variantScope);
this.createPostCompilationTasks(tasks, variantScope);
//再加入其他Transform的Task,且应用依赖
public void createPostCompilationTasks(TaskFactory tasks, VariantScope variantScope) {
...
List customTransforms = extension.getTransforms();
List customTransformsDependencies = extension.getTransformsDependencies();
int preColdSwapTask = 0;
for(int multiDexClassListTask = customTransforms.size(); preColdSwapTask < multiDexClassListTask; ++preColdSwapTask) {
Transform dexOptions = (Transform)customTransforms.get(preColdSwapTask);
List dexTransform = (List)customTransformsDependencies.get(preColdSwapTask);
transformManager.addTransform(tasks, variantScope, dexOptions).ifPresent((t) -> {
if(!dexTransform.isEmpty()) {
t.dependsOn(tasks, dexTransform);
}
...
//add MultiDex/Dex/... Transform
});
}
...
}
了解了Transform的结构、API的定义以及Task的构建,下面就来看看,具体如何在transform方法中进行操作项目源文件吧
class MyLogTransform extends Transform {
@Override
String getName() {
return 'LogTransform'
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS//只接受classes文件(包括jar包里的)
}
@Override
Set<QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT(作用域是整个项目内的,包括子项目和依赖)
}
@Override
boolean isIncremental() {
return false//不使用增量模式构建
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
def inputs = transformInvocation.inputs
//inputs分为两种,一种是目录文件夹,一种是jar包
inputs.each { TransformInput input ->
//目录文件夹是我们的源代码和生成的R文件和BuildConfig文件等
input.directoryInputs.each { DirectoryInput directoryInput ->
//输出地址要通过类型、名字等,由OutputProvider决定
def dest = outputProvider.getContentLocation(directoryInput.name + directoryInput.file.absolutePath.hashCode(),
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY)
//将input的目录复制到output指定目录,因为input一定要output,否则就丢失了(transform为链式的)
FileUtils.copyDirectory(directoryInput.file, dest)
}
//jar文件一般就是依赖
input.jarInputs.each { JarInput jarInput ->
def dest = outputProvider.getContentLocation(jarInput.name + jarInput.file.absolutePath.hashCode(),
jarInput.contentTypes, jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file, dest)
}
}
}
}
代码的注释比较详细了,这里需要解释和注意的是:
每个input都可能有两种文件类型输入,directory和jar,所以都要遍历
每个input都需要对应的output,否则就会丢失输出
output时的位置,要由TransformOutputProvider通过name、contentTypes、scope和Format决定;name要注意,在同一个contentTypes、scope、Format下name要唯一,因为输入的Input的name可能重复,导致覆盖同样的文件,一般可以使用文件绝对路径的hascode作为后缀来确保唯一
TransformInvocation的referencedInputs,是只用来查看的,而不是output的,其原始内容会自动原样传入到下一个Transform的input中
我们可以在transform中拿到源字节码文件,就可以对其内容做修改了,但是手动修改字节码还是比较难的,好在有一些帮助我们用java的方式就可以修改字节码的工具,比如javassist库:
compile 'org.javassist:javassist:3.20.0-GA'
具体的使用可以参考这篇文章。
每个Transform的中间产物(输出内容),都可以在项目的build中看到,如:
在项目的build文件夹中的intermediates中,有一个transforms文件夹,这是所有transform构建生成的文件存储的文件夹,里面会分为各个transform,其文件夹name其实就是我们为Transform定义的name(getName()),再往里面就是针对不同的flavor、BuildType等的文件夹,最里面就是以index命名生成的文件:3.0之前这层文件夹是按照contentTypes和scope的枚举值定义的文件夹名字,升级后使用的是文件的index,比如第一个输入流的内容,输出的文件夹名就叫0,第二个就叫1以此类推
1.Transform会被加入到所有的variant中,即会为debug、release等所有variant构件相应的task,无法根据variant进行应用;后续官方会改正这一问题
2.所有Transform的顺序无法控制,即不要依赖其执行顺序搞事情