AGP tramsform

阳光穿透心脏的1/2处 2022-11-08 14:23 219阅读 0赞

Transform 是什么

TransformAGP提供了一个API,可以在java编译成class后进行一系列的转化,如插桩和埋点统计等。

我们看下正常编译流程:
在这里插入图片描述
启用transform之后:
在这里插入图片描述

示例源码地址:
https://github.com/fanmingyi/AGP-Transfrom-Example

Transform 案例

本文利用Transform完成如下几件事:

  1. 将一个com.example.agptramsform.MainActivity修改继承自BaseProxyActivity,
  2. 在onCreate函数前后统计调用时间

为了实现字节码的修改,我们利用javassist完成。AGP使用4.1.2.

首先声明一个插件类,让插件类找到AGP所提供的函数进行Transform注册.

  1. public abstract class MyGradlePlugin implements Plugin<Project> {
  2. @Override
  3. public void apply(Project project) {
  4. //模块应用android插件如plugins { id 'kotlin-android'}后
  5. //会有一个BaseExtension扩展,这个扩展类提供了注册一个Transform的功能
  6. project.getExtensions().findByType(BaseExtension.class)
  7. .registerTransform(new MyTransform(project));
  8. }
  9. }

接下来编写Transform的实现类即可:

我们需要一个类继承Transform,然后覆盖一些特定的方法。

  1. public class MyTransform extends Transform {
  2. /*
  3. * 你的Transform的名字,你可以随意取。方便在编译时查看日志
  4. */
  5. @Override
  6. public String getName() {
  7. return "MyFmyMyTransform";
  8. }
  9. /*
  10. * 这个函数应返回的你的Transform应该处理什么类型的内容
  11. * 你可以声明逆向处理java的资源文件如图片,或者你只处理类
  12. * ContentType的自类有两种:CLASSES和RESOURCES
  13. *
  14. * TransformManager中有一些方面我们使用的预定义集合类型
  15. * class TransformManager{
  16. * Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
  17. * }
  18. */
  19. @Override
  20. public Set<QualifiedContent.ContentType> getInputTypes() {
  21. //我们这里只处理class文件
  22. return CONTENT_CLASS;
  23. }
  24. /**
  25. * 这个函数你想处理的范围.比如说你只想处理当前工程的类和资源(QualifiedContent.Scope.PROJECT).或者你只想处理外部类库的资源(QualifiedContent.Scope.EXTERNAL_LIBRARIES)
  26. **/
  27. @Override
  28. public Set<? super QualifiedContent.Scope> getScopes() {
  29. Set<QualifiedContent.ScopeType> d =
  30. ImmutableSet.of(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.SUB_PROJECTS, QualifiedContent.Scope.EXTERNAL_LIBRARIES);
  31. return d;
  32. }
  33. //当前工程是否支持增量
  34. //如果不开启那么Transform在多次编译下,很耗费时间
  35. @Override
  36. public boolean isIncremental() {
  37. return true;
  38. }
  39. //这个函数会在进行转化的时候进行回调,也就是执行核心的转化方法。
  40. @Override
  41. public void transform(TransformInvocation transformInvocation) {
  42. }
  43. }

这里我们总结这个类的几个方法:
1 getInputTypes 你想处理类还是资源

2 getScopes 你想处理哪里的 类和资源?当前工程还是依赖的类库?

3 isIncremental 当前Transform是否支持增量调用?如果你不知道什么是增量 返回false即可

4 transform 执行具体的转化函数

我们看下TransformInvocation这个类的有关信息

  1. public interface TransformInvocation {
  2. /**
  3. * 返回transform运行的上下文
  4. */
  5. @NonNull
  6. Context getContext();
  7. /**
  8. * 这个集合是你getScopes和getInputTypes所定义的资源/class的输入信息
  9. */
  10. @NonNull
  11. Collection<TransformInput> getInputs();
  12. /**
  13. * AGP要求getInputs处理之后的资源或者类输出目录。
  14. * 这里注意getInputs的资源和类哪怕你没处理也要输出到TransformOutputProvider所指定的目录
  15. */
  16. @Nullable
  17. TransformOutputProvider getOutputProvider();
  18. /**
  19. * 当前执行的是否是增量操作
  20. */
  21. boolean isIncremental();
  22. }

