第3章 SharedPreferences同步读写陷阱

3.1 原理:SharedPreferences的文件I/O本质

SharedPreferences(SP)底层的存储介质是XML文件。无论是读操作还是写操作,最终都会落到

磁盘文件I/O

上。每次调用 getSharedPreferences() 时,SP实例会尝试加载并解析整个XML文件到内存中;每次调用 commit() 或 apply() 时,都会将全部数据序列化后写回磁盘。这种全量加载与全量写入的机制,决定了它在主线程高频调用时存在天然的ANR风险。

核心风险认知:SP的读写操作会持有一个内部锁。即使使用apply()异步写入,如果写入任务在主线程之外的后台队列积压,当Activity或Service进入后台(例如按下Home键)时,系统会强制等待所有SP写入完成,此时主线程依然可能被阻塞导致ANR。

3.2 commit vs apply:差异与隐藏的ANR风险

对比维度commit()apply()
写入机制同步写盘,直接写入文件系统。异步写盘,先写入内存,再提交到QueuedWork队列。
返回值返回 boolean,表示写入是否成功。无返回值 (void)。
主线程阻塞阻塞主线程,直到写入完成。不阻塞主线程。
ANR风险高(直接阻塞,尤其在写入大文件时)。中高(onPause()/onStop() 会等待QueuedWork完成)。
高频写入影响每次调用都触发一次磁盘I/O,卡顿明显。大量日志积压在队列中,导致生命周期回调超时ANR。

陷阱解析:很多开发者认为apply()是安全的,于是不加节制的在主线程高频调用apply()。虽然每个apply()本身不卡主线程,但大量任务堆积在QueuedWork中。当用户按下Home键,onPause()需要等待QueuedWork全部刷入磁盘,此时主线程被冻结,形成隐性ANR。

3.3 避免在主线程调用 getSharedPreferences

除了写入陷阱,getSharedPreferences()在首次调用时也会同步读取整个XML文件。如果该文件较大(例如超过100KB),或者存储在低速闪存上,主线程可能会被阻塞几十甚至几百毫秒,直接导致应用启动期ANR或界面跳转卡顿。

优化策略:
  1. 延迟初始化:不要在 Activity.onCreate() 或 Application.onCreate() 的主线程中直接调用 getSharedPreferences(),使用 ListenableFuture 或简单线程预加载。
  2. 数据分类:仅将小数据(<1KB)存放在SP中;复杂结构化数据请使用 DataStore、MMKV 或 Room。
  3. 预读取:在 Application 的异步初始化阶段提前加载SP,待界面需要时直接使用缓存。

3.4 实战案例:高频写入导致ANR的复现与修复

复现代码(高风险写法):

// 模拟在主线程高频写入SharedPreferences
SharedPreferences sp = getSharedPreferences("cache", MODE_PRIVATE);
for (int i = 0; i < 500; i++) {
    sp.edit().putString("key_" + i, String.valueOf(i)).apply();
    // 每个apply都压入QueuedWork队列
}
// 用户立即按下Home键 -> onPause()等待队列刷完 -> ANR

修复代码(批量提交 + 避免主线程积压):

// 使用Editor批量提交,仅触发一次apply
SharedPreferences sp = getSharedPreferences("cache", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
for (int i = 0; i < 500; i++) {
    editor.putString("key_" + i, String.valueOf(i));
}
editor.apply(); // 只有一次提交,队列不会积压
关键原则:高频写入场景下,务必使用Editor的批量化操作,将多次put合并为一次apply提交,从根本上避免QueuedWork队列堆积。

3.5 性能与稳定性建议

  • 严格控制文件大小:SP文件应保持在 1KB以下,超过此阈值请选择DataStore、MMKV或数据库。
  • 避免主线程首次加载:通过ExecutorService或HandlerThread异步预读SP,保证主线程不被文件I/O阻塞。
  • 用apply替代commit:绝大多数场景使用apply(),且注意配合Editor批量调用。
  • 生命周期意识:在onPause()或onStop()中不要执行任何SP写入操作,以防止生命周期阻塞。

本章图解:SharedPreferences 主线程调用模型与 ANR 风险路径

图3-1 SP 主线程调用模型与 ANR 风险路径 主线程 SP 调用 commit() 同步写盘 ANR (主线程卡死) apply() 入队 QueuedWork onPause() 等待写入完成 ANR (生命周期超时) 优化方向 1. Editor 批量提交 2. 异步预加载 SP 3. 高频场景使用 MMKV