Android(ART)平台
Android 端与桌面版的本质区别:桌面版在启动时实时加载类并应用 Mixin;Android 上必须先由加载器把游戏与所有模组构建成 dex 缓存,运行时只加载构建好的 dex。本文只讲模组作者需要知道的部分,更底层的实现(ArtPlatform、LoaderActivity 等)请直接看源码。
Android 上还有另一条路线不经 dex 构建——android-bridge 平台:桥在设备上起一个真 JVM,直接运行桌面版游戏与桌面版加载器,Mixin 在运行期生效。两条路线的取舍见那页的对比表;本文讲的是 ART 这条。
为什么需要构建 dex 缓存
Section titled “为什么需要构建 dex 缓存”Android 运行时(ART)不能像桌面 JVM 那样直接加载任意 Jar/class——应用内可执行代码必须是 .dex 格式。因此加载器在设备上(或桌面上)先把字节码转换成 dex:
- 以游戏的桌面 Jar 作为字节码来源,加载游戏容器;
- 加载每个已启用的模组,在构建期应用 Mixin(与桌面版同一套 Mixin 引擎,版本过滤规则同样生效);
- 用 d8 把字节码转成 dex,合并进运行时缓存;
- 设备运行时只加载构建好的 dex,不再做任何字节码变换。
为什么不复用现成的 dex
Section titled “为什么不复用现成的 dex”现有产物(原版游戏 APK 内置的 dex、低 API 级别编译的模组 dex)都无法直接复用,原因如下:
原版游戏必须经加载器改造后才能运行。 加载器的集成能力(Copper 模组桥接、游戏内模组管理禁用、配置目录重定向、扩展数据包注册等)依赖对游戏字节码的修改——这部分修改由核心模组的 Mixin 在构建期注入。原版游戏 APK 自带的 dex 未包含这些修改,直接加载等同于绕过加载器运行游戏。因此游戏必须以桌面 Jar 为字节码来源,注入核心模组的修改后再 dex 化。
旧 API 级别编译的 dex 无法安全复用。 构建器统一以 min-api 30 为脱糖目标(对应 Android 11+),而多数原版模组按更低的 API 级别(Android 8/9 时代,即 API 26/28 左右)编译,两者的脱糖(desugaring)实现不同。直接复用旧目标编译的 dex,在 min-api 30 环境下会因类缺失而在运行时崩溃。
因此,游戏本体、原版模组与 Copper 模组都在构建阶段以统一的目标(min-api 30)重新 dex 化,不存在可复用的现成 dex。
构建分两个命令,--init 只需执行一次,之后只改模组或加载器时只需 --build:
-
--init(一次性,准备基础缓存),需要游戏源码包与 Arc 源码包:- 把游戏的 Android 源码与 Arc 的 Android 后端源码编译成
android.jar(Arc 后端的基类会被改写为加载器自己的 Activity 基类,细节见源码); - 把游戏 Jar 中所有非类文件打包进
asset.jar; - 把 Arc 的原生库(
.so)打包进lib.jar。
- 把游戏的 Android 源码与 Arc 的 Android 后端源码编译成
-
--build(按需,构建运行时缓存),只需要模组与加载器:- 把每个模组各自 dex 化一次(不含 Mixin),作为该模组的基础 dex;
- 加载每个已启用的模组,读取其 Mixin 配置并应用注入(按版本过滤选分支),再把注入产生的字节码与基础 dex 合并成该模组的 Mixin dex;
- 最后写下运行时描述文件,记录当前模组集合里每个模组应该加载哪个 Mixin dex。
构建失败时不会写入运行时描述文件(出错的那个 Mixin dex 也会被丢弃),因此不会留下”半成品缓存”被设备端加载。
源码包的格式
Section titled “源码包的格式”--game-src / --arc-src 必须是从 GitHub 直接下载的源码压缩包(zip/tar.gz 均可)。这类包解压后的顶层是一个文件夹,文件夹名形如 仓库名-分支 或 仓库名-tag(例如 Mindustry-8.0、Mindustry-v159.7),仓库的实际文件均位于该文件夹内。构建器会剥掉这个顶层文件夹名来定位源码;手工打包、或解压后重新整理的目录结构无法被识别,会导致 --init 解析源码失败。
| 参数 | 长参数 | 说明 |
|---|---|---|
-G <path> | --game-jar | 游戏 Jar(必须为桌面版,作为字节码来源) |
-D <path> | --game-data | 游戏数据目录 |
-L <path> | --loader-jar | 加载器 Jar |
-C <path> | --cache-path | 缓存根目录 |
--game-src <path> | 游戏源码包(仅 --init 需要,须为 GitHub 源码包,见上文) | |
--arc-src <path> | Arc 源码包(仅 --init 需要,须为 GitHub 源码包,见上文) | |
--init | 创建基础缓存(一次性) | |
--build | 构建运行时缓存(修改模组/加载器后执行) | |
--clear | 清空已构建的缓存(保留基础缓存) | |
--remove <modId> | 删除与某模组相关的全部缓存 | |
--vanilla | 原版模式:只加载原版模组、核心模组与 Copper 插件(见下文) | |
--verbose | 输出更详细的日志 | |
--desktop | 声明构建器运行在桌面 JVM 上(使用 JvmMixinClassLoader;不加则按”自身运行在 Android dex 上”处理) |
缓存目录结构
Section titled “缓存目录结构”缓存由 DexCache 管理,位于 --cache-path 下:
文件夹cache-path/
- asset.jar —
--init产物:打包的游戏资源 - android.jar —
--init产物:编译好的 Android 组件 - lib.jar —
--init产物:打包的原生库 文件夹dex/
文件夹base/
文件夹模组目录/
- 版本哈希.jar — 该模组未应用 Mixin 的基础 dex
文件夹mixin/
文件夹模组目录/
文件夹哈希目录/
- rt.jar — 该模组的 Mixin dex
- meta — 该 dex 的描述文件
文件夹runtime/
- 哈希文件 — 当前启用模组集合的运行时描述文件
- asset.jar —
base/ 与 mixin/ 下的目录名是模组 id(冒号替换为连字符),runtime/ 下是单个文件。两种哈希的算法分别见下文。
运行时描述文件的哈希命名
Section titled “运行时描述文件的哈希命名”runtime/ 下文件的文件名是当前启用模组列表的 sha256:游戏版本 + 每个模组的 id:版本。因此:
- 增删模组、升级版本都会得到不同的文件;
- 多个模组集合可以共存、按需重新构建,无需手动清空;
- 同一模组集合重复执行
--build会直接跳过构建(描述文件已存在即视为构建完成); - 设备端启动器按”当前模组集合”查找对应文件,找不到就报
runtime cache is not existed, build it first(见使用移动版 · 常见问题)。
Mixin dex 的复用与失效
Section titled “Mixin dex 的复用与失效”每个模组的 Mixin dex 存放在 mixin/ 下「模组目录 / 哈希目录」两层目录里,哈希由该模组自身的版本加上所有向它注入 Mixin 的来源模组的版本共同决定(来源与版本记录在同目录的 meta 文件中)。因此:
- 模组自身版本变化后,重新构建会得到新的哈希目录,旧产物不会被复用;
- 给某个模组写 Mixin 的另一个模组升级了,被注入方的 Mixin dex 同样会失效并重建;
- 游戏本体(
mindustry)的 Mixin dex 来源里包含核心模组,而核心模组版本始终等于加载器版本(core.version = loaderVersion)。因此更新加载器后,游戏本体的 Mixin dex 会按新哈希重新生成,不会复用加载器更替前的旧产物。
基础 dex(base/)只按模组自身的版本哈希命名,与 Mixin 无关:模组版本没变时可直接复用。
原版模式(--vanilla)主要为 Android 启动器服务:启动器必须经由加载器才能在应用内启动内置的游戏。原版模式使加载器只启动内置的原版游戏、跳过普通 Copper 模组,但仍然加载原版 Mindustry 模组、核心模组(对游戏隐藏),以及 Copper 插件——即声明了 "hidden": true 的 Copper 模组(见模组元数据 · hidden)。
对模组作者而言:
- 构建或启动时使用
--vanilla,所得到的即为不含普通 Copper 模组的原版环境; - 插件在原版模式下照常加载:插件对标原版 Mindustry 的插件(Plugin),只承载逻辑、不注册新内容,因此必须随游戏一起运行的配套模组可以做成插件,在原版模式下也照常生效;
- 原版模式下不会调用
registerPackets(),自定义网络数据包(ExtendedPacket)自然不会注册,以避免与原版联机协议产生冲突; - 桌面端需要原版环境时直接运行官方 Jar 即可;若要在原版游戏中保留插件,则改用
--vanilla。
对模组作者的影响
Section titled “对模组作者的影响”- Mixin 完全兼容:构建缓存时运行的是与桌面版一致的真实 Mixin 引擎,注入在构建期生效,版本过滤分支按构建时的游戏版本选择;
- 依赖与冲突声明的重要性提高:玩家更换模组后必须重新执行
build,依赖缺失、版本冲突或不满足会导致构建失败(见版本、依赖与冲突); - 类导出必须完整,d8 对不完整导出更敏感:构建阶段 d8 会一次性静态解析每个类的完整结构(父类链、接口、成员签名),这比桌面运行时按需加载更严格。被其他模组依赖的类,其公有成分(超类、接口、签名中的类型)都必须一并导出,否则 d8 报缺类——见类隔离机制 · 导出完整性;
- 调试手段在构建期:设备运行时(
MockMixinEngine)不做任何字节码变换,注入效果只能通过构建期日志确认,不存在”运行时调试”一说。构建时使用--verbose会为游戏与所有模组启用 Mixin 审计日志,设备端同样有效(启动器 App 的build操作固定携带--verbose);日志写入缓存目录下的last_log.txt,出错时可在 App 的错误对话框中复制查看。构建器命令行没有暴露--mixin-flag类导出参数(如DEBUG_EXPORT),该能力不适用于 ART 构建流程。