Eden 模拟器配置每次崩溃就被重置: 从 C++ 源码的非原子写入讲起

Eden 里的设置总是莫名其妙回到默认, 尤其在我把模拟器强杀、或者它自己崩溃之后。图形驱动、内存布局、语言区域, 全部复位。我一开始以为是 Android 什么时候把应用数据清了, 先后重装过两次, 还在手机上给它开了各种后台保活。重装能好一阵, 然后又犯。

真正的原因和 Android 没关系, 在 Eden 自己的源码里。

不是"没保存", 是"保存到一半被截断"

配置存在 files/config/config.ini。Eden 保存设置的写法, 是把整个文件重新写一遍。问题在打开文件的模式上:

void Config::WriteToIni() const {
    fp = fopen(config_loc.c_str(), "wb");   // "wb" 一打开就把文件截断为 0
    // ...
    config->Save(writer, false);            // 再逐项写入;若此刻崩溃 → 文件残缺
}
void Config::Reload() {
    ReadValues();   // 读现有配置
    SaveValues();   // 立刻又写回去
}

"wb" 这个模式在 POSIX 里的语义是: 打开即把文件截断成 0 字节, 然后从零开始写。也就是说, 从打开到写完的这段时间里, 磁盘上的 config.ini 处于"已经被清空、还没写回来"的状态。如果在这段窗口里进程被强杀或崩溃, 留下的就是一个残缺或空白的文件。下一次启动去解析它, 解析失败, Eden 就回退到内置默认值——看到的现象就是"配置被重置"。

这还没完。Reload() 每次启动都会先读、再立刻写回去。也就是说每次启动都有一个这样的写入窗口, 崩溃并不需要多罕见。写配置本身不是原子的(没有先写临时文件再 rename), 这是根因。

知道这一点之后, 保活、重装都变成了治标: 只要崩溃正好落在写入窗口里, 结果就是重置。

一个被误判的问题: adb 传进去的配置文件, Eden 写不进去

排查过程中我还踩了一个容易混淆的坑。/sdcard/Android/data/<包名>/ 下, 用 adb push 传进去的文件, 属主是 shell, 而 Eden 是另一个 uid。Eden 打开这些文件靠的是 group/other 权限位:

  • 644: Eden 作为 other 可以读, 但 other 没有写权限, 于是界面里改的设置存不进去。
  • 666: group/other 都可写, Eden 才能把改动保存下来。

我一度把"改了设置又变回去"误诊成"Eden 自动把配置改回来了", 其实是"文件根本写不进去"。这里还有一个反直觉的细节: 读取本身一直是好的, 只有写入受限, 所以表面上看配置能正常加载, 却永远保存不了新值。另外, 直接 adb push 覆盖 config.ini 也不推荐, 那会连属主一起换成 shell; 要原地修改应该用 adb shell "cat > 文件", 这样保留原来的属主。

解法: 把 config.ini 冻结成只读

既然崩溃时的截断改不了, 那就让 Eden 根本没机会去重写这个文件。做法是把 config.ini 的属主保持为 shell、权限设成 444 只读:

Eden 仍能正常读到全部设置, 但它再也没有权限去打开写入、截断或改坏这个文件; WriteToIni 会直接失败, 文件保持原样。我用连续强杀 Eden 三次来验证: 每次记下 config.ini 的 SHA256, 三次完全一致, 配置不再漂移。这个方案的代价很明确——在 Eden 界面里改设置不会生效(因为写不进去), 这正好就是防重置的原理。要改配置就走下面的手动流程。

代价与例外: 什么时候不能冻结

冻结的代价是"界面改设置不保存", 但这恰好挡不住一个真实的例外: 金手指和 Mod 的开关状态也存在 config.ini 的 [DisabledAddOns] 段里。如果 nightly 版既要当主力、又要频繁开关金手指, 冻结只读会导致在 Add-ons 里怎么点都关不掉。所以我最后的做法是按构建分开处理: 稳定版继续冻结 444 防止重置, nightly 解锁成 666 让它能保存开关, 代价是 nightly 失去防重置保护, 靠定期备份兜底。这不是一个"哪个更好"的问题, 而是看这个构建更怕丢配置还是更需要改设置。

一个管理脚本

为了不每次都敲一长串 adb, 我把它写成了 eden_config.sh:

./eden_config.sh show            # 查看当前设置(语言/区域/驱动/属主)
./eden_config.sh backup 名称      # 备份当前配置(含 53 个游戏专属配置)
./eden_config.sh restore 名称     # 恢复并自动锁回只读(默认 golden)
./eden_config.sh freeze          # 冻结(只读 444)
./eden_config.sh unfreeze        # 解锁(可写 666)
./eden_config.sh list            # 列出所有备份

脚本里的默认包名可以用环境变量切到 nightly:

EDEN_PKG=dev.eden.eden_emulator.nightly ./eden_config.sh freeze

restore 的流程也值得记一下, 它把几个坑串在了一起: 先 am force-stop 停掉 Eden, 再 chmod 644 解锁, 用 cat > 原地写入保留 shell 属主, 最后 chmod 444 锁回只读并重启。设备上还留了一份黄金备份 /sdcard/eden_backup/golden/, 里面有配置和全部游戏专属配置, 需要时一键回滚。

改配置的正确流程

因为文件是 shell 属主且只读, 正常改配置的流程是固定四步: 先 unfreeze 解锁, 用 adb shell "cat > 文件" 原地写入(保留属主), 再 chmod 444 锁回只读, 最后重启 Eden 生效。改之前先 backup 一份, 改坏了直接 restore。

回头看, “配置被重置"这个现象里其实叠了两个不同层面的问题: 一个是 Eden 自己的非原子写入, 一个是 Android 的 uid 隔离导致的权限问题。前者靠源码才看清, 后者靠权限模型才说通, 而最终的解法是一行 chmod——用操作系统的权限直接把这个写入机会从应用手里拿走。

文章链接:

https://time-friend.com/zh/archive/eden-config-reset-non-atomic-write/

# 相关文章推荐