我们先来看下Android应用程序打包流程:

通过上图可知,我们只要在图中红色箭头处拦截(生成class文件之后,dex文件之前),就可以拿到当前应用程序中所有的.class文件,再去借助ASM之类的库,就可以遍历这些.class文件中所有方法,再根据一定的条件找到需要的目标方法,最后进行修改并保存,就可以插入我们的埋点代码。

Google从 Android Gradle 1.5.0 开始,提供了Transform API。通过Transform API,允许第三方以插件的形式,在Android应用程序打包成dex文件之前的编译过程中操作.class文件。我们只要实现一套Transform,去遍历所有.class文件的所有方法,然后进行修改(在特定的listener回调中插入埋点代码),再对源文件进行替换,即可以达到插入代码的目的。

Gradle Transform概述

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
  • TransformOutputProvider

TransformInput

TransformInput是指输入文件的一个抽象,包括:

  • DitectoryInput集合
    是指以源码的方式参与项目编译的所有目录结构及其目录下的源码文件

  • JarInput集合
    是指以jar包方式参与项目编译的所有本地jar包和远程jar包(此处的jar包包括aar)

TransformOutputProvider

之Transform的输出,通过它可以获取到输出路径等信息

Transform.java

先来了解下Transform类,定义如下

public abstract class Transform {public Transform() {}// Transform名称public abstract String getName();public abstract Set<ContentType> getInputTypes();public Set<ContentType> getOutputTypes() {return this.getInputTypes();}public abstract Set<? super Scope> getScopes();public abstract boolean isIncremental();/** @deprecated */@Deprecatedpublic 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());}public boolean isCacheable() {return false;}...
}

Transform#getName()

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();}

Transform#getInputTypes()

需要处理的数据类型,有两种枚举类型

  • CLASSES
    代表处理的 java 的 class 文件,返回TransformManager.CONTENT_CLASS

  • RESOURCES
    代表要处理 java 的资源,返回TransformManager.CONTENT_RESOURCES

Transform#getScopes()

指 Transform 要操作内容的范围,官方文档 Scope 有 7 种类型:

  1. EXTERNAL_LIBRARIES ----------------只有外部库
  2. PROJECT -----------------------------------只有项目内容
  3. PROJECT_LOCAL_DEPS ------------- 只有项目的本地依赖(本地jar)
  4. PROVIDED_ONLY ------------------------只提供本地或远程依赖项
  5. SUB_PROJECTS -------------------------只有子项目
  6. SUB_PROJECTS_LOCAL_DEPS-----只有子项目的本地依赖项(本地jar)
  7. TESTED_CODE ---------------------------由当前变量(包括依赖项)测试的代码
    如果要处理所有的class字节码,返回TransformManager.SCOPE_FULL_PROJECT

Transform#isIncremental()

增量编译开关

当我们开启增量编译的时候,相当input包含了changed/removed/added三种状态,实际上还有notchanged。需要做的操作如下:

  • NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
  • ADDED、CHANGED: 正常处理,输出给下一个任务;
  • REMOVED: 移除outputProvider获取路径对应的文件。

Transform#transform()

 public void transform(@NonNull TransformInvocation transformInvocation)throws TransformException, InterruptedException, IOException {// Just delegate to old method, for code that uses the old API.//noinspection deprecationthis.transform(transformInvocation.getContext(), transformInvocation.getInputs(),transformInvocation.getReferencedInputs(),transformInvocation.getOutputProvider(),transformInvocation.isIncremental());}

注意点

  • 如果拿取了getInputs()的输入进行消费,则transform后必须再输出给下一级
  • 如果拿取了getReferencedInputs()的输入,则不应该被transform
  • 是否增量编译要以transformInvocation.isIncremental()为准

Transform#isCacheable()

如果我们的transform需要被缓存,则为true,它被TransformTask所用到

Transform编写模板

无增量编译

AspectJTransform.groovy代码如下:

class AspectJTransform extends Transform {final String NAME =  "JokerwanTransform"@OverrideString getName() {return NAME}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return false}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)// OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == nullTransformOutputProvider outputProvider = transformInvocation.getOutputProvider();transformInvocation.inputs.each { TransformInput input ->input.jarInputs.each { JarInput jarInput ->// 处理JarprocessJarInput(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"@OverrideString getName() {return NAME}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return true}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)boolean isIncremental = transformInvocation.isIncremental()// OutputProvider管理输出路径,如果消费型输入为空,你会发现OutputProvider == nullTransformOutputProvider outputProvider = transformInvocation.getOutputProvider()if (!isIncremental) {// 不需要增量编译,先清除全部outputProvider.deleteAll()}transformInvocation.getInputs().each { TransformInput input ->input.jarInputs.each { JarInput jarInput ->// 处理JarprocessJarInputWithIncremental(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:breakcase Status.ADDED:case Status.CHANGED:// 处理有变化的transformJarInputWhenIncremental(jarInput.getFile(), dest, jarInput.status)breakcase Status.REMOVED:// 移除Removedif (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<File, Status> fileStatusMap = directoryInput.getChangedFiles()fileStatusMap.each { Map.Entry<File, Status> 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:breakcase Status.REMOVED:if (destFile.exists()) {FileUtils.forceDelete(destFile)}breakcase 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

