通过上图可知,我们只要在图中红色箭头处拦截(生成class文件之后,dex文件之前),就可以拿到当前应用程序中所有的.class文件,再去借助ASM之类的库,就可以遍历这些.class文件中所有方法,再根据一定的条件找到需要的目标方法,最后进行修改并保存,就可以插入我们的埋点代码。
Google从 Android Gradle 1.5.0 开始,提供了Transform API。通过Transform API,允许第三方以插件的形式,在Android应用程序打包成dex文件之前的编译过程中操作.class文件。我们只要实现一套Transform,去遍历所有.class文件的所有方法,然后进行修改(在特定的listener回调中插入埋点代码),再对源文件进行替换,即可以达到插入代码的目的。
Gradle Transform是Android官方提供给开发者在项目构建阶段(.class -> .dex转换期间)用来修改.class文件的一套标准API,即把输入的.class文件转变成目标字节码文件。目前比较经典的应用是字节码插桩、代码注入等。
我们build一个项目,会打印出如下日志,红框框住的部分就是一个Transform的名称
通过上张图可以看到原生就带了一系列Transform供使用,那么这些Transform是怎么组织在一起的呢,我们用一张图表示:
每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar、aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。 这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。
但其实,上面这幅图,只是展示Transform的其中一种情况。而Transform其实可以有两种输入,一种是消费型的,当前Transform需要将消费型型输出给下一个Transform,另一种是引用型的,当前Transform可以读取这些输入,而不需要输出给下一个Transform,比如Instant Run就是通过这种方式,检查两次编译之间的diff的。
最终,我们定义的Transform会被转化成一个个TransformTask,在Gradle编译时调用。
Transform两个基础概念
TransformInput是指输入文件的一个抽象,包括:
DitectoryInput集合
是指以源码的方式参与项目编译的所有目录结构及其目录下的源码文件
JarInput集合
是指以jar包方式参与项目编译的所有本地jar包和远程jar包(此处的jar包包括aar)
之Transform的输出,通过它可以获取到输出路径等信息
先来了解下Transform类,定义如下
public abstract class Transform {
public Transform() {
}
// Transform名称
public abstract String getName();
public abstract Set getInputTypes();
public Set getOutputTypes() {
return this.getInputTypes();
}
public abstract Set super Scope> getScopes();
public abstract boolean isIncremental();
/** @deprecated */
@Deprecated
public void transform(Context context, Collection inputs, Collection 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());
}
public boolean isCacheable() {
return false;
}
...
}
Transform名称,上面build日志红框框住的部分就是Transform名称
transformClassesWithDexBuilderForDebug
那么最终的名字是如何构成的呢?
在gradle plugin的源码中有一个叫TransformManager的类,这个类管理着所有的Transform的子类,里面有一个方法叫getTaskNamePrefix,在这个方法中就是获得Task的前缀,以transform开头,之后拼接ContentType,这个ContentType代表着这个Transform的输入文件的类型,类型主要有两种,一种是Classes,另一种是Resources,ContentType之间使用And连接,拼接完成后加上With,之后紧跟的就是这个Transform的Name,name在getName()方法中重写返回即可。TransformManager#getTaskNamePrefix()代码如下:
static String getTaskNamePrefix(Transform transform) {
StringBuilder sb = new StringBuilder(100);
sb.append("transform");
sb.append((String)transform.getInputTypes().stream().map((inputType) -> {
return CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, inputType.name());
}).sorted().collect(Collectors.joining("And")));
sb.append("With");
StringHelper.appendCapitalized(sb, transform.getName());
sb.append("For");
return sb.toString();
}
需要处理的数据类型,有两种枚举类型
CLASSES
代表处理的 java 的 class 文件,返回TransformManager.CONTENT_CLASS
RESOURCES
代表要处理 java 的资源,返回TransformManager.CONTENT_RESOURCES
指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:
增量编译开关
当我们开启增量编译的时候,相当input包含了changed/removed/added三种状态,实际上还有notchanged。需要做的操作如下:
public void transform(@NonNull TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// Just delegate to old method, for code that uses the old API.
//noinspection deprecation
this.transform(transformInvocation.getContext(), transformInvocation.getInputs(),
transformInvocation.getReferencedInputs(),
transformInvocation.getOutputProvider(),
transformInvocation.isIncremental());
}
注意点
如果我们的transform需要被缓存,则为true,它被TransformTask所用到
AspectJTransform.groovy代码如下:
class AspectJTransform extends Transform {
final String NAME = "JokerwanTransform"
@Override
String getName() {
return NAME
}
@Override
Set getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set super 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)
// OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
transformInvocation.inputs.each { TransformInput input ->
input.jarInputs.each { JarInput jarInput ->
// 处理Jar
processJarInput(jarInput, outputProvider)
}
input.directoryInputs.each { DirectoryInput directoryInput ->
// 处理源码文件
processDirectoryInputs(directoryInput, outputProvider)
}
}
}
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
// to do some transform
// 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyFiley(jarInput.getFile(), dest)
}
void processDirectoryInputs(DirectoryInput directoryInput, TransformOutputProvider outputProvider) {
File dest = outputProvider.getContentLocation(directoryInput.getName(),
directoryInput.getContentTypes(), directoryInput.getScopes(),
Format.DIRECTORY)
// 建立文件夹
FileUtils.forceMkdir(dest)
// to do some transform
// 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyDirectory(directoryInput.getFile(), dest)
}
}
AspectJTransform.groovy代码如下:
class AspectJTransform extends Transform {
final String NAME = "JokerWanTransform"
@Override
String getName() {
return NAME
}
@Override
Set getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return true
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
boolean isIncremental = transformInvocation.isIncremental()
// OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == null
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
if (!isIncremental) {
// 不需要增量编译,先清除全部
outputProvider.deleteAll()
}
transformInvocation.getInputs().each { TransformInput input ->
input.jarInputs.each { JarInput jarInput ->
// 处理Jar
processJarInputWithIncremental(jarInput, outputProvider, isIncremental)
}
input.directoryInputs.each { DirectoryInput directoryInput ->
// 处理文件
processDirectoryInputWithIncremental(directoryInput, outputProvider, isIncremental)
}
}
}
void processJarInputWithIncremental(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
if (isIncremental) {
// 处理增量编译
processJarInputWhenIncremental(jarInput, dest)
} else {
// 不处理增量编译
processJarInput(jarInput, dest)
}
}
void processJarInput(JarInput jarInput, File dest) {
transformJarInput(jarInput, dest)
}
void processJarInputWhenIncremental(JarInput jarInput, File dest) {
switch (jarInput.status) {
case Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
// 处理有变化的
transformJarInputWhenIncremental(jarInput.getFile(), dest, jarInput.status)
break
case Status.REMOVED:
// 移除Removed
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
break
}
}
void transformJarInputWhenIncremental(JarInput jarInput, File dest, Status status) {
if (status == Status.CHANGED) {
// Changed的状态需要先删除之前的
if (dest.exists()) {
FileUtils.forceDelete(dest)
}
}
// 真正transform的地方
transformJarInput(jarInput, dest)
}
void transformJarInput(JarInput jarInput, File dest) {
// to do some transform
// 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyFile(jarInput.getFile(), dest)
}
void processDirectoryInputWithIncremental(DirectoryInput directoryInput, TransformOutputProvider outputProvider, boolean isIncremental) {
File dest = outputProvider.getContentLocation(
directoryInput.getFile().getAbsolutePath(),
directoryInput.getContentTypes(),
directoryInput.getScopes(),
Format.DIRECTORY)
if (isIncremental) {
// 处理增量编译
processDirectoryInputWhenIncremental(directoryInput, dest)
} else {
processDirectoryInput(directoryInput, dest)
}
}
void processDirectoryInputWhenIncremental(DirectoryInput directoryInput, File dest) {
FileUtils.forceMkdir(dest)
String srcDirPath = directoryInput.getFile().getAbsolutePath()
String destDirPath = dest.getAbsolutePath()
Map fileStatusMap = directoryInput.getChangedFiles()
fileStatusMap.each { Map.Entry entry ->
File inputFile = entry.getKey()
Status status = entry.getValue()
String destFilePath = inputFile.getAbsolutePath().replace(srcDirPath, destDirPath)
File destFile = new File(destFilePath)
switch (status) {
case Status.NOTCHANGED:
break
case Status.REMOVED:
if (destFile.exists()) {
FileUtils.forceDelete(destFile)
}
break
case Status.ADDED:
case Status.CHANGED:
FileUtils.touch(destFile)
transformSingleFile(inputFile, destFile, srcDirPath)
break
}
}
}
void processDirectoryInput(DirectoryInput directoryInput, File dest) {
transformDirectoryInput(directoryInput, dest)
}
void transformDirectoryInput(DirectoryInput directoryInput, File dest) {
// to do some transform
// 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
FileUtils.copyDirectory(directoryInput.getFile(), dest)
}
void transformSingleFile(File inputFile, File destFile, String srcDirPath) {
FileUtils.copyFile(inputFile, destFile)
}
}
参考文章
https://www.jianshu.com/p/37a5e058830a