☰
Byte Buddy Android:在 Android 运行时生成并加载动态类的完整实战指南
2026/10/15 17:53:55 网站建设 项目流程
  • 开发工具
  • 后端

【免费下载链接】byte-buddy

Runtime code generation for the Java virtual machine.

项目地址:https://gitcode.com/gh_mirrors/by/byte-buddy
点击查看免费下载

导读

Byte Buddy 是 JVM 上的运行时字节码生成库,而Byte Buddy Android则把这一能力带到了 Android 运行时:它允许在 Android 应用进程内生成类,并借助专用的AndroidClassLoadingStrategy将这些类加载进当前进程。本文以 byte-buddy-android/README.md 为核心,结合 AndroidClassLoadingStrategy.java 的源码实现与仓库内的测试、示例工程,完整讲解其用法、两种加载策略的底层原理、安全约束与适用前提。读完本文,你将掌握在 Android 应用中用 Byte Buddy 完成「生成子类 → dex 编译 → 加载进当前 VM」的完整链路,并能在Wrapping与Injecting两种策略之间做出正确选择。

一、Byte Buddy Android 解决什么问题

标准 JVM 环境下,Byte Buddy 通过 ClassLoadingStrategy 将动态类型加载进类加载器,例如ClassLoadingStrategy.Default.WRAPPER、INJECTION等默认策略(见 byte-buddy-dep 中的 ClassLoadingStrategy.java 第 63-123 行)。

但 Android 的 Dalvik/ART 虚拟机并不直接执行 JVM 字节码(.class),而是执行经过 dex 编译的.dex文件,因此 Byte Buddy 生成的 JVM class 文件不能直接加载。Byte Buddy Android正是在这一环节架起桥梁:

  1. 把 Byte Buddy 生成的 JVM class 文件交由 Android SDK 的dex 编译器转换为 dex 格式;
  2. 将 dex 打包进 jar 文件;
  3. 通过DexClassLoader或 dex 注入机制,把类加载进当前 Android 应用进程。

模块描述在 byte-buddy-android/pom.xml 中定义为 "Byte Buddy Android allows for limited support of code generation on an Android runtime",即它是「受限的」代码生成支持——原因在于 Android 环境的特殊限制(详见下文第六节)。

二、快速上手:最小的可运行示例

README 给出了一个完整的最小示例:生成一个Object的子类,把toString()拦截为固定返回值,然后加载并验证。

ClassLoadingStrategy strategy = new AndroidClassLoadingStrategy.Wrapping(context.getDir( "generated", Context.MODE_PRIVATE)); Class<?> dynamicType = new ByteBuddy() .subclass(Object.class) .method(ElementMatchers.named("toString")) .intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader(), strategy) .getLoaded(); assertThat(dynamicType.newInstance().toString(), is("Hello World!"));

关键点拆解:

  • context.getDir("generated", Context.MODE_PRIVATE):在应用私有目录下创建名为generated的子目录,作为加载策略的工作目录。MODE_PRIVATE保证该目录仅当前应用可访问,这是安全模型的硬性要求(详见第四节)。
  • new ByteBuddy().subclass(Object.class)...make():标准的 Byte Buddy 动态类型构建流程,生成的是 JVM class 字节码;
  • .load(getClass().getClassLoader(), strategy):将生成的类型交给 Android 专用的加载策略,而不是ClassLoadingStrategy.Default;
  • 验证结果:dynamicType.newInstance().toString()应返回"Hello World!"。

单元测试印证

仓库中的 AndroidClassLoadingStrategyTest.java 印证了这一调用契约:

  • testProcessing测试验证了load方法的完整流水线:dexProcessor.create()创建转换会话 → 为每个类型调用conversion.register(name, byte[])→ 最终conversion.drainTo(OutputStream)把 dex 内容写入输出流(第 71-93 行);
  • testStubbedClassLoading(第 109-120 行)则用new ByteBuddy(ClassFileVersion.JAVA_V6)实际构建了一个改写toString的子类并走完加载流程,佐证了「生成 → dex → 加载」整条链路的可行性。

三、两种加载策略:Wrapping 与 Injecting

README 明确指出:Byte Buddy Android提供两种加载策略,与 Byte Buddy 标准策略中的 wrapping 与 injection 思路相似。二者的抽象基类都是AndroidClassLoadingStrategy(见 AndroidClassLoadingStrategy.java 第 80 行),区别在于doLoad的实现方式。

1. Wrapping:创建包装类加载器

AndroidClassLoadingStrategy.Wrapping(源码第 666-706 行)的doLoad实现为:

