SharedPreferences

207 阅读5分钟

基本使用

SharedPreferences sharedPreferences = context.getSharedPreferences(getLocalClassName(), MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("key", "value");
editor.apply();

SharedPreferences 本身是一个接口,程序无法直接创建 SharedPreferences 实例,只能通过 Context 提供的 getSharedPreferences(String name, int mode) 方法来获取 SharedPreferences 实例,name 表示要存储的 xml 文件名,第二个参数直接写 Context.MODE_PRIVAT,表示该 SharedPreferences 数据只能被本应用读写。当然还有 MODE_WORLD_READABLE 等,但是已经被废弃了,因为 SharedPreference 在多进程下表现并不稳定。

适用范围

保存少量的数据,且这些数据的格式简单,适用保存应用的配置参数,但不建议使用 SP 来存储大规模数据,可能会降低性能。

核心原理

保存基于 XML 文件存储的 key-value 键值对数据,在 /data/data//shared_prefs 目录下。SharedPreferences 本身只能获取数据而不支持存储和修改,存储修改是通过 SharedPreferences.Editor 来实现的,它们两个都只是接口,真正的实现在 SharedPreferencesImpl 和 EditorImpl 。

源码分析

在此之前,我们先看 ContextImpl 中的一个静态成员变量:

/*
 * String:包名,File:SP 文件,SharedPreferencesImpl:SP 实例对象
 */
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;

因为一个进程只会存在一个 ContextImpl.class 对象,所以同一个进程内的所有 SharedPreferences 都保存在这个静态列表中。我们在看它的初始化:

private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
        if (sSharedPrefsCache == null) {
            sSharedPrefsCache = new ArrayMap<>();
        }

        final String packageName = getPackageName();
        ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
        if (packagePrefs == null) {
            packagePrefs = new ArrayMap<>();
            sSharedPrefsCache.put(packageName, packagePrefs);
        }

        return packagePrefs;
    }

总结

  1. sSharedPrefsCache 是一个 ArrayMap<String,ArrayMap<File,SharedPreferencesImpl>>,它会保存加载到内存中的 SharedPreferences 对象,ContextImpl 类中并没有定义将 SharedPreferences 对象移除 sSharedPrefsCache 的方法,所以一旦加载到内存中,就会存在直至进程销毁。相对的,也就是说,SP 对象一旦加载到内存,后面任何时间使用,都是从内存中获取,不会再出现读取磁盘的情况
  2. SharedPreferences 和 Editor 都只是接口,真正的实现在 SharedPreferencesImpl 和 EditorImpl ,SharedPreferences 只能读数据,它是在内存中进行的,Editor 则负责存数据和修改数据,分为内存操作和磁盘操作
  3. 获取 SP 只能通过 ContextImpl#getSharedPerferences 来获取,它里面首先通过 mSharedPrefsPaths 根据传入的 PackageName 拿到 File ,然后根据 File 从 ArrayMap<File, SharedPreferencesImpl> cache 里取出对应的 SharedPrederenceImpl 实例,edit()每次都是创建新的EditorImpl对象.
  4. SharedPreferencesImpl 实例化的时候会启动子线程来读取磁盘文件,但是在此之前如果通过 SharedPreferencesImpl#getXxx 或者 SharedPreferences.Editor 会阻塞 UI 线程,因为在从 SP 文件中读取数据或者往 SP 文件中写入数据的时候必须等待 SP 文件加载完
  5. 在 EditorImpl 中 putXxx 的时候,是通过 HashMap 来存储数据,提交的时候分为 commit 和 apply,它们都会把修改先提交到内存中,然后在写入磁盘中。只不过 apply 是异步写磁盘,而 commit 可能是同步写磁盘也可能是异步写磁盘,在于前面是否还有写磁盘任务。对于 apply 和 commit 的同步,是通过 CountDownLatch 来实现的,它是一个同步工具类,它允许一个线程或多个线程一致等待,直到其他线程的操作执行完之后才执行
  6. SP 的读写操作是线程安全的,它对 mMap 的读写操作用的是同一把锁,考虑到 SP 对象的生命周期与进程一致,一旦加载到内存中就不会再去读取磁盘文件,所以只要保证内存中的状态是一致的,就可以保证读写的一致性
  7. 使用了apply方式异步写sp的时候每次apply()调用都会形成一次提交,每次有系统消息发生的时候(handleStopActivity, handlePauseActivity)都会去检查已经提交的apply写操作是否完成,如果没有完成则阻塞主线程。SharedPreference apply 引起的 ANR 问题

注意事项以及优化建议

  1. 强烈建议不要在 SP 里面存储特别大的 key/value ,有助于减少卡顿 / ANR
  2. 请不要高频的使用 apply,尽可能的批量提交;commit 直接在主线程操作,更要注意了
  3. 不要使用 MODE_MULTI_PROCESS
  4. 高频写操作的 key 与高频读操作的 key 可以适当的拆分文件,以减少同步锁竞争
  5. 不要连续多次 edit,每次 edit 就是打开一次文件,应该获取一次 edit,然后多次执行 putXxx,减少内存波动,所以在封装方法的时候要注意了

问题

SharedPreferences每次写入时是增量写入吗?

每次commit时会把全部的数据更新的文件, 所以整个文件是不应该过大的, 影响整体性能;


SharedPreference apply 引起的 ANR 问题

Google 在 Activity 和 Service 调用 onStop 之前阻塞主线程来处理 SP。 剖析 SharedPreference apply 引起的 ANR 问题

SharedPreferences支持进程同步吗?怎么让它支持

SharedPreferences支持进程同步吗?怎么让它支持

SharedPreferences是线程安全的吗?

apply 与commit的对比

apply没有返回值, commit有返回值能知道修改是否提交成功 apply是将修改提交到内存,再异步提交到磁盘文件; commit是同步的提交到磁盘文件; 多并发的提交commit时,需等待正在处理的commit数据更新到磁盘文件后才会继续往下执行,从而降低效率; 而apply只是原子更新到内存,后调用apply函数会直接覆盖前面内存数据,从一定程度上提高很多效率。

参考:

Android 数据持久化之 SharedPreferences 全面剖析SharedPreferences