跳转到内容

android-bridge 平台

android-bridge 是 Android 上的另一条实现路线:它在设备上起一个真 JVM,直接运行桌面版游戏 Jar 与桌面版加载器。本文面向需要理解或接入这条路线的人——宿主开发者、以及要判断「某个游戏版本该走哪条路」的维护者。玩家侧不需要这些,见使用移动版。

另一条路线见 Android(ART)平台。

ART 路线android-bridge 路线
游戏版本兼容性好:一套实现能覆盖很多版本要跟着游戏版本走,游戏侧的形状一变就得重新适配
维护成本低,不需要怎么维护就能支持很多版本高,需要经常维护
游戏跑在哪直接跑在 ART 上桥起一个真 JVM,跑桌面版本体
启动速度首次要先构建 dex 缓存不需要预先构建,装好即启动
性能与功耗好:ART 是面向移动端优化的 Java 运行时,直接跑在上面有成效可能打一些折扣
Mixin构建期注入运行期注入,与桌面版完全一致
载荷游戏 Jar + 游戏源码包(在设备上 dex 化)JRE + arc 原生库 + bridge.jar
换模组之后必须重新构建缓存重新启动即可

一句话:要覆盖尽量多的游戏版本、看重性能与功耗,走 ART;要桌面版本体原样运行、要 Mixin 与桌面完全一致、要启动快,走桥。 桥的代价是版本适配要持续跟进。

sequenceDiagram
    participant 宿主
    participant 桥
    participant JVM as 真 JVM
    participant 加载器
    participant 游戏

    宿主->>桥: 交出游戏 Jar 与启动参数
    桥->>JVM: 拉起 JVM,摆好 classpath
    JVM->>加载器: 由桌面版加载器接管启动
    加载器->>加载器: 读取模组,应用 Mixin
    加载器->>游戏: 执行游戏入口,游戏开始运行

桥自己不认识模组:它只负责起 JVM、摆好 classpath、起游戏。模组能力来自被它启动的桌面版加载器,所以 Mixin 等修改器的行为与桌面版完全一致。

项目范围
Android11(API 30)及以上
ABIarm64-v8a、armeabi-v7a、x86_64
游戏版本官方发行版 146.0 及以上,BE 构建号 24369 及以上(两套编号各自独立,互不可比)
游戏 Jar必须是带资源的桌面版;只有类的那种跑不起来

桥是「一个 jar」,但不携带跑游戏需要的一切。宿主必须提供下面这些,全是绝对路径:

载荷参数说明
游戏本体 Jar-G <path>带资源的桌面版。它自带 arc 与 assets,不用另外给 arc 的 Java 包
JRE--java <jre>/bin/javaAndroid 版 OpenJDK 25
arc 原生库--arc-lib <dir>原生实现(.so),不是 Java 包。缺了游戏能起,但会缺掉一部分依赖原生实现的功能(例如声音)
bridge.jar由组件工厂追加 --bridge-jar不要自己传这个参数
加载器-L / --loader-jar要模组或 Mixin 时用,见下文
(可选)ANGLE--angle 或 --angle-path <dir>只给 GL 驱动有问题的设备
选项值必需说明
-G, --game-jarpath✅游戏 Jar / 额外 classpath Jar,可重复;顺序有意义
-D, --game-datapath✅游戏数据目录:存档、设置、模组、last_log.txt
-C, --cache-pathpath✅桥的运行期目录:抽出的原生库与 tmp
--javapath✅Android JRE 的 bin/java,不是桌面 JDK
--arc-libpath实际必需arc 原生库目录
-L, --loader-jarpath注入加载器的 Jar,可重复;顺序即 classpath 顺序
--mainclass注入加载器的主类(配 -L 用)
--bridge-jarpath宿主加载桥的那份 Jar,只由组件工厂追加
--gl3 / --gl2flag请求 OpenGL ES 3(默认)/ ES 2
--abiabi一般不用给:不传时按设备认;只在排查「载荷与设备对不上」时覆盖
-J, --jvm-argsarg一条一个 -J,可重复
--no-jvm-argsflag关掉桥注入的 JVM 调优参数
-d / --verboseflag桥自己的日志开到 [D] / [V]
--logcatflag除文件外也写 Android log;不加就只有 [E] 行会进 logcat
裸词 / -- 之后位置参数:直接给游戏;注入加载器时先给加载器

