Skip to content

Android(ART)平台

This content is not available in your language yet.

Android 端与桌面版的本质区别:桌面版在启动时实时加载类并应用 Mixin;Android 上必须先由加载器把游戏与所有模组构建成 dex 缓存,运行时只加载构建好的 dex。本文只讲模组作者需要知道的部分,更底层的实现(ArtPlatform、LoaderActivity 等)请直接看源码。

Android 上还有另一条路线不经 dex 构建——android-bridge 平台:桥在设备上起一个真 JVM,直接运行桌面版游戏与桌面版加载器,Mixin 在运行期生效。两条路线的取舍见那页的对比表;本文讲的是 ART 这条。

Android 运行时(ART)不能像桌面 JVM 那样直接加载任意 Jar/class——应用内可执行代码必须是 .dex 格式。因此加载器在设备上(或桌面上)先把字节码转换成 dex:

  1. 以游戏的桌面 Jar 作为字节码来源,加载游戏容器;
  2. 加载每个已启用的模组,在构建期应用 Mixin(与桌面版同一套 Mixin 引擎,版本过滤规则同样生效);
  3. 用 d8 把字节码转成 dex,合并进运行时缓存;
  4. 设备运行时只加载构建好的 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:

  1. --init(一次性,准备基础缓存),需要游戏源码包与 Arc 源码包:

    • 把游戏的 Android 源码与 Arc 的 Android 后端源码编译成 android.jar(Arc 后端的基类会被改写为加载器自己的 Activity 基类,细节见源码);
    • 把游戏 Jar 中所有非类文件打包进 asset.jar;
    • 把 Arc 的原生库(.so)打包进 lib.jar。
  2. --build(按需,构建运行时缓存),只需要模组与加载器:

    • 把每个模组各自 dex 化一次(不含 Mixin),作为该模组的基础 dex;
    • 加载每个已启用的模组,读取其 Mixin 配置并应用注入(按版本过滤选分支),再把注入产生的字节码与基础 dex 合并成该模组的 Mixin dex;
    • 最后写下运行时描述文件,记录当前模组集合里每个模组应该加载哪个 Mixin dex。

    构建失败时不会写入运行时描述文件(出错的那个 Mixin dex 也会被丢弃),因此不会留下”半成品缓存”被设备端加载。

--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 上”处理)

缓存由 DexCache 管理,位于 --cache-path 下:

  • Directorycache-path/
    • asset.jar — --init 产物:打包的游戏资源
    • android.jar — --init 产物:编译好的 Android 组件
    • lib.jar — --init 产物:打包的原生库
    • Directorydex/
      • Directorybase/
        • Directory模组目录/
          • 版本哈希.jar — 该模组未应用 Mixin 的基础 dex
      • Directorymixin/
        • Directory模组目录/
          • Directory哈希目录/
            • rt.jar — 该模组的 Mixin dex
            • meta — 该 dex 的描述文件
      • Directoryruntime/
        • 哈希文件 — 当前启用模组集合的运行时描述文件

base/ 与 mixin/ 下的目录名是模组 id(冒号替换为连字符),runtime/ 下是单个文件。两种哈希的算法分别见下文。

runtime/ 下文件的文件名是当前启用模组列表的 sha256:游戏版本 + 每个模组的 id:版本。因此:

  • 增删模组、升级版本都会得到不同的文件;
  • 多个模组集合可以共存、按需重新构建,无需手动清空;
  • 同一模组集合重复执行 --build 会直接跳过构建(描述文件已存在即视为构建完成);
  • 设备端启动器按”当前模组集合”查找对应文件,找不到就报 runtime cache is not existed, build it first(见使用移动版 · 常见问题)。

每个模组的 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。
  • Mixin 完全兼容:构建缓存时运行的是与桌面版一致的真实 Mixin 引擎,注入在构建期生效,版本过滤分支按构建时的游戏版本选择;
  • 依赖与冲突声明的重要性提高:玩家更换模组后必须重新执行 build,依赖缺失、版本冲突或不满足会导致构建失败(见版本、依赖与冲突);
  • 类导出必须完整,d8 对不完整导出更敏感:构建阶段 d8 会一次性静态解析每个类的完整结构(父类链、接口、成员签名),这比桌面运行时按需加载更严格。被其他模组依赖的类,其公有成分(超类、接口、签名中的类型)都必须一并导出,否则 d8 报缺类——见类隔离机制 · 导出完整性;
  • 调试手段在构建期:设备运行时(MockMixinEngine)不做任何字节码变换,注入效果只能通过构建期日志确认,不存在”运行时调试”一说。构建时使用 --verbose 会为游戏与所有模组启用 Mixin 审计日志,设备端同样有效(启动器 App 的 build 操作固定携带 --verbose);日志写入缓存目录下的 last_log.txt,出错时可在 App 的错误对话框中复制查看。构建器命令行没有暴露 --mixin-flag 类导出参数(如 DEBUG_EXPORT),该能力不适用于 ART 构建流程。