如何在多进程环境中安全地共享 SharedPreferences 数据?

解读

国内 App 为了保活、插件化或厂商推送,普遍会把 Service、Provider 甚至 WebView 放在独立进程。面试时,面试官想确认两点:

  1. 候选人是否知道 SharedPreferences 的底层文件锁只在单进程生效,多进程并发写会损坏 XML;
  2. 能否给出在国内“无 GMS、可落地”的完整替代方案,而不是背官方文档。
    回答必须体现“安全”与“性能”双重考虑:既要防止 ANR、数据损坏,又要兼顾小米、华为机型 ROM 对文件描述符回收、IO 调度差异。

知识点

  1. SharedPreferences 文件锁(FileLock)只在进程内生效,多进程同时 commit/apply 会触发 truncate→write→fsync 竞态,导致 XML 结构破损或旧数据回滚。
  2. apply() 异步刷盘通过 QueuedWork 在 Activity.onPause 阻塞主线程,多进程下更易出现 100+ ms 卡顿,国内低端机表现尤其明显。
  3. Android 7.0 之后不支持 MODE_MULTI_PROCESS,官方直接标记 deprecated。
  4. ContentProvider 内部通过 Binder 串行化访问,天然支持跨进程;但 bulkInsert 未实现默认串行,需手动加 synchronized。
  5. 国内主流 ROM(MIUI、EMUI、ColorOS)对后台进程限制 20 s 回收,ContentProvider 若依赖后台线程写盘,需加 JobScheduler 补写,否则进程被杀后数据丢失。
  6. 加密场景:TEE 仅对系统密钥库有效,App 层需用 AndroidKeystore + AES/GCM 对共享文件做页级加密,防止 adb pull 泄露。
  7. 性能指标:单次跨进程 < 5 ms、并发 200 次写 < 100 ms、包体增量 < 50 KB,是美团、字节等厂内部压测红线。

答案

线上环境禁止直接多进程读写 SharedPreferences。国内项目落地采用“ContentProvider + 串行化 + 加密 + 异常降级”四件套:

  1. 自建 Provider 并声明 android:multiprocess="false",避免系统为每个进程创建独立实例;
  2. 在 Provider 内部维护单线程 Executor(Executors.newSingleThreadExecutor()),所有 put/remove 操作序列化,返回值使用 Bundle 传递,避免 Cursor 泄漏;
  3. 数据落地用 XML 或 MMKV:
    a. XML 方案:Provider 内部仍用 SharedPreferences,但文件路径放在 /data/data/<pkg>/shared_prefs/central_config.xml,权限 600,防止 adb pull;
    b. MMKV 方案:腾讯 MMKV 基于 mmap,支持多进程锁,Provider 仅做 key 白名单校验,直接调 MMKV.mmkvWithID("global", MULTI_PROCESS_MODE),性能提升 5×;
  4. 加密:对 value 做 AES/GCM 加密,密钥由 AndroidKeystore 生成,Provider 在 query/insert 时自动加解密,客户端无感知;
  5. 异常降级:Provider 被系统回收时,捕获 DeadObjectException,立即把未同步数据写入可执行文件目录的 backup.xml,并在进程重启后由 Application.onCreate 重新导入,保证“0 丢失”;
  6. 版本迁移:首次升级在 Provider 的 call 方法里返回 version_code,客户端比对后触发一次性全量拷贝,防止旧代码仍直接访问 SharedPreferences 导致双写。

示例核心代码(ContentProvider 内部):

private val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? = runBlocking(dispatcher) {
    when (method) {
        "putString" -> {
            val encrypted = encrypt(extras!!.getString("value"))
            mmkv.encode(extras.getString("key"), encrypted)
            Bundle().apply { putBoolean("ok", true) }
        }
        "getString" -> {
            val cipher = mmkv.decodeString(arg)
            Bundle().apply { putString("value", decrypt(cipher)) }
        }
        else -> null
    }
}

对外暴露封装类 SpProxy,接口与 SharedPreferences 完全一致,内部通过 ContentResolver.call 跨进程,业务层无需改动旧代码。

拓展思考

  1. 折叠屏双开场景:同一 UID 下两个进程(主屏与副屏)同时写,Provider 需加 UID 级锁,避免 Binder 重入导致数据覆盖。
  2. 车载与 Wear 异构设备:车机进程数 > 30,Provider 方案 Binder 句柄耗尽,可改用 Unix Domain Socket + mmap 共享环形队列,实现零拷贝。
  3. 隐私沙盒(Android 13 受限存储):SharedPreferences 文件路径将被重定向至 /Android/sandbox,Provider 需在 query 时动态解析真实路径,否则 FileNotFoundException。
  4. 未来替代:Google 在 AOSP 主干已引入 SharedMemory 持久化 KV(codename“RocketKV”),支持 SELinux 标签隔离,面试时可提及“已关注源码,预计 Android 15 落地”,展示技术前瞻性。