桥本身不认识模组,要让游戏跑在 Copper 上就得让桌面版加载器先接管启动。给 CopperLoader 写的适配器是 loader-wrapper,做法是把两个 Jar 交给桥,并让适配器当主类:

--loader-jar loader-wrapper-<版本>.jar
--loader-jar desktop-<版本>.jar
--main copper.wrapper.Main
  • -L 的顺序就是 classpath 顺序:适配器在前(它是主类),加载器桌面 Jar 在后;
  • wrapper 把桥给的 classpath 原样翻译成加载器自己的 --game-jar,加载器随后接管启动——读取模组、建立 Mixin 引擎,最后回到桥的入口把游戏起来;
  • 加载器自己的选项(--vanilla、-d、--mixin-log 等)因此能直接从启动参数给,剩下的交给游戏本体;
  • 模组放在 <数据目录>/copper/mods/ 与 <数据目录>/mods/,与桌面版一致。

桥的库多版本共用,每个版本只各占一份自己的运行期目录与数据目录:

  • 文件夹数据根/
    • 文件夹bridge/ — 桥的库(各版本共用)
      • bridge-<版本>.jar — 桥本身
      • 文件夹jre/ — Java 运行环境(按设备架构装一次)
        • …
      • 文件夹arc/<arc 引用>/<架构>/ — arc 原生库,按游戏版本用到的 arc 分开存放
        • …
    • 文件夹<版本目录>/
      • 文件夹data/ — 该版本的游戏数据目录(存档、设置、模组、日志)
        • …
      • 文件夹bridge/cache/ — -C:桥为这个版本摆出来的运行期文件
        • …
  1. 放 --arc-lib、--angle-path 的目录必须可写。 桥要在里面把 .so 改成「可执行 + 只读」(Android 的 W^X 限制:被加载的库不能带写位、必须有执行位)。目录只读时改不动,加载就可能失败。
  2. 换载荷要先删旧文件。 被改成只读之后原地覆盖写不进去;arc 原生库、JRE 的库、--angle-path 那两份都这样。桥自己在 -C 里放的文件不用管,它按内容比对,不一样就重写。
  3. 一个进程只能有一个 JVM。 第二次启动前必须让上一次的进程死掉,否则报 UnsatisfiedLinkError: … already opened by ClassLoader 0x…。宿主应在每次启动前清掉残留进程。
  4. 失败怎么呈现。 参数错、Jar 缺失这类错误会同步报回宿主;之后的失败发生在游戏进程里,宿主收不到回调,只能看 <游戏数据目录>/last_log.txt 或占位界面的堆栈。

点击启动后界面回来,什么都没发生

Section titled “点击启动后界面回来,什么都没发生”

原因:组件工厂没被命中——占位 activity 的类名与清单条目、启动时用的类名不是逐字符相同。

原因:参数非法、Jar 缺失、ABI 不符……读堆栈第一行。

failed to initialise the native bridge / is for EM_… instead of EM_…

Section titled “failed to initialise the native bridge / is for EM_… instead of EM_…”

原因:ABI 不一致——JRE、arc 原生库、bridge.jar 必须同为一种 ABI。

解决:载荷里放一份 ABI 记录,启动前和设备自己报的 ABI 比一下。

游戏能跑,但少了功能(例如没有声音)

Section titled “游戏能跑,但少了功能(例如没有声音)”

原因:该版本的 arc 原生库没取到,依赖原生实现的功能会缺失。

解决:按本体的 commitHash 查 archash 后补下对应的 .so。

日志里只有 failed to start the JVM 和一段堆栈

Section titled “日志里只有 failed to start the JVM 和一段堆栈”

原因:多半是 --arc-lib 没给、或目录里没有 .so。

换了 arc 原生库 / JRE / ANGLE 之后跑的还是旧的

Section titled “换了 arc 原生库 / JRE / ANGLE 之后跑的还是旧的”

原因:旧文件已被改成只读,覆盖没写进去。先删掉旧文件再放新的。

一份 <游戏数据目录>/last_log.txt,每次启动清空一次,桥与游戏的输出都在里面(游戏设置里的「导出日志」读的也是它)。要在 logcat 里同时看,必须传 --logcat。