ProtoDataStore 和 PreferencesDataStore 分别适用于什么场景?

解读

国内面试中,DataStore 是 2021 年后 Google 官方主推的“SharedPreferences 替代方案”。面试官问“分别适用场景”并不是想听“一个存对象、一个存键值”这种一句话结论,而是想看候选人能否把“数据形态、读写频率、一致性要求、升级迁移、多进程、异常恢复、包体积”这些真实落地痛点全部摆出来,再给出工程级选型建议。答得越深,越能体现线上踩坑经验。

知识点

  1. 实现原理
    PreferencesDataStore 基于 Kotlin Coroutines + Flow,文件格式是 protobuf 版“preferences.proto”,但只支持 key=String、value=Int/String/Long/Float/Boolean 五种标量,内部用一整个 ProtoMap 一次性序列化/反序列化。
    ProtoDataStore 允许开发者自定义 protobuf schema(*.proto),文件里只存一条 message,读写都是整包覆盖,同样基于 Coroutines + Flow。

  2. 事务与一致性
    两者都保证“事务级原子性”:写入先写 .tmp 文件,fsync 后 rename;crash 时要么旧文件完整保留,要么新文件完整生效,不会出现 SharedPreferences 的“半写坏 XML”问题。

  3. 读写性能
    全量读写:启动时一次性把整包反序列化到内存,后续访问全部走内存快照;每次 write 都是全量落盘。
    高频增量写场景(例如每 200 ms 统计一次滑动事件)会放大 IO,导致 blkio 抖动,甚至触发 Android 11 后的“频繁后台 IO 限流”。

  4. 多进程
    官方明确不支持多进程同时读写;如果业务有多进程需求(如推送、支付插件),需要自行加文件锁或把数据下沉到 ContentProvider/Room。

  5. 版本迁移与向后兼容
    ProtoDataStore 靠 protobuf 的“field tag”机制天然向前兼容;PreferencesDataStore 只能把旧 SP 文件一次性整体迁移,无法做字段级缺省值升级。

  6. 包体积
    protobuf-lite 会给 apk 增加 60~80 KB 左右;PreferencesDataStore 因为固定 schema 体积增量更小。如果 apk 对“64 K 方法数”或“下载大小”极度敏感(国内渠道包 30 MB 红线),需要评估。

  7. 混淆与 R8
    proto 生成的 **$$Serializer 类必须被 keep,否则安装后首次运行直接 crash;PreferencesDataStore 无此坑。

  8. 国内 ROM 兼容
    部分魔改 ROM(例:某厂深度定制 5.1)对 /data/data/<pkg>/files/datastore/ 目录做了“后台清理”策略,导致文件被删后读取抛 IOException;需要 catch 后重建文件,否则 ANR。

答案

PreferencesDataStore 适用场景

  1. 数据形态全部是“简单键值”,且 key 数量级在百级以内;
  2. 读多写少,写操作以用户主动触发为主(例如切换夜间模式、修改账号开关);
  3. 无多进程并发写需求;
  4. 对包体积增量极度敏感,希望只增加几十 KB;
  5. 业务原本用 SharedPreferences,迁移成本要求最低——可直接使用 produceSharedPreferencesMigration() 一次性整体搬迁。

ProtoDataStore 适用场景

  1. 需要存储结构化对象(嵌套 POJO、List<Bean>、Map<String,UserSetting>)且字段会随版本不断新增或废弃;
  2. 对“强类型”与“模式演进”有硬性要求,拒绝手动拼 JSON;
  3. 写频率低或整包覆盖可接受(例如用户修改一次个人资料、保存一次草稿);
  4. 愿意接受额外 60~80 KB 的 protobuf-lite 体积,以及编写 .proto 文件、配置 protobuf 插件的构建复杂度;
  5. 未来可能跨模块、跨平台(Flutter、车载、Wear)共用同一份 schema,借助 protobuf 的跨语言能力减少重复开发。

一句话总结:
“简单键值 + 轻量快速迁移”选 PreferencesDataStore,“结构化数据 + 版本兼容 + 强类型”选 ProtoDataStore;高频增量写或严格多进程场景,两者都不合适,请直接上 Room + 协程或 MMKV。

拓展思考

  1. 线上曾出现“PreferencesDataStore 文件 2 MB 大小、200 个 key”导致启动 IO 耗时 80 ms 的案例,优化方案是把大对象拆到独立文件,再用 MultiProcessDataStore(官方未开源,可参考 AOSP 实现自行封装)按业务域分片。
  2. 对于需要“毫秒级同步写 + 多进程共享”的埋点配置,国内大厂普遍采用 MMKV 或自研 mmap KV;面试时可以补充“DataStore 与 MMKV 的混合策略”:热路径用 MMKV,冷配置用 DataStore,既保证性能又享受 Jetpack 协程链式观察。
  3. 折叠屏/多窗口场景下,应用可能因配置变更重启,需在 ViewModel 中缓存 DataStore<Flow> 实例,避免重复创建 DataStore 导致的文件句柄泄漏。
  4. 国内渠道包加固后,首次启动会触发“多 dex 加载”,此时 protobuf 类可能还没被加载完就执行 read,会抛 NoClassDefFoundError;可通过延迟初始化或自定义 Serializer 懒加载解决,面试提到这一点可直接把“深度碎片化兼容”经验打满。