ClassLoader dexClassLoader = new DexClassLoader( jar.getAbsolutePath(), // dex 所在 jar 路径 privateDirectory.getAbsolutePath(), // 优化后 dex 的输出目录 EMPTY_LIBRARY_PATH, // 空 native 库路径 classLoader); // 以传入的类加载器为父加载器

随后通过Class.forName(typeDescription.getName(), false, dexClassLoader)逐个加载类型。

特点:

  • 每次加载都会新建一个独立的DexClassLoader,加载进来的类型对该类加载器之外的代码不可见;
  • 以传入的类加载器为父,遵循父优先委托模型;
  • 适合不想污染现有类加载器命名空间、希望动态类型可被整体卸载(随类加载器一起回收)的场景。

2. Injecting:注入到目标类加载器

AndroidClassLoadingStrategy.Injecting(源码第 711-921 行)把类型注入到传入的类加载器本身,而非新建包装加载器。其实现分两个时代:

  • Android P(API 28)及更高版本:通过反射调用BaseDexClassLoader#addDexPath(String, boolean),将 dex 路径追加进目标类加载器(Dispatcher.ForAndroidPVm,第 861-919 行)。源码在第 891-892 行做了明确校验:On Android P, a class injection can only be applied to BaseDexClassLoader,即目标类加载器必须是BaseDexClassLoader的子类,否则抛出IllegalArgumentException;
  • 旧版本 VM:通过dalvik.system.DexFile.loadDex(...)加载 dex 后调用dexFile.loadClass(name, classLoader)注入(Dispatcher.ForLegacyVm,第 819-856 行)。两个 dispatcher 通过静态块中的反射探测自动选择(第 722-730 行)。

特点:

  • 不创建新类加载器,加载的类型与目标类加载器中原有类型处于同一命名空间;
  • 注入的类可以访问同包下其他类的包级私有成员;
  • 额外限制:不能向 bootstrap 类加载器(null)注入。Injecting.load方法(第 756-761 行)会显式抛出IllegalArgumentException: Cannot inject classes into the bootstrap class loader on Android,对应测试testInjectBootstrapLoader(测试文件第 100-106 行)也验证了这一约束。

3. 两个构造函数与自定义 dex 编译器

两种策略各有两个构造函数:

构造函数说明
Wrapping(File privateDirectory)使用默认的基于 SDK dex 编译器的DexProcessor
Wrapping(File privateDirectory, DexProcessor dexProcessor)允许传入自定义DexProcessor
Injecting(File privateDirectory)同上,使用默认 dex 处理器
Injecting(File privateDirectory, DexProcessor dexProcessor)允许传入自定义DexProcessor

注意:基类构造器(第 148-155 行)要求传入的目录必须真实存在且是目录,否则抛出IllegalArgumentException: Not a directory ...(测试testAndroidClassLoaderRequiresDirectory第 95-98 行验证了该行为)。

四、目录选择与 Android 安全模型(重要约束)

源码 javadoc(第 53-78 行)与 README 都强调了一个关键约束:加载策略必须把临时文件写入不被其他应用共享的目录。

Android 的沙箱模型不允许把待加载的 dex/jar 文件放到其他应用可以篡改的位置(例如外部存储),否则第三方应用可替换这些文件从而破坏沙箱隔离。README 中使用的context.getDir("generated", Context.MODE_PRIVATE)正是标准做法,源码 javadoc 还建议了两个合法选择:

  1. android.content.Context#getDir(String, int)并以Context.MODE_PRIVATE创建私有目录;
  2. Android API 21 及以上暴露的getCodeCacheDir()目录。

其余细节:

  • 临时文件清理:load方法在 finally 块中会删除生成的临时 jar 文件(源码第 184-188 行,删除失败时仅记录 warning 日志)。但目录本身不会被自动清理——javadoc 明确说明"目录在应用退出后不会被 Byte Buddy 清理,这仍是使用者的责任"(第 141-143 行)。
  • POSIX 权限加固:源码第 107-122 行通过反射尝试调用java.nio.file.Files#setPosixFilePermissions将生成的 jar 标记为只读(OWNER_READ),在支持该 API 的平台上进一步加强文件防护;不支持时静默退化为空操作。

五、底层原理:从 class 字节码到 dex 的转换链路

AndroidClassLoadingStrategy.load(源码第 160-189 行)的完整工作流程为:

  1. 调用dexProcessor.create()开启一次 dex 转换会话;
  2. 遍历types映射,对每个类型调用conversion.register(name, byte[])注册 JVM class 字节码;
  3. 在私有目录中创建随机命名的 jar 文件;
  4. 在 jar 内写入classes.dex条目(DexClassLoader约定的查找路径,常量DEX_CLASS_FILE,第 86 行),调用conversion.drainTo(jarOutputStream)把 dex 内容写出;
  5. 对 jar 做只读加固;
  6. 调用抽象方法doLoad(classLoader, typeDescriptions, jar),由Wrapping/Injecting各自实现实际加载。

DexProcessor:可替换的 dex 编译核心

DexProcessor接口(第 285-315 行)抽象了「JVM class 文件 → dex 文件」的转换,内置默认实现DexProcessor.ForSdkCompiler(第 321-661 行)基于 Android SDK 的dx 编译器(API 兼容 1.7 版本)工作:

  • 它通过com.android.dx.cf.direct.DirectClassFile解析 JVM class 字节码,再用CfTranslator.translate(...)翻译为ClassDefItem加入DexFile(第 454-464 行);
  • 为了兼容不同时代的 dx 库 API,静态块中通过反射探测挑选三种 dispatcher(第 336-369 行):兼容 API 26+ 的ForApi26LevelCompatibleVm(多一个DxContext参数)、旧版ForLegacyVm,以及无法匹配时的Unavailable(直接抛异常);
  • makeDefault()把 dex 的目标 API 级别固定为DEX_COMPATIBLE_API_VERSION = 13(第 326、376-380 行),以兼容内置编译器。

关于 class 文件版本的说明

源码 javadoc(第 68-77 行)特别提示:内置的 dx 编译器(1.7 版)要求输入为 Java 6(ClassFileVersion.JAVA_V6)格式的 class 文件。这一点在测试中得到印证——testStubbedClassLoading使用new ByteBuddy(ClassFileVersion.JAVA_V6)构建动态类型(测试文件第 110 行)。

该版本略显过时(因为 Maven Central 上没有更新版本的 dx),但它足够满足多数字节码生成场景。若需要更新版本的 dex 编译器,可自行实现DexProcessor接口并委托给新版编译器;若新版编译器的 API 与内置版兼容,甚至只需把新版 dx 加入应用构建路径、同时排除内置版本即可(详见源码 javadoc 第 70-77 行)。

依赖构成

byte-buddy-android/pom.xml 展示了模块的关键依赖:

  • net.bytebuddy:byte-buddy:核心字节码生成引擎;
  • com.google.android:android(provided作用域):Android SDK 类;
  • com.jakewharton.android.repackaged:dalvik-dx:Android SDK 的 dex 编译器(dx 工具)的重新打包版本。

模块还定义了java6-compatibilityprofile(第 119-129 行):在 JDK 1.6 下构建时回退到dalvik-dx1.0 版。

六、适用前提与平台限制

README 明确了两条硬性前提/限制,务必在选型前确认:

  1. 要求 Android API 21 或更高版本(Android 5.0 Lollipop)。API 21 以下没有DexClassLoader的现代能力,且getCodeCacheDir等私有目录 API 也不可用;
  2. 无法转换(transform)已加载的类,也无法注册 Java agent。这是因为 Android 运行时没有标准的java.lang.instrumentagent 机制,Instrumentation重定义类的能力在 Android 上不可用。因此 Byte Buddy Android 的适用边界是「生成并加载新类型」,而不是「修改已加载类」。

七、仓库内的真机验证示例

为验证上述机制在真实 Android 环境下的可行性,仓库提供了完整的 Android 测试应用模块byte-buddy-android-test:

  • TestActivity.java 是一个可安装运行的Activity:界面提供「Run test: wrapping」和「Run test: injecting」两个按钮(对应布局 main.xml),点击后即执行一次完整的「生成子类 → 加载 → 实例化 → 校验toString()」流程,并通过 Toast 显示成功/失败;
  • 示例第 144-147 行使用MethodDelegation.to(Interceptor.class)拦截toString,并借@SuperCall Callable<String>生成辅助类型(auxiliary type),专门用来验证辅助类型的 dex 转换与加载也能正常工作(见 Interceptor 的 javadoc,第 84-89 行)——这是仅用FixedValue无法覆盖的更深层场景;
  • 该模块的构建由androidMaven profile 激活(byte-buddy-android-test/pom.xml 第 50-57 行,需本机安装 Android SDK),并依赖android-maven-plugin打包 APK;根 pom.xml 第 28 行也注明:"Builds an Android test application. An Android SDK is required for doing so. (excluded from release)",即该模块不参与发布。

八、使用要点速查

主题结论
加载策略AndroidClassLoadingStrategy.Wrapping/Injecting两种,均基于DexClassLoader/ dex 注入
最低 APIAndroid API 21 及以上(README 明确)
工作目录必须是应用私有目录(Context.getDir(name, MODE_PRIVATE)或getCodeCacheDir()),严禁外部共享存储
默认 dex 编译器Android SDK dx 编译器(API 兼容 1.7),要求输入 Java 6 格式 class 文件;可通过DexProcessor接口替换
临时文件jar 在加载完成后自动删除;私有目录本身需应用自行管理清理
平台限制不能重定义/转换已加载类;不能注册 Java agent;Injecting不能用于 bootstrap 类加载器
依赖需引入byte-buddy+dalvik-dx,Android SDK 依赖为provided
源码入口byte-buddy-android/src/main/java/net/bytebuddy/android/AndroidClassLoadingStrategy.java
测试与示例AndroidClassLoadingStrategyTest.java、TestActivity.java

结语

Byte Buddy Android 的定位清晰:在 Android 运行时生成并加载新类,而非改造已有类。它以AndroidClassLoadingStrategy为唯一入口,内部串联起「JVM class → dex → jar → 类加载器」的完整转换链;Wrapping与Injecting两种策略分别对应「隔离可见性」与「注入现有命名空间」两种需求。使用时的核心红线是:目录必须私有(Android 沙箱安全)、API 必须 ≥ 21、且不要试图在 Android 上做 agent 式的类重定义。结合仓库中的单元测试与真机示例工程,你可以快速验证并把它接入自己的 Android 项目中。

  • 开发工具
  • 后端

【免费下载链接】byte-buddy

Runtime code generation for the Java virtual machine.

项目地址:https://gitcode.com/gh_mirrors/by/byte-buddy
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询