请点赞关注,你的支持对我意义重大。
🔥 Hi,我是小彭。本文已收录到 GitHub · AndroidFamily 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,关注公众号 [彭旭锐] 带你建立核心竞争力。
前言
目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task。在这篇文章里,我将带你理解 Transform 的工作机制、使用方法和核心源码解析,并通过一个 Github · DemoHall · HelloTransform Demo 帮助你融会贯通。
这篇文章是 Gradle 系列文章第 8 篇,相关 Android 工程化专栏完整文章列表:
一、Gradle 基础:
- 1、Gradle 基础 :Wrapper、Groovy、生命周期、Project、Task、增量
- 2、Gradle 插件:Plugin、Extension 扩展、NamedDomainObjectContainer、调试
- 3、Gradle 依赖管理
- 4、Maven 发布:SHAPSHOT 快照、uploadArchives、Nexus、AAR
- 5、Gradle 插件案例:EasyPrivacy、so 文件适配 64 位架构、ABI
二、AGP 插件:
- 1、AGP 构建过程
- 2、AGP 常用配置项:Manifest、BuildConfig、buildTypes、壳工程、环境切换
- 3、APG Transform:AOP、TransformTask、增量、字节码、Dex
- 4、AGP 代码混淆:ProGuard、R8、Optimize、Keep、组件化
- 5、APK 签名:认证、完整性、v1、v2、v3、Zip、Wallet
- 6、AGP 案例:多渠道打包
三、组件化开发:
- 1、方案积累:有赞、蘑菇街、得到、携程、支付宝、手淘、爱奇艺、微信、美团
- 2、组件化架构基础
- 3、ARouter 源码分析
- 4、组件化案例:通用方案
- 5、组件化案例:组件化事件总线框架
- 6、组件化案例:组件化 Key-Value 框架
四、AOP 面向切面编程:
- 1、AOP 基础
- 2、Java 注解
- 3、Java 注解处理器:APT、javac
- 4、Java 动态代理:代理模式、Proxy、字节码
- 5、Java ServiceLoader:服务发现、SPI、META-INF
- 6、AspectJ 框架:Transform
- 7、Javassist 框架
- 8、ASM 框架
- 9、AspectJ 案例:限制按钮点击抖动
五、相关计算机基础
1.1 什么是 Transform?
Transform API 是 Android Gradle Plugin 1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class→Dex 这个节点修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,借助 Javassist 或 ASM 等字节码编辑工具进行修改,插入自定义逻辑。一般来说,这些自定义逻辑是与业务逻辑无关的。
使用 Transform 的常见的应用场景有:
- 埋点统计: 在页面展现和退出等生命周期中插入埋点统计代码,以统计页面展现数据;
- 耗时监控: 在指定方法的前后插入耗时计算,以观察方法执行时间;
- 方法替换: 将方法调用替换为调用另一个方法。
1.2 Transform 的基本原理
先大概了解下 Transform 的工作机制:
- 1、工作时机: Transform 工作在 Android 构建中由 Class → Dex 的节点;
- 2、处理对象: 处理对象包括 Javac 编译后的 Class 文件、Java 标准 resource 资源、本地依赖和远程依赖的 JAR/AAR。Android 资源文件不属于 Transform 的操作范围,因为它们不是字节码;
- 3、Transform Task: 每个 Transform 都对应一个 Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出。每个 TransformTask 的输出都分别存储在
app/build/intermediates/transform/[Transform Name]/[Variant]
文件夹中; - 4、Transform 链: TaskManager 会将每个 TransformTask 串联起来,前一个 Transform 的输出会作为下一个 Transform 的输入。
1.3 Transform API
了解 Transform 的基本工作机制后,我们先来看 Transform 的核心 API。这里仅列举出 Transform 抽象类中最核心的方法,有几个次要的方法后面再说。
com.android.build.api.transform.java
1 | typescript复制代码public abstract class Transform { |
1.4 ContentType 内容类型
ContentType 是一个枚举类接口,表示输入或输出内容的类型,在 AGP 中定义了 DefaultContentType
和 ExtendedContentType
两个枚举类。但是,我们在自定义 Transform 时只能使用 DefaultContentType 中定义的枚举,即 CLASSES
和 RESOURCES
两种类型,其它类型仅供 AGP 内置的 Transform 使用。
自定义 Transform 需要在两个位置定义内容类型:
- 1、Set getInputTypes(): 指定输入内容类型,允许通过 Set 集合设置输入多种类型;
- 2、Set getOutputTypes(): 指定输出内容类型,默认取 getInputTypes() 的值,允许通过 Set 集合设置输出多种类型。
ExtendedContentType.java
1 | scss复制代码// 加强类型,自定义 Transform 无法使用 |
QualifiedContent.java
1 | scss复制代码enum DefaultContentType implements ContentType { |
在 TransformManager 中,预定义了一部分内容类型集合,常用的是 CONTENT_CLASS 操作 Class。
TransformManager.java
1 | swift复制代码public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES); |
1.5 ScopeType 作用域
ScopeType 也是一个枚举类接口,表示输入内容的范畴。在 AGP 中定义了 InternalScope
和 Scope
两个枚举类。但是,我们在自定义 Transform 只能使用 Scope 中定义的枚举,其它类型仅供 AGP 内置的 Transform 使用。
Transform 需要在两个位置定义输入内容范围:
- 1、Set getScopes() 消费型输入内容范畴: 此范围的内容会被消费,因此当前 Transform 必须将修改后的内容复制到 Transform 的中间目录中,否则无法将内容传递到下一个 Transform 处理;
- 2、Set getReferencedScopes() 指定引用型输入内容范畴: 默认是空集合,此范围的内容不会被消费,因此不需要复制传递到下一个 Transform,也不允许修改。
InternalScope.java
1 | scss复制代码// 内部使用的作用域,自定义 Transform 无法使用 |
QualifiedContent.java
1 | scss复制代码enum Scope implements ScopeType { |
在 TransformManager 中,预定义了一部分作用域集合,常用的是 SCOPE_FULL_PROJECT 所有模块。需要注意,Library 模块注册的 Transform 只能使用 Scope.PROJECT。
TransformManager.java
1 | swift复制代码public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT); |
1.6 transform 方法
transform() 是实现 Transform 的核心方法,方法的参数是 TransformInvocation,它提供了所有与输入输出相关的信息:
1 | csharp复制代码public interface TransformInvocation { |
- isIncremental(): 当前 Transform 任务是否增量构建;
- getInputs(): 获取 TransformInput 对象,它是消费型输入内容,对应于 Transform#getScopes() 定义的范围;
- getReferencedInputs(): 获取 TransformInput 对象,它是引用型输入内容,对应于 Transform#getReferenceScope() 定义的内容范围;
- getOutPutProvider(): TransformOutputProvider 是对输出文件的抽象。
输入内容 TransformInput 由两部分组成:
- DirectoryInput 集合: 以源码方式参与构建的输入文件,包括完整的源码目录结构及其中的源码文件;
- JarInput 集合: 以 Jar 和 aar 依赖方式参与构建的输入文件,包含本地依赖和远程依赖。
输入内容信息 TransformOutputProvider 有两个功能:
- deleteAll(): 当 Transform 运行在非增量构建模式时,需要删除上一次构建产生的所有中间文件,可以直接调用 deleteAll() 完成;
- getContentLocation(): 获得指定范围+类型的输出目标路径。
TransformOutputProvider.java
1 | typescript复制代码public interface TransformOutputProvider { |
获取输入内容对应的输出路径:
1 | scss复制代码for (input in transformInvocation.inputs) { |
1.7 Transform 增量模式
任何构建系统都会尽量避免重复执行相同工作,Transform 也不例外。虽然增量构建并不是必须的,但作为一个合格的 Transform 实现应该具备增量能力。
1、增量模式标记位: Transform API 有两个增量标志位,不要混淆:
- Transform#isIncremental(): Transform 增量构建的使能开关,返回 true 才有可能触发增量构建;
- TransformInvocation#isIncremental(): 当次 TransformTask 是否增量执行,返回 true 表示正在增量模式。
2、Task 增量模式与 Transform 增量模式的区别: Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。
3、增量模式的输入: 增量模式下的所有输入都是带状态的,需要根据这些状态来做不同的处理,不需要每次所有流程都重新来一遍。比如新增的输入就需要处理,而未修改的输入就不需要处理。Transform 定义了四个输入文件状态:
com.android.build.api.transform.Status.java
1 | arduino复制代码public enum Status { |
1.8 注册 Transform
在 BaseExtension 中维护了一个 Transform 列表,自定义 Transform 需要注册才能生效,而且还支持额外设置 TransformTask 的依赖。
BaseExtension.kt
1 | kotlin复制代码abstract class BaseExtension { |
注册 Transform:
1 | kotlin复制代码// 获取 Android 扩展 |
提示: 为了提高编译效率,可以判断 Variant 为 release 类型才注册 Transform,也可以通过重写 Transform#applyToVariant() 来决定是否执行 Transform。
这一节我们来分析 Transform 相关核心源码,这里我们引用的是 Android Gradle Plugin 7.1.0 版本的源码。
2.1 Transform 与 Task 的关系
Project 的构建逻辑由一系列 Task 的组成,每个 Task 负责完成一个基本的工作,例如 Javac 编译 Task。Transform 也是依靠 Task 执行的,在配置阶段,Gradle 会为注册的 Transform 创建对应的 Task。
提示: 说 “创建” 可能不太严谨,TransformManager 使用 register 懒创建的方式注册 Task,其实还没有创建 Task 实例。我们不要复杂化了,就说创建吧。
而 Task 的依赖关系是通过 TransformTask 的输入输出关系隐式确定的,TransformManager 通过 TransformStream 链接各个 TransformTask 的输入输出,进而控制 Transform 的依赖关系顺序。
LibraryTaskManager.java
1 | scss复制代码@Override |
网上很多朋友提到 “自定义 Transform 的执行时机早于系统内置 Transform”,但从 AGP 7.1.0 源码看,并不存在系统 Transform。猜测是新版本 AGP 将这部分 “系统内置 Transform” 修改为由 Task 直接实现,毕竟 从 AGP 7.0 开始 Transform 标记为过时了。
2.2 Transform 的创建过程
- 1、注册 Transform: 注册 Transform 仅是将对象注册到 BaseExtension 中的列表中。TransformManager 会通过 Task 的输入输出隐式建立 Transform 的依赖顺序,另外还支持在注册时添加额外的依赖。
BaseExtension.kt
1 | kotlin复制代码abstract class BaseExtension { |
- 2、创建 TransformTask 的执行链: TransformTask 属于 Android 构建构成的一部分,所有 Android Task 的创建入口都从 BasePlugin#createAndroidTasks() 开始。其中会为所有 Variant 变体创建相关的 Task,经过一系列调用后,会通过抽象方法 TaskManager#doCreateTaskForVariant() 分派到 ApplicationTaskManager 和 LibraryTaskManager 两个子类中,以区分 App 模块和 Library 模块。
调用链概要:
1 | scss复制代码BasePlugin#createAndroidTasks() |
2.3 TransformTask 的命名格式
Transform#getName() 会用于构造 Task Name,命名格式为 transform[InputTypes]With[name]For[Configuration]
,例如 transformClassed。这块源码体现在 TransformManager 中创建 Task 的位置:
TransformManager.java
1 | scss复制代码// 创建 Transform Task |
2.4 TransformTask 的输入输出
TransformTask 通过 @Input 和 @OutputDirectory 等注解,将 Transform API 关联到 Task 的输入输出上:
TransformTask.java
1 | scala复制代码public abstract class TransformTask extends StreamBasedTask { |
2.5 执行 transform() 方法
每个 Task 内部都保持了一个 Action 列表 actions
,执行 Task 就是按顺序执行这个列表,对于自定义 Task,可以通过 @TaskAction
注解添加默认 Action。
TransformTask.java
1 | less复制代码@TaskAction |
2.6 Library 模块限制
Library 模块仅只支持使用 Scope.PROJECT 作用域:
LibraryTaskManager.java
1 | scss复制代码// Check the transform only applies to supported scopes for libraries: |
上一节我们探讨了 Transform 的基本工作机制,第 3 节和第 4 节我们来实现一个 Transform Demo。Transform 的核心代码在 transform() 方法中,我们要做的就是遍历输入文件,再把修改后的文件复制到目标路径中,对于 JarInputs 还有一次解压和压缩。更进一步,再考虑增量编译的情况。
因此,整个 Transform 的核心过程是有固定套路,模板流程图如下:
—— 图片引用自 rebooters.github.io/2020/01/04/…
我们把整个流程图做成一个抽象模板类,子类需要重写 provideFunction()
方法,从输入流读取 Class 文件,修改完字节码后再写入到输出流。甚至不需要考虑 Trasform 的输入文件遍历、加解压、增量等,舒服!
BaseCustomTransform.kt
1 | kotlin复制代码abstract class BaseCustomTransform(private val debug: Boolean) : Transform() { |
现在,我手把手带你基于 BaseCustomTransform 实现一个 Transform Demo。示例代码我已经上传到 Github · DemoHall · HelloTransform。有用请给个免费的 Star 支持下。
Demo 效果很简单:
- 实现一个 Transform,在编译时在 Activity#onCreate() 方法末尾织入一个 Toast 语句;
- 仅通过自定义注解 @Hello 修饰的 Activity#onCreate() 方法会生效。
4.1 步骤 1:初始化代码框架
首先,我们先搭建工程的整体框架,再来编写核心的 Transform 逻辑。我们选择自定义 Gradle 插件来承载 Transform 的逻辑,可维护性更好。关于自定义 Gradle 插件的步骤具体见上一篇文章《手把手带你自定义 Gradle 插件》,此处不展开。
提示: 提醒一下,并不是说一定要由 Gradle 插件来承载,你直接在 .gradle 文件中实现也是 OK 的。
插件实现类如下:
ToastPlugin.kt
1 | kotlin复制代码class ToastPlugin : Plugin<Project> { |
4.2 步骤 2:拷贝 Transform 模板类
将我们实现的 BaseCustomTransform 模板类复制到工程下,再实现一个子类:
ToastTransform.kt
1 | kotlin复制代码internal class ToastTransform(val project: Project) : BaseCustomTransform(true) { |
其中,provideFunction() 是模板代码,参数分别表示源 Class 文件的输入流和目标 Class 文件输出流。子类要做的事,就是从输入流读取 Class 信息,修改后写入到输出流。
4.3 步骤 3:使用 Javassist 修改字节码
使用 Javassist API 从输入流加载数据,在匹配到 onCreate() 方法后检查是否声明 @Hello 注解。是则在该方法末尾织入一句 Toast:Hello Transform。本文重点不是 Javassist,此处就不展开了。
1 | kotlin复制代码override fun provideFunction() = { ios: InputStream, zos: OutputStream -> |
4.4 步骤 4:应用插件
sample 模块 build.gradle
1 | arduino复制代码apply plugin: 'com.pengxr.toastplugin' |
4.5 步骤 5:声明 @Hello 注解
HelloActivity.kt
1 | kotlin复制代码class HelloActivity : AppCompatActivity() { |
4.6 步骤 6:运行
完成以上步骤后,编译运行程序。可以在 Build Output 看到以下输出,HelloActivity 启动时会弹出 Toast HelloTransform,说明织入成功。
1 | ruby复制代码... |
从 AGP 7.0 开始,Transform API 已经被废弃了。是的,就是卷,而且这次直接是降维打击。以前 Transform 是 AGP 的特性,现在 Gradle 也来整 Transform,不过换了个名字,叫 —— TransformAction。
那么,我们还有必要学 AGP Transform API 吗?如果你现在涉足字节码插桩这块,你建议你还是学以下:
- 1、社区沉淀: AGP Transform API 发展多年,目前社区中已经沉淀下非常多优秀的开源组件和博客,这些资源对你非常有帮助。而 TransformAction 的社区沉淀还非常单薄;
- 2、技术思维: 虽然换了一套 API,但背后的思路 / 套路是相似的。理解 AGP Transform 的工作机制,对你理解 Gradle TransformAction 有事半功倍的效果。
例如,以下是 Gradle 官方文档的演示代码,是不是套路差不多?
1 | less复制代码abstract class CountLoc implements TransformAction<TransformParameters.None> { |
本文的示例代码已上传到 github.com/pengxurui/D…,请 Star 支持。关注我,带你了解更多,我们下次见。
2022 年 8 月 15 号更新
经评论区 @奋斗的bigHead 反馈,BaseCustomTransform 存在缺陷:在 classFilter() 设置过滤部分 class 文件(例如 className.endsWith("Activity.class")
)时,编译时会出现找不到类。这是因为 BaseCustomTransform 未透明复制被过滤掉的文件。该缺陷现已修复,模板代码和 GitHub Demo 已同步修改。
参考资料
- Gradle Transform + ASM 探索 —— REBOOTERS 著
- 深入理解 Transform —— toothpickTina 著
- 现在准备好告别 Transform 了吗? —— 究极逮虾户 著
- AGP Transform API 被废弃意味着什么? —— johnsonlee 著
- Transforming dependency artifacts on resolution —— Gradle 官方文档
我是小彭,带你构建 Android 知识体系。技术和职场问题,请关注公众号 [彭旭锐] 私信我提问。
本文转载自: 掘金