问题

之前始终在听他们说函数插桩,字节码插桩,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一书,还是不够彻底,字节码还是要多理解,而且我对打包的流程也更加理解了,真的是遇到问题才会更加容易的发现问题。