Gradle Transform是Android官方提供给开发者在项目构建阶段即由class到dex转换期间修改class文件的一套api。目前比较经典的应用是字节码插桩、代码注入技术。
TranFrom原理
每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar. aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。
下面我们先看下一按TransForm下面对应的方法
public abstract class Transform {
public Transform() {
}
// 获取TransForm的名称
public abstract String getName();
/**
* 需要处理的数据类型,有两种枚举类型
*
* CLASSES
* 代表处理的 java 的 class 文件,返回TransformManager.CONTENT_CLASS
*
* RESOURCES
* 代表要处理 java 的资源,返回TransformManager.CONTENT_RESOURCES
*
* @return
*/
public abstract Set<ContentType> getInputTypes();
public Set<ContentType> getOutputTypes() {
return this.getInputTypes();
}
/***
* 指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
*
* EXTERNAL_LIBRARIES : 只有外部库
* PROJECT : 只有项目内容
* PROJECT_LOCAL_DEPS : 只有项目的本地依赖(本地jar)
* PROVIDED_ONLY : 只提供本地或远程依赖项
* SUB_PROJECTS : 只有子项目
* SUB_PROJECTS_LOCAL_DEPS: 只有子项目的本地依赖项(本地jar)
* TESTED_CODE :由当前变量(包括依赖项)测试的代码
* 如果要处理所有的class字节码,返回TransformManager.SCOPE_FULL_PROJECT
*
* @return
*/
public abstract Set<? super Scope> getScopes();
/***
* 增量编译开关
*
* 当我们开启增量编译的时候,相当input包含了changed/removed/added三种状态,实际上还有notchanged。需要做的操作如下:
*
* NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
* ADDED、CHANGED: 正常处理,输出给下一个任务;
* REMOVED: 移除outputProvider获取路径对应的文件。
*
* @return
*/
public abstract boolean isIncremental();
/** @deprecated */
@Deprecated
public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
}
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
this.transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental());
}
/***
* 如果我们的transform需要被缓存,则为true,它被TransformTask所用到
* @return
*/
public boolean isCacheable() {
return false;
}
...
}
自定义TransForm 模板 让我们更好的认识一下TransForm
public class CustomTransform extends Transform {
public static final String TAG = "CustomTransform";
public CustomTransform() {
super();
}
@Override
public String getName() {
return "CustomTransform";
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
//当前是否是增量编译
boolean isIncremental = transformInvocation.isIncremental();
//消费型输入,可以从中获取jar包和class文件夹路径。需要输出给下一个任务
Collection<TransformInput> inputs = transformInvocation.getInputs();
//引用型输入,无需输出。
Collection<TransformInput> referencedInputs = transformInvocation.getReferencedInputs();
//OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
for(TransformInput input : inputs) {
for(JarInput jarInput : input.getJarInputs()) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyFile(jarInput.getFile(), dest);
}
for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
//将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyDirectory(directoryInput.getFile(), dest);
}
}
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public Set<QualifiedContent.ContentType> getOutputTypes() {
return super.getOutputTypes();
}
@Override
public Set<? super QualifiedContent.Scope> getReferencedScopes() {
return TransformManager.EMPTY_SCOPES;
}
@Override
public Map<String, Object> getParameterInputs() {
return super.getParameterInputs();
}
@Override
public boolean isCacheable() {
return true;
}
@Override
public boolean isIncremental() {
return true; //是否开启增量编译
}
}
可以看到,在transform方法中,我们将每个jar包和class文件复制到dest路径,这个dest路径就是下一个Transform的输入数据,而在复制时,我们就可以做一些狸猫换太子,偷天换日的事情了,先将jar包和class文件的字节码做一些修改,再进行复制即可,至于怎么修改字节码,就要借助我们后面介绍的ASM了
Transform的优化:增量与并发
到此为止,看起来Transform用起来也不难,但是,如果直接这样使用,会大大拖慢编译时间,为了解决这个问题,摸索了一段时间后,也借鉴了Android编译器中Desugar等几个Transform的实现,发现我们可以使用增量编译,并且上面transform方法遍历处理每个jar/class的流程,其实可以并发处理,加上一般编译流程都是在PC上,所以我们可以尽量敲诈机器的资源。
想要开启增量编译,我们需要重写Transform的这个接口,返回true。
@Override
public boolean isIncremental() {
return true;
}
虽然开启了增量编译,但也并非每次编译过程都是支持增量的,毕竟一次clean build完全没有增量的基础,所以,我们需要检查当前编译是否是增量编译。
如果不是增量编译,则清空output目录,然后按照前面的方式,逐个class/jar处理
如果是增量编译,则要检查每个文件的Status,Status分四种,并且对这四种文件的操作也不尽相同
NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
ADDED、CHANGED: 正常处理,输出给下一个任务;
REMOVED: 移除outputProvider获取路径对应的文件。
添加增量后的Transform
@Override
public void transform(TransformInvocation transformInvocation){
Collection<TransformInput> inputs = transformInvocation.getInputs();
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
boolean isIncremental = transformInvocation.isIncremental();
//如果非增量,则清空旧的输出内容
if(!isIncremental) {
outputProvider.deleteAll();
}
for(TransformInput input : inputs) {
for(JarInput jarInput : input.getJarInputs()) {
Status status = jarInput.getStatus();
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
if(isIncremental && !emptyRun) {
switch(status) {
case NOTCHANGED:
break;
case ADDED:
case CHANGED:
transformJar(jarInput.getFile(), dest, status);
break;
case REMOVED:
if (dest.exists()) {
FileUtils.forceDelete(dest);
}
break;
}
} else {
transformJar(jarInput.getFile(), dest, status);
}
}
for(DirectoryInput directoryInput : input.getDirectoryInputs()) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY);
FileUtils.forceMkdir(dest);
if(isIncremental && !emptyRun) {
String srcDirPath = directoryInput.getFile().getAbsolutePath();
String destDirPath = dest.getAbsolutePath();
Map<File, Status> fileStatusMap = directoryInput.getChangedFiles();
for (Map.Entry<File, Status> changedFile : fileStatusMap.entrySet()) {
Status status = changedFile.getValue();
File inputFile = changedFile.getKey();
String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath);
File destFile = new File(destFilePath);
switch (status) {
case NOTCHANGED:
break;
case REMOVED:
if(destFile.exists()) {
FileUtils.forceDelete(destFile);
}
break;
case ADDED:
case CHANGED:
FileUtils.touch(destFile);
transformSingleFile(inputFile, destFile, srcDirPath);
break;
}
}
} else {
transformDir(directoryInput.getFile(), dest);
}
}
}
}
实现了增量编译后,我们最好也支持并发编译,并发编译的实现并不复杂,只需要将上面处理单个jar/class的逻辑,并发处理,最后阻塞等待所有任务结束即可。