共计 8669 个字符,预计需要花费 22 分钟才能阅读完成。
问题
之前始终在听他们说函数插桩,字节码插桩,ASM,总感觉很牛逼很高大上,晓得一个大略意思,就是 Java 文件编译成字节码,批改字节码,达到批改函数的目标,那么明天就尝试一个 Demo 级别的工程,实现 APP 打包,插入本人的代码, 并且通过 Plugin 插件的形式实现。
- 什么是 Transform,怎么自定义 Transform?
- Transform 的作用周期是在哪里呢?在打包的哪一个阶段呢?
- ASM 工具是干嘛用的呢?
实在场景
当初 APP 的奔溃是一个很失常不过的问题,为了放大影响范畴,因为每一秒对互联网来说都是钱啊,也就有了很多的热修复框架,很多都用到了 ASM 技术,在打包的时候批改 class 文件,注入本人的逻辑达到本人的目标。
举个栗子:
失常逻辑的代码,是间接返回字符串的长度,然而没有判空,可能就会有空指针异样,为了平安,一些框架,会在 APP 打包,class2dex 的时候,去烦扰 class 文件,批改字节码,在每个函数的函数体减少 if-else 的操作,没有异样的时候走失常的逻辑,crash 的值就是 false,如果产生了奔溃,就会走到修复的逻辑,从而防止奔溃。然而这个注入是怎么操作的呢?这个就设计到 ASM 字节码插桩了。
// 这个失常逻辑的代码
public int getStringLength(String name){return name.length;}
// 批改字节码之后的代码
public int getStringLength(String name){if(crash){
// 奔溃之后的解决逻辑
...
}else{return name.length;}
}
什么是 Transform
Gradle 从 1.5 开始,内置了 Transform 的 API,咱们能够通过插件,在 class2dex 的时候,对字节码文件进行操作,实现字节码插桩或者代码注入。每一个 Transform 都是一个工作,都是一个 Task,他们是链式构造的,咱们只须要实现 Transform 的接口,并且实现注册,这些 Transform 就会通过 TaskManager 串联,每一个的输入都会是下一个输出,顺次执行。
Transform 外围办法
getName()
指定以后 transform 的名称
isIncremental()
以后 transform 是否反对增量编译,增量编译能够放慢编译速度。
getInputTypes
指定以后 transform 要解决的数据类型,能够不只一种类型。比方本地 class 文件,资源文件等。
getScopes()
指定以后 transform 的作用域,比方只解决以后我的项目,只解决 jar 包等,很好了解。
TransformInvocation 外围办法
getInputs()
返回输出文件,一般来说咱们关怀的是 DirectoryInput 和 JarInput,前者指的是咱们源码形式参加编译的代码,后者就是 Jar 包形式参加编译的代码了。
getOutputProvider()
获取输入,能够取得输入的门路。
Transform 应用
注册 Transform
Transform 逻辑
指标,在源码的每个办法中,插入 System.out.println() 输入代码。
模板代码
public class LogTransform extends Transform {
@Override
public String getName() {
// 名称
return getClass().getSimpleName();
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
// 须要解决的数据类型
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
// 作用范畴
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
// 是否反对增量编译
return true;
}
@Override
public void transform(TransformInvocation transformInvocation) throws IOException {boolean incremental = transformInvocation.isIncremental();
// 获取输入,如果没有上一级的输出,输入可能也就是空的
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
// 如果不反对增量编译,须要把之前生成的都删除掉,不缓存复用
if (!incremental) {outputProvider.deleteAll();
}
// 当然工作也能够放在并发的线程池进行,期待工作完结
for (TransformInput input : transformInvocation.getInputs()) {
// 解决 Jar
Collection<JarInput> jarInputs = input.getJarInputs();
if (jarInputs != null && jarInputs.size() > 0) {for (JarInput jarInput : jarInputs) {processJarFile(jarInput, outputProvider, incremental);
}
}
// 解决 source
Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
if (directoryInputs != null && directoryInputs.size() > 0) {for (DirectoryInput directoryInput : directoryInputs) {processDirFile(directoryInput, outputProvider, incremental);
}
}
}
}
...
外围逻辑办法梳理
这份办法是 Transform 的必须要实现的办法,咱们能够从 TransformInvocation 获取输出,以及输入的门路,去做一些咱们本人的逻辑操作,执行转换。这里的逻辑很简略,就是获取输出,而后获取 DirectoryInput 源码,JarInput 的 jar 包,进行转换,比方批改字节码。
@Override
public void transform(TransformInvocation transformInvocation) throws IOException {boolean incremental = transformInvocation.isIncremental();
// 获取输入,如果没有上一级的输出,输入可能也就是空的
// 之前说过,Transform 是链式的,上一个 Transform 的输入就是以后的输出
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
// 如果不反对增量编译,须要把之前生成的都删除掉,不缓存复用
if (!incremental) {outputProvider.deleteAll();
}
// for 循环遍历所有的输出
for (TransformInput input : transformInvocation.getInputs()) {
// 解决 Jar
Collection<JarInput> jarInputs = input.getJarInputs();
if (jarInputs != null && jarInputs.size() > 0) {for (JarInput jarInput : jarInputs) {processJarFile(jarInput, outputProvider, incremental);
}
}
// 解决 source
Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
if (directoryInputs != null && directoryInputs.size() > 0) {for (DirectoryInput directoryInput : directoryInputs) {processDirFile(directoryInput, outputProvider, incremental);
}
}
}
}
这个形式是用来解决 Jar 文件。首先获取输入文件,输入文件就是转换好之后的文件了,获取信息。而后判断是否是反对增量编译。如果不反对,会把所有的输入删除,间接把以后文件解决,把 inputFile 复制到指定的输入门路,下一步解决。如果反对,复用之前的输入,那么就须要判断以后文件的状态了。如果是 NOTCHANGED,以后 InputFile 不必任何操作。如果是 ADDED、CHANGED,新增或者批改,须要拷贝输出文件到输入。如果是 REMOVED,就把之前复用的输入文件移除。很好了解。
/**
* 解决 Jar
*/
private void processJarFile(JarInput input, TransformOutputProvider outputProvider,
boolean incremental) {
// 获取到输入文件
File dest = outputProvider.getContentLocation(input.getFile().getAbsolutePath(),
input.getContentTypes(), input.getScopes(), Format.JAR);
File inputFile = input.getFile();
if (!incremental) {transformJarFile(inputFile, dest);
} else {switch (input.getStatus()) {
case NOTCHANGED:
break;
case ADDED:
case CHANGED:
transformJarFile(inputFile, dest);
break;
case REMOVED:
deleteIfExists(dest);
break;
}
}
}
private void transformJarFile(File inputFile, File outputFile) {
try {FileUtils.touch(outputFile);
FileUtils.copyFile(inputFile, outputFile);
} catch (Exception e) {e.printStackTrace();
}
}
private void deleteIfExists(File file) {
try {if (file.exists()) {FileUtils.forceDelete(file);
}
} catch (IOException e) {e.printStackTrace();
}
}
这个办法是用来解决源文件的,同理,首先获取输入目录,如果不反对增量编译,移除之前的复用的输入,并尝试从新创立文件夹,如果是文件夹的话。而后判断是不是反对增量编译,如果不反对,间接把以后所有文件复制到输入,如果反对,同样须要判断每一个文件的状态。首先获取扭转的文件汇合,遍历这个汇合,判断器状态 NOTCHANGED、ADDED、CHANGED、REMOVED,和 Jar 的解决雷同。最初在 transformDirFile 办法中,尝试批改字节码。
/**
* 解决 source-file
*/
private void processDirFile(DirectoryInput input, TransformOutputProvider outputProvider,
boolean incremental) {
// 解决源文件
File dest = outputProvider.getContentLocation(input.getFile().getAbsolutePath(),
input.getContentTypes(), input.getScopes(), Format.DIRECTORY);
// 创立文件夹
try {FileUtils.forceMkdir(dest);
} catch (IOException e) {e.printStackTrace();
}
if (incremental) {
// 输出的门路
String inputDirPath = input.getFile().getAbsolutePath();
// 输入的门路
String destDirPath = dest.getAbsolutePath();
// 获取更改的
Map<File, Status> changedFileMap = input.getChangedFiles();
// 持续遍历
for (Map.Entry<File, Status> entry : changedFileMap.entrySet()) {File inputFile = entry.getKey();
String destFilePath = inputFile.getAbsolutePath().replace(inputDirPath, destDirPath);
File outputFile = new File(destFilePath);
switch (entry.getValue()) {
case NOTCHANGED:
break;
case ADDED:
case CHANGED:
transformDirFile(inputFile, outputFile);
break;
case REMOVED:
deleteIfExists(outputFile);
break;
}
}
} else {copyDir(input.getFile(), dest);
}
}
private void copyDir(File input, File dest) {deleteIfExists(dest);
String srcDirPath = input.getAbsolutePath();
String destDirPath = dest.getAbsolutePath();
File[] inputFiles = input.listFiles();
if (inputFiles != null && inputFiles.length > 0) {for (File file : inputFiles) {String destFilePath = file.getAbsolutePath().replace(srcDirPath, destDirPath);
File destFile = new File(destFilePath);
if (file.isDirectory()) {copyDir(file, destFile);
} else if (file.isFile()) {transformDirFile(file, destFile);
}
}
}
}
private void transformDirFile(File inputFile, File outputFile) {
try {FileUtils.touch(outputFile);
// 这里要留神下,只批改 class 文件,只有 class 文件才有有字节码插桩
// 不然就会报错,很好了解
if (inputFile.getName().endsWith(".class")) {LogASM.insertCode(inputFile, outputFile);
} else {FileUtils.copyFile(inputFile, outputFile);
}
} catch (Exception e) {e.printStackTrace();
}
}
日志 Transform 小结
至此,Transform 逻辑梳理结束,核心思想就是,首先是不是反对增量编译,如果不反对,就删除复用的所有的输入,判断以后遍历所有的源文件,间接拷贝。如果反对,复用之前的输入,而后遍历文件,找出产生状态更改的文件,选择性的更新。而后在批改单个文件的时候,执行字节码插桩批改,这样就实现了指标逻辑。
ASM 简略应用
ASM 是一个字节码操作和剖析的框架,能够间接用二进制批改现有类或者动静生成一个类,简略来说就是帮忙你写 class 字节码。大略就是这个意思。
引入 ASM 依赖
// ASM 相干
implementation 'org.ow2.asm:asm:9.2'
implementation 'org.ow2.asm:asm-util:9.1'
implementation 'org.ow2.asm:asm-commons:9.2'
implementation 'androidx.room:room-compiler:2.3.0'
罕用对象介绍
ClassVisitor
Java 类拜访用的,都是通过 ClassRender 调用的,比方拜访 method,拜访 filed,annotation 等。
MethodVisitor
Java 中拜访读取办法用的
FieldVisitor
Java 中拜访读取字段属性用的
AnnotationVisitor
Java 中拜访读取注解用的,如果在一个类中用了某个注解,你须要做某种操作,能够匹配注解。
ASM 简略应用
public class LogASM {public static void insertCode(File inputFile, File outputFile) {
try {FileInputStream fileInputStream = new FileInputStream(inputFile);
FileOutputStream fileOutputStream = new FileOutputStream(outputFile);
ClassReader classReader = new ClassReader(fileInputStream);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
classReader
.accept(new LogMethodVisitor(Opcodes.ASM7, classWriter), ClassReader.EXPAND_FRAMES);
fileOutputStream.write(classWriter.toByteArray());
fileInputStream.close();
fileOutputStream.close();} catch (Exception e) {e.printStackTrace();
}
}
}
public class LogMethodVisitor extends ClassVisitor {public LogMethodVisitor(int api, ClassVisitor classVisitor) {super(api, classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature,
String[] exceptions) {
MethodVisitor methodVisitor =
super.visitMethod(access, name, descriptor, signature, exceptions);
return new MethodVisitor(api, methodVisitor) {
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor,
boolean isInterface) {mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("ASM Transform Running");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V",
false);
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
};
}
}
外围就是拜访 class 文件,拜访办法,注入代码。具体的具体规定,能够本人找材料,具体理解一下 ASM 用法,还能够找相应的插件帮忙你理解字节码。
要害截图
目录在 app/build/transforms/LogTransform
总结
这篇文章,咱们须要晓得 Transform 是什么,什么是 ASM,咱们通过两者能够做一些什么事件就能够了。我也自我总结一下,我发现之前看过的 JVM 一书,还是不够彻底,字节码还是要多理解,而且我对打包的流程也更加理解了,真的是遇到问题才会更加容易的发现问题。