SharedPreferences 存储大量数据时会出现什么性能问题?如何优化?

解读

面试官想确认两点:

  1. 候选人是否真正踩过“SP 存大对象”的坑,而不仅仅背“轻量级存储”概念;
  2. 能否给出国内真实业务场景下的量化数据与工程化解决方案,而不是泛泛而谈“用 MMKV/数据库”。
    回答时要围绕“一次 commit/apply 到底做了什么”展开,把主线程阻塞、GC、anr、电量、系统锁竞争讲透,再给出国内主流 App(日活千万级)验证过的优化路径。

知识点

  1. 实现机制:SP 本质是 /data/data/<pkg>/shared_prefs/*.xml 的 DOM 树,全部驻内存(SharedPreferencesImpl.mMap),每次 commit/apply 全量序列化到磁盘,文件大小与内存占用 1:1。
  2. 事务模型:commit 同步阻塞调用方,apply 异步但仍把全量 xml 写入队列,队列由单线程 QueuedWork.singleThreadExecutor 串行执行,阻塞后续 apply 与组件生命周期(Activity stop/Service stop)的 QueuedWork.waitToFinish()
  3. 性能拐点:国内低端机实测,xml 超过 100 KB 时第一次读入耗时 15-20 ms;超过 300 KB 时单次 apply 落盘 80-120 ms,连续 5 次触发“等待”逻辑即可造成 Input ANR(主线程 5 s 无响应)。
  4. 稳定性:xml 采用 FileUtils.writeFileAtomically(),先写 .bak 再重命名,大文件场景下 fsync 耗时翻倍,极端情况下触发系统 WatchDog。
  5. 国内加固与渠道:部分加固框架会 hook SharedPreferencesImpl.writeToFile(),导致全量写放大 1.3-1.5 倍;华为/小米后台省电策略对 fsync 限频,进一步放大卡顿。
  6. 官方替代:Jetpack DataStore 基于 Kotlin Coroutines 与 Protobuf,增量写入,无 xml,但 API 仍在 1.0.x,国内大型 App 落地节奏慢;MMKV 基于 mmap,增量更新,微信 10 亿级验证,已写入腾讯开源镜像站,国内接入成本最低。

答案

“SharedPreferences 设计初衷是存少量标志位,当 value 累计到几十 KB 以上就会出现三类性能问题:

  1. 主线程阻塞:commit 直接阻塞调用线程;apply 虽异步,但 Activity 生命周期会等待 QueuedWork 完成,xml 越大等待越久,低端机 300 KB 就能触发 ANR。
  2. 内存与 GC:整个 xml 被加载到 ConcurrentHashMap,再序列化 StringBuilder 到字节数组,大对象直接进入 Old 区,频繁编辑造成 GC 抖动。
  3. 电量与锁竞争:每次全量写盘伴随 fsync,文件越大耗时越久,后台监控显示连续写入 500 KB 可使 CPU 使用率瞬间升高 15%,在小米/华为限频机型上容易被系统标记为异常耗电。

国内一线 App 的优化套路分三步:
第一步,量化:在灰度环境埋点,统计所有 xml 文件大小及 apply 耗时,把 >100 KB 的文件列为“红色阈值”。
第二步,拆分与降级:

  • 把缓存型大数据(如 JSON、路由表、埋点采样配置)迁移到 MMKV,读写耗时降低 90%,内存占用减少 70%;
  • 把结构化业务数据(用户足迹、商品快照)迁移到 Room,利用 SQLite 的页缓存与 WAL 增量日志;
  • 仅保留真正的“标志位”在 SP,如首次启动、隐私协议弹窗状态,单文件控制在 20 KB 以内。
    第三步,写入策略:
  • 批量聚合后一次性 apply,避免循环写;
  • 对即时性要求不高的 key,使用 LifecycleCoroutineScope 在进程空闲时延迟写入,减少与主线程生命周期冲突;
  • 在应用启动时预加载常用 SP 到内存缓存,后续只读不写,彻底消除运行时 IO。

通过以上手段,我们现网将 SP 平均文件大小从 260 KB 降到 8 KB,ANR 率下降 38%,后台耗电评分提升 0.12 分(华为应用市场评分),验证了方案在国内主流机型上的有效性。”

拓展思考

  1. 如果团队已全面 Kotlin 协程化,可继续追问“DataStore 与 MMKV 在一致性、跨进程、备份恢复上的差异”,考察是否读过源码。
  2. 车载或 Wear 场景下闪存寿命敏感,需进一步追问“mmap 频繁增量写入是否会带来 flash 写放大,如何权衡缓存与寿命”。
  3. 国内厂商后台清理策略各异,可延伸讨论“如何将 SP 迁移到云端配置 + 本地 LRU 缓存”实现可灰度、可回滚的用户体验。