Gradle Transform 详解相关推荐

  1. Android build.gradle文件详解(转述自《Android第一行代码》第二版)

    Android build.gradle文件详解 1. 最外层目录下的build.gradle文件 1.1 repostories 1.2 dependencies 2. app目录下的build.g ...

  2. html旋转角度计算,CSS3属性transform详解之(旋转:rotate,缩放:scale,倾斜:skew,移动:translate) | 0101后花园...

    CSS3属性transform详解之(旋转:rotate,缩放:scale,倾斜:skew,移动:translate) | 0101后花园 2018-09-26 在CSS3中,可以利用transfor ...

  3. 史上最全Android build.gradle配置详解

    Android Studio是采用gradle来构建项目的,gradle是基于groovy语言的,如果只是用它构建普通Android项目的话,是可以不去学groovy的.当我们创建一个Android项 ...

  4. Gradle Wrapper 详解

    Gradle Wrapper 详解 我们介绍了 Android 项目的目录及 Gradle 配置,我们提到有个目录是/gradle/wrapper.今天这篇文章我们来学习 Gradle Wrapper ...

  5. Transform详解

    目录 1.Transform简介 2.Transform结构 3.Transform encoder过程 4.Attention 5.Self-Attention 5.1.self-Attention ...

  6. Unity快速入门之二 GUI Transform 详解

    Unity快速入门之一 3D基础概念.Camera.Canvas RenderMode的几种方式对比_翕翕堂 Unity快速入门之二 GUI Transform 详解_翕翕堂 Unity快速入门之三 ...

  7. Gradle 配置详解

    Gradle 配置详解 我们为大家介绍一下 Android 项目中 Gradle 的配置. 1. AndroidStudio 项目结构 我们介绍 AndroidStudio 中 Android 项目的 ...

  8. Gradle命令详解

    Gradle命令详解 我们介绍了 Gradle 的任务声明,任务依赖,Gradle 构建的顺序等.其实在文章中我们也提到了一些 Gradle 命令.本文我们将为大家介绍一下 Gradle 的命令,包括 ...

  9. Android Studio build.gradle配置详解

    Android Studio是采用gradle来构建项目的,gradle是基于groovy语言的,如果只是用它构建普通Android项目的话,是可以不去学groovy的.当我们创建一个Android项 ...

最新文章

  1. ECSHOP学习笔记
  2. android Spinner 例子
  3. 应用vb编程_用VB编程来解决实际生活问题
  4. 23种设计模式中的蝇量(享元)模式
  5. jvm十:类加载器解析
  6. Linux 技巧:让进程在后台可靠执行的几种方法
  7. mysql 编译 bsion_mysql编译安装
  8. 聊聊hystrix的semaphore.maxConcurrentRequests属性
  9. jitpack发布_JitPack –发布您的Android库
  10. php 协议头,入门PHP实现MQTT协议的固定头部(Fix header)
  11. Linux之ssh无密码登录
  12. 基于python的注册登录界面_基于python的Tkinter编写登陆注册界面
  13. 2022考研【王道计算机408】【天勤计算机408】数据结构+操作系统+计算机组成原理+计算机网络
  14. hello.java_helloworld怎么写java
  15. 注册github邮箱验证收不到邮件问题
  16. GCC:warning:control reaches end of non-void function [-Wreturn-type] 、 Coredump的情况
  17. html中鼠标点击图片变动,JS实现页面鼠标点击出现图片特效
  18. “网上世博会”带来创新体验,水晶石着力推动“数字展览”应用
  19. Python 05 包Packet
  20. 7-15 计算圆周率

热门文章

  1. 基于ssm框架的人才招聘网站
  2. 1.Transformer的word embedding、position embedding、编码器子注意力的掩码
  3. linux rm的使用与注意事项
  4. Chosen.2 初始 球场 改变
  5. C# 企业微信接口发送消息出现错误代码60020解决方案,希望能给大家带来帮助。
  6. 什么是Jackson?(常用Jackson属性解析)
  7. 衡石数据全旅程之数据准备
  8. 学习笔记:触摸事件MotionEvent
  9. 字节跳动和美团为什么都在「变硬」
  10. 英伟达开发板打包遇到问题记录