解析 SharedPreferences:从原理到实战优化

10 阅读4分钟

一、SharedPreferences 的核心存储机制揭秘

SharedPreferences(简称 SP)作为 Android 中最常用的轻量级存储方案,其底层实现暗藏诸多设计细节。当我们调用getSharedPreferences时,系统会按以下流程工作:

  1. 文件与缓存机制

    • SP 以 XML 文件形式存储在/data/data/包名/shared_prefs/目录,文件名由用户指定(如gityuan.xml)。
    • ContextImpl通过静态缓存 sSharedPrefsCache维护所有 SP 实例,以包名→文件→SharedPreferencesImpl的三层映射结构避免重复创建实例,提升性能。
  2. 加载流程的线程设计

    • 首次获取 SP 时,SharedPreferencesImpl会创建新线程异步加载 XML 文件(loadFromDisk())。

    • 加载完成前,所有getXXX()edit()操作会被阻塞(通过wait()等待线程通知),直到数据完全载入内存的mMap中。

关键源码片段

java

// SharedPreferencesImpl初始化时启动异步加载
SharedPreferencesImpl(File file, int mode) {
    startLoadFromDisk(); // 开启新线程加载文件
}

private void loadFromDisk() {
    // 解析XML文件并将数据存入mMap
    map = XmlUtils.readMapXml(str); 
    synchronized (this) {
        mLoaded = true;
        notifyAll(); // 唤醒阻塞的线程
    }
}

二、Editor 操作的核心流程与坑点

当调用edit()获取编辑器时,实际上创建的是EditorImpl实例,其数据操作分为两步:

  1. 内存暂存阶段

    • putXXX()操作仅将数据存入EditorImpl.mModified map,此时未影响磁盘文件。
    • remove()操作通过存入特殊值this标记删除,clear()则标记mClear=true
  2. 提交阶段:commit 与 apply 的本质差异

    • commit() :同步写入磁盘,返回布尔值表示是否成功。调用时会阻塞线程直到写入完成,适合需确认结果的场景(如登录状态保存)。

    • apply() :异步写入磁盘,无返回值。数据先提交到内存mMap,再通过单线程池写入文件,适合频繁提交的场景(如配置项更新)。

性能对比案例
在 1000 次连续提交测试中:

  • commit () 耗时约 150ms(主线程阻塞),

  • apply () 耗时约 30ms(异步执行)。

核心差异源码

java

// commit()同步等待磁盘写入
public boolean commit() {
    mcr.writtenToDiskLatch.await(); // 阻塞等待
    notifyListeners(mcr);
    return mcr.writeToDiskResult;
}

// apply()异步提交
public void apply() {
    QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable); // 线程池执行
    notifyListeners(mcr);
}

三、多进程与性能优化的实战指南

  1. 多进程陷阱与替代方案

    • MODE_MULTI_PROCESS模式通过每次加载时对比文件时间戳实现跨进程同步,但存在以下问题:

      • 频繁加载导致性能损耗;
      • 无法处理并发写入冲突(后写入的进程会覆盖先写入的数据)。
    • 推荐方案:使用ContentProviderDataStore配合ViewModel实现跨进程数据共享。

  2. 高频操作优化策略

    • 批量提交:避免多次调用edit(),应合并操作后一次性提交:

      java

      SharedPreferences.Editor editor = sp.edit();
      editor.putString("key1", "value1");
      editor.putInt("key2", 2);
      editor.apply(); // 一次提交所有修改
      
    • 拆分文件:将高频读写的 key 与低频操作的 key 分离到不同 SP 文件,减少锁竞争:

      java

      // 高频配置单独存储
      SharedPreferences frequentSp = getSharedPreferences("frequent_config", MODE_PRIVATE);
      // 低频数据存储
      SharedPreferences infrequentSp = getSharedPreferences("infrequent_data", MODE_PRIVATE);
      
  3. ANR 预防与监控

    • SP 导致 ANR 的常见场景:

      • 在主线程执行大量 commit () 操作;
      • 加载大文件时阻塞主线程。
    • 监控方案:通过StrictMode检测主线程磁盘操作:

      java

      StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
          .detectDiskReads()
          .detectDiskWrites()
          .penaltyLog()
          .build());
      

四、与 DataStore 的对比与迁移建议

Android Jetpack 推出的DataStore作为 SP 的替代方案,在以下场景更具优势:

特性SharedPreferencesDataStore(Preferences)
数据类型基本类型 + StringSet基本类型 + Flow 响应式数据
线程安全需手动处理自动异步处理
性能(大数据量)较差(全量写入)更好(增量更新)
跨进程支持有限(MODE_MULTI_PROCESS)内置支持

迁移示例

java

// 旧方案:SP同步提交
SharedPreferences sp = getSharedPreferences("config", MODE_PRIVATE);
sp.edit().putInt("theme", 1).commit();

// 新方案:DataStore异步提交
val dataStore = context.createDataStore("config")
dataStore.edit { preferences ->
    preferences["theme"] = "1"
}

五、常见问题与源码级解决方案

  1. 数据丢失问题

    • 原因:写入磁盘时进程被杀,未完成的事务导致数据未更新。
    • 解决方案:SP 通过.bak备份文件实现部分恢复(写入前重命名原文件为 bak,成功后删除)。
  2. 内存泄漏风险

    • 原因:长时间持有SharedPreferencesImpl实例,或未移除OnSharedPreferenceChangeListener
    • 解决方案:在 Activity/Fragment 销毁时调用unregisterOnSharedPreferenceChangeListener
  3. 跨版本兼容性

    • Android N 后禁止MODE_WORLD_READABLEMODE_WORLD_WRITEABLE,需改用权限控制。
    • Android 10 + 中 SP 文件默认存储在沙盒目录,外部应用无法直接访问。

六、总结:SP 的正确打开方式

  1. 适用场景

    • 轻量级配置(如主题、语言、登录状态);
    • 少量高频读写的数据(如计数器)。
  2. 禁忌场景

    • 存储大文本(如 JSON 字符串);
    • 频繁在主线程执行 commit ();
    • 跨进程共享数据(推荐 DataStore)。
  3. 最佳实践口诀

    • 小数据、轻量级,apply 异步更合适;

    • 批量提交少调用,文件拆分减竞争;

    • 多进程用新方案,内存泄漏及时清。

通过深入理解 SP 的源码机制与性能瓶颈,开发者可在实际项目中规避陷阱,结合 DataStore 等新方案实现更高效的数据存储。