我们在继续讲解transform类之前我们先写一个工具类,对某个MainActivity进行字节操作。
下面我们利用javassist完成字节操作

  1. public class TransformKit {
  2. ClassPool pool = ClassPool.getDefault();
  3. Project project;
  4. public TransformKit(Project project) {
  5. this.project = project;
  6. }
  7. //searchDir类路径 比如是build/intermediates/javac/debug/classes
  8. public void transform(String searchDir) throws NotFoundException, CannotCompileException, IOException {
  9. //添加到javassist搜索路径中
  10. pool.appendClassPath(searchDir);
  11. //查找类
  12. CtClass mainCtClass = pool.get("com.example.agptramsform.MainActivity");
  13. //我们的将要MainActivity继承的类
  14. CtClass baseProxyCtClass = pool.get("com.example.agptramsform.BaseProxyActivity");
  15. //修改字节码
  16. mainCtClass.setSuperclass(baseProxyCtClass);
  17. //这里再次获得AGP的扩展类,这里主要是为了得到sdk的中android部分类库。不然无法找到android.os.Bundle等android类
  18. BaseExtension android = project.getExtensions().findByType(BaseExtension.class);
  19. //将sdk中的类库资源添加到搜索区域
  20. pool.appendClassPath(android.getBootClasspath().get(0).toString();
  21. pool.importPackage("android.os.Bundle");
  22. CtMethod onCreate = mainCtClass.getDeclaredMethod("onCreate");
  23. //函数运行前的插入一个代码
  24. onCreate.insertBefore("long _startTime = System.currentTimeMillis();");
  25. //函数运行最后一行插入代码
  26. onCreate.insertAfter("long _endTime = System.currentTimeMillis();");
  27. mainCtClass.writeFile(searchDir);
  28. mainCtClass.detach();
  29. baseProxyCtClass.detach();
  30. }
  31. }

我们最后看下MyTransformtransform函数实现

  1. public class MyTransform extends Transform {
  2. Project project;
  3. @Override
  4. public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
  5. super.transform(transformInvocation);
  6. //构造一个字节码操作工具类
  7. TransformKit transformKit = new TransformKit(project);
  8. //得到期望的资源/类的输入路径信息
  9. Collection<TransformInput> inputs = transformInvocation.getInputs();
  10. //AGP要求transform输出的目录
  11. TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
  12. //当前不是增量更新的那么删除这个transform的缓存文件 //build/intermediates/transforms/xxxxxx
  13. if (!transformInvocation.isIncremental()) {
  14. //如果当前不是增量,应该删除之前的所有缓存信息,防止意料之外错误
  15. outputProvider.deleteAll();
  16. }
  17. //遍历所有输入信息,进行处理
  18. for (TransformInput transformInput : inputs) {
  19. //TransformInput输入类型有两种目录类型,一种是jar类型的,一种就是目录
  20. //遍历jar文件 对jar不操作,但是要输出到out路径
  21. //parallelStream多线程进行处理集合类
  22. transformInput.getJarInputs().parallelStream().forEach(jarInput -> {
  23. //获取AGP要求输出的目录,哪怕你没修改也要输出
  24. File dst = outputProvider.getContentLocation(
  25. jarInput.getName(), jarInput.getContentTypes(), jarInput.getScopes(),
  26. Format.JAR);
  27. //处理增量情况
  28. if (transformInvocation.isIncremental()) {
  29. switch (jarInput.getStatus()) {
  30. //未做任何改变
  31. case NOTCHANGED:
  32. break;
  33. case ADDED:
  34. //如果一个jar被添加,需要被拷贝回来
  35. case CHANGED:
  36. //是一个新的文件,那么需要拷贝回来
  37. try {
  38. FileUtils.copyFile(jarInput.getFile(), dst);
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. break;
  43. //当前的输入源已经被删除,那么transform下的对应文件理应被删除
  44. case REMOVED:
  45. if (jarInput.getFile().exists()) {
  46. try {
  47. FileUtils.forceDelete(jarInput.getFile());
  48. } catch (IOException e) {
  49. e.printStackTrace();
  50. }
  51. }
  52. break;
  53. }
  54. } else {
  55. //非增量拷贝源集类路径
  56. try {
  57. FileUtils.copyFile(jarInput.getFile(), dst);
  58. } catch (IOException e) {
  59. throw new RuntimeException(e);
  60. }
  61. }
  62. });
  63. //同上
  64. for (DirectoryInput directoryInput : transformInput.getDirectoryInputs()) {
  65. // 获取输出目录
  66. File dest = outputProvider.getContentLocation(directoryInput.getName(),
  67. directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
  68. FileCollection filter = project.fileTree(directoryInput.getFile())
  69. .filter(innerFile -> innerFile.getName().equals("MainActivity.class"));
  70. //当前是增量的状态,所以遍历这个文件夹下的所有文件
  71. if (transformInvocation.isIncremental()) {
  72. Map<File, Status> changedFiles = directoryInput.getChangedFiles();
  73. //遍历文件状态
  74. BiConsumer<File, Status> fileStatusBiConsumer = (file, status) -> {
  75. switch (status) {
  76. //这个文件夹不做任何事情
  77. case NOTCHANGED:
  78. break;
  79. case CHANGED:
  80. case ADDED:
  81. //顺带检查下是否存在我们目标的文件,如果存在那么修改字节码后在拷贝
  82. if (file.getName().equals("MainActivity.class")) {
  83. try {
  84. transformKit.transform(directoryInput.getFile().getAbsolutePath());
  85. } catch (Exception e) {
  86. e.printStackTrace();
  87. }
  88. }
  89. try {
  90. /**
  91. * 处理方式一 简单粗暴
  92. */
  93. //偷懒就直接拷贝文件夹 但是效率低
  94. // FileUtils.copyDirectory(directoryInput.getFile(), dest);
  95. /**
  96. * 处理方式二 高效 略复杂
  97. */
  98. //构建目录连带包名
  99. //file 可能的目录是 /build/intermediates/java/debug/com/fmy/MainActivity.class
  100. //dest 可能目标地址 /build/intermediates/transforms/mytrasnsfrom/debug/40/
  101. //directoryInput.getFile() 可能的输入类的文件夹 /build/intermediates/java/debug/
  102. File dirFile = directoryInput.getFile();
  103. String prefixPath = file.getAbsolutePath().replaceFirst(dirFile.getAbsolutePath(), "");
  104. System.out.println();
  105. //重新拼接成/build/intermediates/transforms/mytrasnsfrom/debug/40/com/fmy/MainActivity.class
  106. File specifyDest = new File(dest.getAbsolutePath(), prefixPath);
  107. FileUtils.copyFile(file, specifyDest);
  108. } catch (Exception e) {
  109. e.printStackTrace();
  110. }
  111. break;
  112. case REMOVED:
  113. //文件被删除直接删除相关文件即可
  114. try {
  115. FileUtils.forceDelete(file);
  116. } catch (IOException e) {
  117. e.printStackTrace();
  118. }
  119. break;
  120. }
  121. };
  122. changedFiles.forEach(fileStatusBiConsumer);
  123. } else {
  124. if (!filter.isEmpty()) {
  125. try {
  126. transformKit.transform(directoryInput.getFile().getAbsolutePath());
  127. } catch (Exception e) {
  128. e.printStackTrace();
  129. }
  130. }
  131. FileUtils.copyDirectory(directoryInput.getFile(), dest);
  132. }
  133. }
  134. }
  135. }
  136. }

运行后可以看到编译输出多了一个task任务:
在这里插入图片描述

参考

Gradle-初探代码注入Transform

Gradle 学习之 Android 插件的 Transform API

Transform Api

发表评论

表情:
评论列表 (有 0 条评论,219人围观)

还没有评论,来说两句吧...

相关阅读