本次分享深入讲解 Android 编译期字节码插桩技术,分为三大模块:
基础概念与技术选型:介绍为何以及如何在编译期修改代码(AOP思想),并对比主流工具(ASM、Javassist、AspectJ、APT等)的优缺点
核心实战(Transform):详细讲解传统 Transform API 的工作原理、配置和高级优化(增量与并发编译),提供结合 ASM 框架实现方法耗时统计、全局异常捕获等5个典型实战案例
现代化方案(AsmClassVisitorFactory):解析AGP新方案的核心优势与API,分享如何从Transform迁移到更高效、更简洁的官方新标准
1. 编译期 Hook 技术
在现代 Java 开发中,我们常常借助各种自动化工具来减少样板代码、提升开发效率。例如,通过简单的注解就能生成 Builder 模式代码、实现对象映射,甚至完成依赖注入配置的静态注册。这些能力的背后,往往依赖于 Java 编译过程中的一个强大机制——编译期 Hook。
1.1 什么是 Java 编译期 Hook?
“编译期 Hook”并非 Java 的官方术语,但它形象地描述了在 javac 将源代码(.java)编译为字节码(.class)的过程中,插入自定义逻辑的扩展机制。它允许开发者在编译期“钩住”编译流程,执行代码分析、验证或生成操作,从而实现源码级别的自动化增强。
比如我们最熟知的一种标准的编译期扩展 API:注解处理器(Annotation Processor),也称为 APT(Annotation Processing Tool)机制。开发者可以注册一个处理器,使其在编译时被 javac 自动调用。该处理器能够扫描源码中的特定注解,并基于这些元数据生成新的 Java 源文件或资源文件。
这个过程完全发生在编译阶段,生成的代码在 IDE 中可见(通常位于 generated 目录),且无需任何运行时反射或代理开销。这种机制正是 编译期 Hook 的典型体现——它提供了一个“入口”,让开发者能够在编译流程中注入自定义逻辑。
1.2 从编译期增强到 AOP 编程
上述基于 APT 的代码生成,本质上已经体现了 AOP(Aspect-Oriented Programming,面向切面编程) 的核心思想:将横切关注点(cross-cutting concerns)从主业务逻辑中分离。常见的横切关注点包括日志记录、事务管理、权限校验、性能监控等,它们往往跨越多个模块,直接嵌入业务代码会导致重复和耦合。
以下是一种形象化的表达,虽然每个人的一天可能过程是不一样的,但仍有一些特点是相同的,例如小明、小丽、小黑他们三位早、中、晚饭是都要吃的,它们都可能是横向的分布在不通的业务层级(对象层级)中,但是又和具体的核心业务无直接关系,诸如这样类型的代码,在程序中被称作横切(cross cutting),我们应该考虑将这一类代码进行统一管理,提高复用性:
AOP 的目标是将这些通用逻辑模块化为“切面(Aspect)”,并以声明式的方式织入目标位置,而无需侵入原有代码。根据织入时机的不同,AOP 可分为运行时织入、编译期织入、加载期织入。其中,编译期织入正是编译期 Hook 的典型应用场景。通过构建工具(如 Maven 或 Gradle 插件)集成注解处理器或字节码操作工具,AOP 框架可以在编译过程中“钩住”编译流程,分析切面定义,并将增强逻辑织入目标类。这种方式生成的字节码在运行时无需额外代理,性能更高,适合对性能敏感的场景。
1.3 字节码操作框架:ASM 与 Javassist 的比较分析
字节码操作框架是实现编译期 Hook 最底层、最直接的技术手段,其中,ASM 和 Javassist 是两个最为知名和广泛使用的库。它们都允许开发者在类加载到 JVM 之前,动态地读取、修改或生成 Java 类的字节码,从而实现诸如** AOP、动态代理、ORM 映射**等功能。尽管目标相似,但两者在设计理念、API 抽象层次和适用场景上存在显著差异。
ASM(Analysis-based Software Modeling)是一个高性能的 Java 字节码操作和分析框架。它的核心设计哲学是基于访问者模式(Visitor Pattern),不试图构建整个类的语法树,而是在读取字节码流的同时,逐个事件地调用相应的回调方法来处理类结构信息。开发者需要继承ClassVisitor、MethodVisitor等抽象类,并重写其中的方法来定义自己的修改逻辑。这种“自下而上”的控制流使得 ASM 极其轻量和高效,能够直接操作 JVM 指令集,因此在性能上具有天然优势。然而,这种低级别的抽象也带来了陡峭的 学习曲线,开发者必须对 Java Class 文件格式和 JVM 内部指令有深入的理解才能熟练使用。ASM 的包体积相对较小,这使其成为对性能有极致追求的框架(如 Spring、MyBatis、Kryo)的首选 。
相比之下,Javassist(Java Programming Assistant)则提供了一个更高层次的抽象,旨在简化 Java 字节码的操作。Javassist 的设计目标是让开发者可以用一种更接近 Java 源代码的方式来修改类,而无需深入了解底层的字节码细节 。它提供了两种主要的 API:源码级 API 和字节码级 API。源码级 API 允许开发者通过字符串形式直接插入 Java 源代码,Javassist 会负责在编译时将其转换为对应的字节码指令。这种直观的方式极大地提高了开发效率,降低了学习门槛 。Javassist 的字节码级 API 虽然也涉及底层操作,但相比 ASM 的访问者模式要更容易理解。不过,这种高层封装也带来了一定的性能开销,其字节码处理速度通常慢于 ASM。Javassist 的包体积也更大,因为它包含了自己的一套解析器和编译器。
下表总结了 ASM 与 Javassist 在关键维度上的对比:
在实际项目中,选择 ASM 还是 Javassist 取决于具体需求。如果项目对性能有严苛的要求,且团队有能力投入时间和精力去掌握底层字节码知识,那么 ASM 无疑是更好的选择。反之,如果项目更看重开发速度和代码的可维护性,或者需要进行的字节码修改相对简单,那么 Javassist 则是更为理想的选择。实践中,一些框架甚至会同时支持这两种技术,例如 Booster 框架就默认使用 ASM 以优化性能,同时也兼容 Javassist。此外,两者都可以与 Gradle 插件(如 Transform API)集成,用于在 Android 构建过程中进行字节码插桩,例如解决跨 dex 引用或进行性能监控。
1.4 AspectJ:强大的 AOP 框架与编译期织入实践
AspectJ 是由 Eclipse 基金会维护的一个成熟的、功能强大的 Java 语言扩展,专为实现面向切面编程(AOP)而设计。与依赖动态代理的 AOP 框架不同,AspectJ 的核心优势在于它能在编译期或类加载期将切面逻辑静态地织入到 Java 字节码中,从而避免了运行时的代理开销,实现了卓越的性能。这种在编译阶段修改 .class 文件的能力,使得 AspectJ 能够拦截包括private、final、static方法在内的几乎所有方法,并解决了 Spring AOP 因依赖代理而导致的内部方法调用失效问题。
AspectJ 的工作流程与其他编译期 Hook 工具类似,但它引入了自己的编译器——ajc(AspectJ Compiler)。开发者使用ajc来编译包含切面的 Java 源代码。这个过程大致如下:首先,ajc会像标准 javac 一样编译所有的 .java 文件;然后,它会扫描所有的.class文件,查找匹配切点表达式的连接点,并将预先定义好的通知(Advice)逻辑,以新增方法的形式插入到目标类中;最后,它会生成修改后的 .class 文件。通过这种方式,最终生成的类文件本身就已经包含了增强逻辑,运行时无需任何代理 。反编译一个被 AspectJ 增强的类,可以清晰地看到新增了类似aroundBody$advice的方法以及对proceed()的调用,这就是切面织入的直接证据 。
AspectJ 的织入机制非常灵活,支持多种织入时机:
-
编译时织入(Compile-time Weaving,CTW): 这是最常用的织入方式。开发者使用
ajc编译器代替javac,在源码编译的同时完成织入。在 Maven 项目中,可以通过aspectj-maven-plugin插件来配置 CTW 。 -
编译后织入(Post-compile Weaving,PCW): 如果无法修改源代码(例如,需要对第三方 jar 包进行增强),可以使用
ajc的 PCW 功能。它作用于已编译好的.class文件或.jar文件,对其进行织入 。 -
加载时织入(Load-time Weaving, LTW): 这种方式在类被 JVM 加载到内存时进行织入 。它需要借助 Java 的
java.lang.instrument包和-javaagentJVM 参数来启动一个 AspectJ 的类加载器代理(aspectjweaver.jar)。LTW 的优势在于它可以在不修改任何编译命令的情况下,对应用进行 AOP 增强,尤其适合对未被 Spring IoC 容器管理的第三方类进行增强 。织入规则通常通过位于META-INF/aop.xml的配置文件来指定 。
在 Android 开发中,为了在打包流程中无缝集成 AspectJ,通常会使用社区提供的 Gradle 插件,如gradle_plugin_android_aspectjx。该插件本质上是对 Android Gradle Plugin 的 Transform API 进行了封装,利用它在.class文件转为.dex文件之前,调用ajc编译器执行字节码插桩。网易新闻客户端就曾采用此方案,在 Java 源码编译完成后、生成 Dex 文件前进行字节码插桩,实现了方法级别的热修复 。尽管 AspectJ 功能强大,但它并非没有缺点。其学习曲线 依然较陡峭,切点表达式虽然强大但也容易变得复杂。此外,在某些新版本的 Android Gradle 插件或 D8/Dexer 中可能存在兼容性问题,需要特别注意。
1.5 注解处理器(APT/KAPT/KSP):编译期代码生成的艺术
注解处理器(Annotation Processing Tool,APT)是 Java 和 Kotlin 编译器内置的一项强大功能,它允许开发者在编译期间扫描和处理源代码中的注解,并在此基础上生成新的 Java 或 Kotlin 源文件 。APT 的核心思想是“元编程”(meta programming):通过定义元数据(注解),驱动代码生成引擎(Processor)来自动化地生成样板代码,从而减少手动编写重复性工作的负担。与字节码操作技术直接修改 .class 文件不同,APT 工作在编译链的更早阶段,它处理的是 .java/.kt 文件,并输出新的 .java/.kt 文件,这些新文件随后会被正常的 javac/kotlinc 编译器再次编译成 .class 文件 。因此,APT 本质上是一种“编译期代码生成”技术。
APT 的典型工作流程如下:
-
在项目中定义自定义注解。
-
创建一个实现了
javax.annotation.processing.Processor接口的类。-
在这个 Processor 的
process方法中,开发者可以利用 APT 提供的 API(如Elements、Types、Filer)来遍历源代码中的注解,收集信息,并使用Filer创建新的.java文件 。 -
为了使编译器能找到我们的 Processor,还需要在指定位置注册其全限定名(
META-INF下)。Google 的auto-service库可以自动完成这个繁琐的步骤 。
-
-
在 Android Gradle 项目中,我们只需在
dependencies中声明注解处理器(使用annotationProcessor配置),Gradle 插件会自动将它集成到编译链中。APT 的应用场景极为广泛,许多知名的开源框架都基于此技术构建。 -
同时,为了简化代码生成过程,开发者通常会使用
JavaPoet或KotlinPoet这样的库,它们以面向对象的方式构建 Java/Kotlin 代码,避免了手动拼接字符串的低效和易错。
以下是一些常见的注解处理器相关代码:
ButterKnife
EventBus
基于 KSP 的深拷贝实现
随着 Kotlin 语言的流行,针对 Kotlin 的注解处理工具应运而生。最初,Android 生态沿用了 Java 的 APT 机制,对应的 Gradle 配置是kapt。然而,Kotlin 的很多特性(如内联类、密封类)在 Java 的 APT 环境中难以被完全解析,导致 KAPT 存在诸多限制和性能瓶颈,KAPT耗时的部分可见: 老项目的救赎:小猿口算 Android 项目的优化实践 总结。
KAPT 需要先将 Kotlin 代码编译成 JVM 字节码,然后再生成一份临时的 Java Stub 文件,以便 Java 的 APT Processor 能够处理,这个过程既耗时又可能导致信息丢失。为了解决这些问题,Google 推出了全新的 Kotlin Symbol Processing (KSP)。KSP 直接在 Kotlin 编译器层面工作,绕过了生成 Java Stub 的中间环节,它直接解析 Kotlin 的抽象语法树(AST)并提供符号(Symbol)级别的 API 供处理器使用 。这使得 KSP 的编译速度比 KAPT 快了数倍,因为它避免了大量的文件读写和编译任务。例如,一个 KAPT 任务耗时 1 分 13 秒的项目,在迁移到 KSP 后,任务耗时降至 7 秒 。KSP 目前已成为 Kotlin 项目中注解处理的首选方案,它在性能和功能上都超越了 KAPT。然而,KSP 也牺牲了一部分与 Java 生态的互通性,例如它不直接支持处理 Java 注解。
1.6 技术选型与差异综合对比
在众多编译期 Hook 技术中进行选择是一项复杂的决策,需要综合考量项目类型、性能要求、开发效率、织入时机和技术栈等多个因素。每种技术都有其独特的优势和适用场景,不存在绝对的最优解。本节将对 ASM、Javassist、AspectJ、APT/KAPT/KSP 进行多维度的综合对比,以帮助做出明智的技术选型。
从这张表格中我们可以提炼出几个关键的选型原则。
-
性能优先:如果项目对运行时性能有极致要求,例如在游戏引擎、高频交易系统或底层框架中,ASM 无疑是最佳选择,因为它提供了最小的运行时开销。
-
功能完整性:当需要实现完整的 AOP 解决方案,包括方法、字段、构造器、静态初始化块等全方位的切入,并且希望获得 AspectJ 那样强大的功能和性能时,即使面临较高的学习成本,AspectJ 也是值得考虑的。
-
开发效率至上:对于大多数应用层开发而言,特别是需要快速迭代和开发的项目,字节码操作框架的复杂性可能会成为生产力的瓶颈。此时,如果需求主要是生成辅助类或简单的代码增强,那么 APT/KSP 是更合适的选择 。它将 AOP 的思想应用于代码生成,让开发者可以用更自然的方式思考问题,而不需要深入到底层的字节码世界。KSP 凭借其卓越的性能,已经成为现代 Kotlin 项目的首选。有关 KSP 如何使用,可以参考此篇文章: 使用 KSP 高效处理 Kotlin 注解。
-
项目环境与生态整合:技术选型不应脱离具体的项目背景。在 Android 开发中,由于 Gradle 插件体系的存在,集成 AspectJ 或 Javassist 都非常方便 。但在 Kotlin 项目中,KSP 已是事实标准,因为它解决了 KAPT 的根本性缺陷 。同样,Spring 框架虽然自身实现了 AOP,但在需要更强大功能时,也会集成 AspectJ 作为其 AOP 能力的补充。因此,技术选型应充分考虑现有技术栈的整合难度和社区生态的支持情况。
总而言之,不存在万能的编译期 Hook 技术。开发者需要根据具体的需求,在性能、功能、易用性和项目环境之间做出权衡,选择最适合当前场景的组合。在某些复杂的系统中,甚至可能会混合使用多种技术,例如使用 AspectJ 处理核心的、复杂的横切关注点,同时使用 APT/KSP 来生成大量、简单的辅助代码,从而达到最佳的效果。
2. Transform 介绍
对于安卓开发而言最常用的是ASM,
2.1 Transform技术概述
2.1.1 什么是Transform
Transform是Android Gradle插件从1.5.0版本开始引入的核心机制,它允许第三方插件在.class文件转换为.dex文件之前对字节码进行操作。本质上是一个自动注入到Android构建流程中的Gradle Task,按照特定顺序串联在构建链条中。
Transform的工作原理可以形象地比喻为汽车装配流水线上的特殊工位:在Java源代码编译成.class文件后,打包成.dex文件前,Transform能够拦截这些"零件"(字节码),进行修改后再继续后续的组装流程。
**典型场景:**在所有类的开头加入日志打印
2.1.2 Transform的核心价值
Transform技术为Android开发者提供了以下关键能力:
- 字节码级修改:直接操作编译后的**.class文件**
// 所有 activity onCreate 增加一行日志
{
super.onCreate()
// hook
log.d()
}
- 全局性干预:能够处理项目中所有模块的class文件,包括第三方库
// Transform 输入文件所属的范围
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return EnumSet.of(
QualifiedContent.Scope.PROJECT, // 主工程代码
QualifiedContent.Scope.SUB_PROJECTS, // 子模块代码
QualifiedContent.Scope.EXTERNAL_LIBRARIES // 第三方依赖库
);
}
-
构建时处理:修改行为发生在编译期,不影响运行时性能
-
语言无关性:同时支持Java和Kotlin代码的转换
2.1.3 典型应用场景
Transform 技术在Android开发中有广泛的应用场景:
- 性能监控:插入方法耗时统计、内存泄漏检测等代码
public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
long startTime = System.currentTimeMillis(); // 插入开始时间戳
super.onCreate(savedInstanceState);
this.setContentView(R.layout.activity_main);
this.initView();
this.loadData();
long endTime = System.currentTimeMillis(); // 插入结束时间戳
long duration = endTime - startTime;
Log.d("MethodTrace", "MainActivity.onCreate cost: " + duration + "ms"); // 插入日志输出
}
}
- 代码注入:自动生成日志TAG、添加try-catch块等
public class MainActivity extends AppCompatActivity {
// 手动定义的TAG被保留(若存在),或...
private static final String TAG = "MainActivity";
// ...字节码工具会自动生成一个TAG字段(若不存在),字段名可配置,例如:
// private static final String _AUTO_TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 字节码工具会将此处的TAG引用替换为自动生成的字段名
Log.d(TAG, "onCreate: Activity started");
// 或者工具可能直接修改日志方法的参数,将其第一个参数替换为 `MainActivity.class.getSimpleName()`
// Log.d(MainActivity.class.getSimpleName(), "onCreate: Activity started");
}
}
-
API Hook:修改第三方库行为而不直接改动其源码
-
AOP编程:实现面向切面的编程范式
-
**核心思想:**将那些遍布在应用程序多个模块中的共性功能(称为"横切关注点")从核心业务逻辑中剥离出来,集中管理和维护
-
AOP 与 OOP 的关系
-
OOP (面向对象编程):关注的是将业务逻辑按功能模块进行纵向的抽象和封装,例如将用户、订单等概念抽象成类和对象。
-
AOP (面向切面编程):关注的则是那些横向散布在不同模块、不同类中的通用功能,例如日志记录、权限校验等。它允许开发者将这些功能模块化,然后"织入"到需要的业务点上
通过结合AOP和OOP,开发者可以更好地实现关注点分离,让系统更加清晰、模块化,并减少代码冗余
-
-
热修复支持:为热修复框架提供底层支持
美团的 Robust 是利用 Transform 支持热修复的典型代表。它的核心思路是:
在编译阶段,通过 Transform 遍历所有方法
对每个方法,在其开始处插入一段预定义的逻辑代码。这段代码会检查一个预先准备好的“补丁映射表”,判断当前方法是否有对应的修复版本
如果存在补丁方法,则执行流就会被引导至补丁方法;否则,继续执行原始方法逻辑
2.2 Transform的工作原理与核心API
2.2.1 Transform在构建流程中的位置
理解Transform需要先了解Android完整的构建流程。Transform 阶段位于编译阶段之后,打包阶段之前:
预构建阶段 → 代码生成阶段 → 资源处理阶段 → 编译阶段 → Transform阶段 → 打包阶段
| 阶段 | 动作 | 输入 | 输出 | |
|---|---|---|---|---|
| 生成.class文件 |
Java编译器对工程本身的java代码进行编译 |
|
.class文件 | |
| 打包成.dex文件 | dex工具对class文件和依赖的三方库文件,生成可执行的.dex文件,可能有一个或多个,涵盖所有的class信息(项目自身的class和依赖的class) |
|
.dex文件 | |
| 生成未经签名的.apk文件 | apkbuilder工具将.dex文件和编译后的资源文件生成未经签名对齐的apk文件 |
|
未经签名的.apk文件 | |
| 生成最终的apk文件 | 分别由Jarsigner和zipalign对apk文件进行签名和对齐,生成最终的apk文件 | 最终的apk文件 | 补充:jarsigner 用于给 APK 添加签名以确保安全性(v1),而 zipalign 用于优化 APK 文件结构,提高运行效率。在现代 Android 构建中,推荐使用 apksigner 替代 jarsigner,并始终使用 zipalign 对齐 APK。 |
2.2.2 Transform的链式执行机制
Transform采用链式结构,每个Transform都是一个Gradle Task,Android编译器通过TaskManager将它们串联起来。第一个Transform接收javac编译的结果,以及jar包依赖和resource资源,这些编译中间产物在Transform链上流动。
2.2.3 Transform的核心API
实现一个自定义Transform需要继承Transform类并实现关键方法:
public class BytecodeTransform extends Transform {
//指明 Transform 的名字
@Override
public String getName() {
return "customTransform";
}
//指明 Transform 处理的输入类型
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return Collections.singleton(QualifiedContent.DefaultContentType.CLASSES);
}
// Transform 输入文件所属的范围
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return EnumSet.of(
QualifiedContent.Scope.PROJECT, // 主工程代码
QualifiedContent.Scope.SUB_PROJECTS, // 子模块代码
QualifiedContent.Scope.EXTERNAL_LIBRARIES // 第三方依赖库
);
}
// 是否支持增量编译
@Override
public boolean isIncremental() {
return true;
}
@Override
public void transform(TransformInvocation invocation) {
// 核心处理逻辑
for (TransformInput input : invocation.getInputs()) {
// 处理JAR输入
for (JarInput jarInput : input.getJarInputs()) {
System.out.println("处理JAR: " + jarInput.getName());
}
// 处理目录输入
for (DirectoryInput dirInput : input.getDirectoryInputs()) {
System.out.println("处理目录: " + dirInput.getName());
}
}
}
}
2.2.3.1 基础配置方法
- getName():指明 Transform 的名字,对应该
Transform所代表的Task名称,例如当返回值为InjectTransform时,编译后可以看到名为transformClassesWithInjectTransformForxxx的task
@Override
public String getName() {
return "InjectTransform";
}
- getInputTypes():指定处理的输入类型,旧版使用的是
TransformManager中定义的类型,AGP=7.4.2,TransformManager中的类型已经被移除,可使用QualifiedContent进行替换
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
//Class文件
QualifiedContent.DefaultContentType.CLASSES
//RESOURCES文件
QualifiedContent.DefaultContentType.RESOURCES
- getScopes():指定作用范围,常用
TransformManager.SCOPE_FULL_PROJECT(处理整个项目的代码)
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT;
}
enum Scope implements ScopeType {
/** 仅包含项目(模块)内容 */
PROJECT(0x01),
/** 仅包含子项目(其他模块) */
SUB_PROJECTS(0x04),
/** 仅包含外部库 */
EXTERNAL_LIBRARIES(0x10),
/** 当前构建变体所测试的代码,包括其依赖项 */
TESTED_CODE(0x20),
/** 仅为 “提供”(provided)类型的本地或远程依赖项 */
PROVIDED_ONLY(0x40),
/*** 仅包含该项目的本地依赖项(本地的 JAR 包)
@已过时 本地依赖项现在按 “外部库” 进行处理*/
@Deprecated
PROJECT_LOCAL_DEPS(0x02),
/**仅包含子项目的本地依赖项(本地的 JAR 包)
@已过时 现在本地依赖项将作为 (外部库)来处理*/
@Deprecated
SUB_PROJECTS_LOCAL_DEPS(0x08);
private final int value;
Scope(int value) {
this.value = value;
}
@Override
public int getValue() {
return value;
}
}
- isIncremental():是否支持增量编译,应尽量返回true以提高构建速度
@Override
public boolean isIncremental() {
return true;
}
2.2.3.2 核心处理方法
transform(TransformInvocation invocation):核心处理方法,在这里实现字节码的修改逻辑。关键对象包括:
@Override
public void transform(TransformInvocation invocation) {
// 遍历所有输入
for (TransformInput input : invocation.getInputs()) {
// 处理JAR输入
for (JarInput jarInput : input.getJarInputs()) {
File dest = invocation.getOutputProvider().getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
// 处理并复制JAR文件
processJar(jarInput.getFile(), dest);
}
// 处理目录输入
for (DirectoryInput dirInput : input.getDirectoryInputs()) {
File dest = invocation.getOutputProvider().getContentLocation(
dirInput.getName(),
dirInput.getContentTypes(),
dirInput.getScopes(),
Format.DIRECTORY);
// 处理并复制目录
processDir(dirInput.getFile(), dest);
}
}
}
- TransformInvocation
@Deprecated
public interface TransformInvocation {
// 上下文
@NonNull
Context getContext();
// transform 的输入
@NonNull
Collection<TransformInput> getInputs();
// 返回不被这个 transformation 消费的 input
@NonNull Collection<TransformInput> getReferencedInputs();
/**
返回自上次以来二级文件的变更列表。只有本转换能够以增量方式处理的二级文件才会包含在这个变更集中。
@return 影响到 {@link SecondaryInput}(二级输入)的变更列表
*/
@NonNull Collection<SecondaryInput> getSecondaryInputs();
// 返回允许创建内容的 output provider
@Nullable
TransformOutputProvider getOutputProvider();
/**
指示转换执行是否为增量式的。
@return 如果是增量式调用则返回 true,否则返回 false。
*/
boolean isIncremental();
}
-
TransformInput:包含两类输入:
-
directoryInputs:开发者编写的源代码编译后的class文件目录 -
jarInputs:依赖的JAR文件集合
@Deprecated public interface TransformInput { // 表示 Jar 包 @NonNull Collection<JarInput> getJarInputs(); //返回一个目录输入的集合 @NonNull Collection<DirectoryInput> getDirectoryInputs(); } -
-
TransformOutputProvider:管理输出路径,通过
getContentLocation获取输出位置
@Deprecated
public interface TransformOutputProvider {
/**
删除所有内容。这在非增量模式下运行时很有用。
@throws IOException 如果删除输出操作失败,则抛出此异常。
*/
void deleteAll() throws IOException;
// 根据 name、ContentType、QualifiedContent.Scope 返回对应的文件(jar / directory)
@NonNull
File getContentLocation(
@NonNull String name,
@NonNull Set<QualifiedContent.ContentType> types,
@NonNull Set<? super QualifiedContent.Scope> scopes,
@NonNull Format format);
}
2.3 Transform的增量与并发处理
2.3.1 增量编译实现
Transform处理大量class文件可能显著增加构建时间,实现增量编译是必要的优化手段。增量编译的核心是根据文件状态(ADDED、CHANGED、REMOVED、NOTCHANGED)决定处理方式。
增量编译实现示例:
@Override
public void transform(TransformInvocation invocation) {
boolean isIncremental = invocation.isIncremental();
if (!isIncremental) {
// 非增量模式:全量清理输出目录
invocation.getOutputProvider().deleteAll();
}
for (TransformInput input : invocation.getInputs()) {
for (DirectoryInput dirInput : input.getDirectoryInputs()) {
File dest = invocation.getOutputProvider().getContentLocation(...);
if (isIncremental) {
// 增量模式:只处理变更文件
Map<File, Status> changedFiles = dirInput.getChangedFiles();
for (Map.Entry<File, Status> entry : changedFiles.entrySet()) {
File inputFile = entry.getKey();
Status status = entry.getValue();
switch (status) {
case ADDED:
case CHANGED:
processFile(inputFile, getOutputFile(dest, inputFile));
break;
case REMOVED:
deleteOutputFile(dest, inputFile);
break;
}
}
} else {
// 全量处理
processDir(dirInput.getFile(), dest);
}
}
}
}
2.3.2 并发处理优化
Transform处理每个jar/class文件是独立的,可以并发执行以充分利用多核CPU:
private WaitableExecutor waitableExecutor = WaitableExecutor.useGlobalSharedThreadPool();
// 异步并发处理jar
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveJar(srcJar, destJar);
return null;
});
// 异步并发处理class
waitableExecutor.execute(() -> {
bytecodeWeaver.weaveSingleClassToFile(file, outputFile, inputDirPath);
return null;
});
// 等待所有任务结束
waitableExecutor.waitForTasksWithQuickFail(true);
2.4 Transform与字节码操作框架的结合
2.4.1 如何查看字节码文件
| 方法类型 | 工具/插件名称 | 关键操作步骤 |
|---|---|---|
| 插件方式 | jclasslib Bytecode Viewer | 通过 Plugins 市场安装,通过 View > Show Bytecode with jclasslib 查看 |
| 插件方式 | ASM Bytecode Viewer | 下载插件包后从磁盘安装,重启生效 |
| 终端命令 | javap 命令行工具 | 在 Terminal 中执行 javap -c -v 完整.类名.class 或 javap -c -v -p 完整.类名.class(包含私有成员) |
| 内置功能 | 针对 Kotlin 源文件 | 对 Kotlin 文件使用 Tools > Kotlin > Show Kotlin Bytecode,可点击 Decompile 反编译为近似 Java 代码便于理解 |
2.4.2 ASM框架简介
Transform通常需要配合字节码操作框架实现具体功能,ASM是最常用的选择,它具有以下优势:
-
高性能:直接操作字节码,比反射或动态代理更高效
-
细粒度控制:可以精确控制对类、方法、字段等的修改
-
广泛支持:支持最新的Java版本特性
-
生态完善:被众多知名项目(如Groovy、Kotlin编译器)使用
2.4.3 ASM核心API
ASM使用访问者模式操作字节码,核心类包括:
-
ClassReader:解析.class文件
-
ClassWriter:生成修改后的字节码
-
ClassVisitor:访问类结构的各个部分
-
MethodVisitor:访问方法的各个部分
基本使用流程:
void processClass(File input, File output) {
// 1. 创建ClassReader读取类文件
ClassReader cr = new ClassReader(Files.readAllBytes(input.toPath()));
// 2. 创建ClassWriter
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS);
// 3. 创建自定义ClassVisitor
ClassVisitor cv = new MyClassVisitor(Opcodes.ASM9, cw);
// 4. 应用修改
cr.accept(cv, 0);
// 5. 输出修改后的字节码
Files.write(output.toPath(), cw.toByteArray());
}
ClassVisitor
- **visit():**用于开始访问一个类,并设置类的基础信息
//version: Java 版本 (e.g., Opcodes.V1_8)
//access: 访问修饰符 (e.g., Opcodes.ACC_PUBLIC)
//name: 类的内部名称(全限定名,点换成斜杠),如 "com/example/MyClass"
//signature: 泛型签名,非泛型类可为 null
//superName: 父类的内部名称,如 "java/lang/Object"
//interfaces: 实现的接口数组,如 new String[]{"java/io/Serializable"}
public void visit(int version,
int access,
String name,
String signature,
String superName,
String[] interfaces)
//生成新的类
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "com/example/HelloWorld", null, "java/lang/Object", null);
//修改现有类
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
System.out.println("访问类: " + name + ", 父类: " + superName); // 打印类信息
}
- visitField() :用于声明一个字段,并返回一个
FieldVisitor用于进一步配置该字段(如注解、值等),最后需要调用visitEnd()。
//access: 字段修饰符,如 Opcodes.ACC_PRIVATE + Opcodes.ACC_STATIC
//name: 字段名
//descriptor: 字段类型描述符 (e.g., I 表示 int, Ljava/lang/String; 表示 String)
//signature: 泛型签名,非泛型字段可为 null
//value: 初始值(仅对静态常量字段有效,否则为 null)
public FieldVisitor visitField(int access,
String name,
String descriptor,
String signature,
Object value)
//添加一个 private String message 字段
FieldVisitor fv = cw.visitField(Opcodes.ACC_PRIVATE, "message", "Ljava/lang/String;", null, null);
fv.visitEnd(); // 结束字段的访问
//修改现有字段
@Override
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
// 示例:过滤掉名为"password"的敏感字段
if ("password".equals(name)) {
return null; // 删除该字段
}
return super.visitField(access, name, descriptor, signature, value);
}
MethodVisitor
- visitMethod() :用于声明一个方法,并返回一个
MethodVisitor。MethodVisitor是用来生成方法体的关键,你需要使用它的visitXxxInsn()等方法来写入字节码指令
//access: 方法修饰符,如 Opcodes.ACC_PUBLIC
//name: 方法名,如 "<init>" (构造方法) 或 "hello"
//descriptor: 方法描述符,格式为 (参数类型描述符)返回值类型描述符
//signature: 泛型签名,非泛型方法可为 null
//exceptions: 声明的异常数组,可为 null
public MethodVisitor visitMethod(int access,
String name,
String descriptor,
String signature,
String[] exceptions)
//生成新方法public void hello()
MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "hello", "()V", null, null);
mv.visitCode(); // 开始代码生成
mv.visitFieldInsn(Opcodes.GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello, ASM!"); // 加载字符串常量
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
mv.visitInsn(Opcodes.RETURN); // 返回指令
mv.visitMaxs(0, 0); // 委托ClassWriter计算栈和局部变量表大小 (COMPUTE_MAXS)
mv.visitEnd(); // 结束方法的访问
//修改现有方法
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("敏感操作")) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitCode() {
super.visitCode();
// 插入日志记录代码
visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
visitLdcInsn("方法: " + name + " 被调用了");
visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
}
};
}
return mv;
}
以下是 MethodVisitor的一些常用 API 介绍:
| 指令类型 | API | 参数 | 作用 |
|---|---|---|---|
| 方法开始与结束 | visitCode() |
此方法标志着方法字节码访问或生成的开始,在开始访问方法的指令前调用一次 | |
visitMaxs(int maxStack, int maxLocals) |
设置操作数栈的最大深度和局部变量表的最大索引值(注意不是个数),它标志着方法体的结束。 在使用 ClassWriter 并设置 COMPUTE_MAXS 或 COMPUTE_FRAMES 参数时,ASM 可以自动计算这些值,此时传入的参数会被忽略,但仍需调用此方法 |
||
visitEnd() |
方法访问的结束 | ||
| 访问局部变量 | visitVarInsn(int opcode, int var) |
|
访问加载或存储局部变量的指令 |
| 访问方法 | visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) |
|
|
| 访问无操作数 | visitInsn(int opcode) |
|
|
| 访问类型 | visitTypeInsn(int opcode, String type) |
|
|
| 访问字段 |
visitFieldInsn(int opcode, String owner, String name, String descriptor) |
|
|
| 访问跳转指令与标签 | visitJumpInsn(int opcode, Label label) |
|
|
visitLabel(Label label) |
用于访问一个标签,标记跳转指令的目标位置 | ||
| 访问常量指令 | visitLdcInsn(Object value) |
用于访问 LDC 指令,将常量值(如 int, float, long, double, String, Class 或 MethodType 等)从运行时常量池压入操作数栈 | |
| 访问 try-catch 块 | visitTryCatchBlock(Label start, Label end, Label handler, String type) |
|
用于访问一个异常处理器,即 try-catch 块 |
- **visitEnd() :**访问结束
@Override
public void visitEnd() {
super.visitEnd();
System.out.println("类访问结束"); // 可以在这里添加收尾工作
// 例如,在这里可以确定所有元素已访问完毕,安全地添加新字段或方法
}
- **visitAnnotation() **:访问类上的注解
public AnnotationVisitor visitAnnotation(
String descriptor, // 注解类型的描述符,如"Lorg/junit/Test;"
boolean visible // 注解在运行时是否可见
)
@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
System.out.println("发现注解: " + descriptor);
return super.visitAnnotation(descriptor, visible);
}
2.4.4 典型ASM操作案例
案例1:方法耗时统计
public class TimeCostVisitor extends ClassVisitor {
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitCode() {
// 插入:long startTime = System.nanoTime();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(LSTORE, 1); // 存储结果到局部变量槽位1
super.visitCode(); // 继续执行原方法逻辑
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
// 插入结束计时和日志代码
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
mv.visitVarInsn(LLOAD, 1); // 加载startTime
mv.visitInsn(LSUB); // 计算耗时:endTime - startTime
mv.visitLdcInsn(name); // 压入方法名
// 调用日志方法:TimeRecorder.record(duration, methodName)
mv.visitMethodInsn(INVOKESTATIC, "com/example/TimeRecorder", "record",
"(JLjava/lang/String;)V", false);
}
super.visitInsn(opcode); // 执行原返回指令
}
};
}
}
案例2:全局异常捕获
public class ExceptionHandlerVisitor extends ClassVisitor {
private String className;
public ExceptionHandlerVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM9, cv);
this.className = className;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if ("onCreate".equals(name)) {
return new TryCatchWrapper(mv, className, name);
}
return mv;
}
}
class TryCatchWrapper extends MethodVisitor {
// 在方法开头添加try块
public void visitCode() {
mv.visitTryCatchBlock(...);
super.visitCode();
}
// 在异常处理块插入上报代码
public void visitMaxs(int maxStack, int maxLocals) {
Label end = new Label();
mv.visitLabel(end);
// 插入异常上报代码
mv.visitMethodInsn(INVOKESTATIC, "com/error/CrashReporter", "report",
"(Ljava/lang/Throwable;)V", false);
mv.visitInsn(ATHROW);
super.visitMaxs(maxStack, maxLocals);
}
}
案例3:自动生成日志TAG
需求:自动为每个类添加TAG字段,值为类名,简化日志打印。
实现步骤:
-
创建自定义Transform识别所有类
-
使用ASM在类中添加静态字段
TAG -
为字段赋值为类名
关键代码:
public class LogTagVisitor extends ClassVisitor {
private String className;
public LogTagVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM9, cv);
this.className = className;
}
@Override
public void visit(int version, int access, String name,
String signature, String superName, String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
// 添加TAG字段
FieldVisitor fv = cv.visitField(
ACC_PUBLIC | ACC_STATIC | ACC_FINAL,
"TAG",
"Ljava/lang/String;",
null,
className.replace("/", "."));
if (fv != null) {
fv.visitEnd();
}
}
}
案例4:OkHttp全局拦截器
需求:为项目中所有OkHttpClient实例添加统一拦截器,包括第三方库中的OkHttpClient。
实现思路:
-
定位OkHttpClient的构造方法
-
在构造方法后插入添加拦截器的代码
-
处理所有依赖库中的OkHttpClient类
关键代码:
public class OkHttpVisitor extends ClassVisitor {
public OkHttpVisitor(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if ("<init>".equals(name) && "()V".equals(desc)) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitInsn(int opcode) {
if (opcode == RETURN) {
// 在构造方法返回前插入代码
mv.visitVarInsn(ALOAD, 0); // this
mv.visitTypeInsn(NEW, "com/example/MyInterceptor");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL, "com/example/MyInterceptor",
"<init>", "()V", false);
mv.visitMethodInsn(INVOKEVIRTUAL, "okhttp3/OkHttpClient", "addInterceptor",
"(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient$Builder;", false);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
}
案例5:UI线程耗时监控
需求:监控所有UI线程方法的执行耗时,统计卡顿方法。
实现方案:
-
识别Activity、Fragment等UI相关类
-
在关键方法(如onCreate、onResume)前后插入耗时统计代码
-
聚合统计结果并输出报告
关键代码:
public class UITimingVisitor extends ClassVisitor {
private String className;
private boolean isUIClass;
public UITimingVisitor(ClassVisitor cv, String className) {
super(Opcodes.ASM9, cv);
this.className = className;
this.isUIClass = isUIClass(className);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
if (isUIClass && isLifecycleMethod(name)) {
return new MethodVisitor(Opcodes.ASM9, mv) {
@Override
public void visitCode() {
mv.visitLdcInsn(className + "." + name);
mv.visitMethodInsn(INVOKESTATIC, "com/example/UITimeMonitor",
"start", "(Ljava/lang/String;)V", false);
super.visitCode();
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitLdcInsn(className + "." + name);
mv.visitMethodInsn(INVOKESTATIC, "com/example/UITimeMonitor",
"end", "(Ljava/lang/String;)V", false);
}
super.visitInsn(opcode);
}
};
}
return mv;
}
private boolean isUIClass(String className) {
return className.startsWith("androidx/") ||
className.startsWith("android/app/") ||
className.endsWith("Activity") ||
className.endsWith("Fragment");
}
private boolean isLifecycleMethod(String name) {
return name.startsWith("on");
}
}
3. AsmClassVisitorFactory
AsmClassVisitorFactory 是 Android Gradle Plugin(AGP)7.0 及以上版本中用于替代传统 Transform API 的新接口,专为字节码插桩而设计。它基于 Gradle 的 Transform Action 实现,通过简化流程、提升性能,成为 AGP 8.0 移除 Transform 后的官方推荐方案。
3.1 核心特性与优势
-
性能优化
-
减少 I/O 操作:传统 Transform 每个任务独立读写字节码文件,导致多次 I/O。
AsmClassVisitorFactory通过 ClassVisitor 链表机制,将多个插桩操作串联为一次 I/O 流程,编译速度提升约 18%。 -
增量编译支持:自动处理增量逻辑,无需开发者手动实现,显著减少全量/增量构建耗时。
-
-
简化开发流程
-
专注字节码操作:开发者只需实现
ClassVisitor逻辑,无需关注输入/输出路径、缓存管理等底层细节。 -
废弃 Transform 的替代:AGP 8.0 已移除 Transform API,
AsmClassVisitorFactory是兼容未来版本的唯一标准方案。
-
-
作用域控制 通过
InstrumentationScope参数(如ALL、PROJECT)控制插桩范围,支持仅处理当前模块或依赖库的类。
3.2 核心 API 与工作原理
-
关键接口方法
-
isInstrumentable(classData: ClassData): Boolean过滤需插桩的类,通过类名、注解等条件减少扫描量(例如限定包名),提升效率 -
createClassVisitor()返回自定义ClassVisitor,接收nextClassVisitor参数以构建处理链,确保字节码修改的连贯性
-
-
类信息辅助接口
-
ClassData:提供类基本信息(类名、注解、父类等),用于条件判断 -
ClassContext:访问类的上下文信息(如 API 版本),支持更复杂的插桩逻辑
-
abstract class MyAsmClassVisitorFactory :
AsmClassVisitorFactory<MyAsmClassVisitorFactory.Parameters> {
interface Parameters : AsmClassVisitorFactory.Parameters {
// 可以在这里定义传递给 ClassVisitor 的参数
val someConfig: String
}
override fun createClassVisitor(classContext: ClassContext, environment: ClassVisitorEnvironment): ClassVisitor {
return object : ClassVisitor(ASM9) {
override fun visit(...) {
// 在这里可以修改类的结构
super.visit(...)
}
}
}
override fun isInstrumentable(classData: ClassData): Boolean {
// 返回 true 表示这个类需要被处理
return true
}
}
- 注册方式
通过
AndroidComponentsExtension注册到变种(Variant),替代旧版AppExtension:
androidComponents.onVariants { variant ->
variant.transformClassesWith(MyVisitorFactory::class.java, InstrumentationScope.ALL) {}
variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
}
3.3 实战示例:方法耗时统计
以下代码演示如何在方法入口/出口插入计时逻辑:
// 1. 实现 AsmClassVisitorFactory
abstract class TimeCostTransform : AsmClassVisitorFactory<InstrumentationParameters.None> {
override fun createClassVisitor(classContext: ClassContext, nextVisitor: ClassVisitor): ClassVisitor {
return TimeCostClassVisitor(nextVisitor)
}
override fun isInstrumentable(classData: ClassData): Boolean {
return classData.className.startsWith("com.example") // 仅处理指定包
}
}
// 2. 自定义 ClassVisitor
class TimeCostClassVisitor(nextVisitor: ClassVisitor) : ClassVisitor(Opcodes.ASM5, nextVisitor) {
override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?, exceptions: Array<out String>?): MethodVisitor {
val mv = super.visitMethod(access, name, desc, signature, exceptions)
return object : AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {
override fun onMethodEnter() {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
// 保存开始时间到局部变量
}
override fun onMethodExit(opcode: Int) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)
// 计算耗时并输出日志
}
}
}
}