SharedPreferences 是 Android 中一种轻量级的持久化存储方案,其本质是使用 XML 文件存储一系列的键值对,为了提高其使用效率,采用了异步写、内存缓存等方法。

关于调用

SharedPreferences 是一个接口,其实现类是 SharedPreferencesImpl ,它有一个构造方法SharedPreferencesImpl(File, int),使用 protected 修饰,所以我们在使用它的时候一般都不能直接实例化一个 SharedPreferences ,而是通过 Context 的getSharedPreferences(String, int)得到其实例。在 ContextImpl 类中有关于这个方法的具体实现,实现分为两步:第一步将 name 转化为对应的 File 文件,第二步通过 File 和 Mode 得到对应的 SharedPreferences 实例。并且在这两步中都使用了 ArrayMap 做缓存,分别缓存了 File 和已经实例化的 SharedPreferences 。

@Override
public SharedPreferences getSharedPreferences(String name, int mode) {// At least one application in the world actually passes in a null// name.  This happened to work because when we generated the file name// we would stringify it to "null.xml".  Nice.if (mPackageInfo.getApplicationInfo().targetSdkVersion <Build.VERSION_CODES.KITKAT) {if (name == null) {name = "null";}}File file;synchronized (ContextImpl.class) {if (mSharedPrefsPaths == null) {mSharedPrefsPaths = new ArrayMap<>();}file = mSharedPrefsPaths.get(name);if (file == null) {file = getSharedPreferencesPath(name);mSharedPrefsPaths.put(name, file);}}return getSharedPreferences(file, mode);
}@Override
public SharedPreferences getSharedPreferences(File file, int mode) {SharedPreferencesImpl sp;synchronized (ContextImpl.class) {final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();sp = cache.get(file);if (sp == null) {checkMode(mode);if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {if (isCredentialProtectedStorage()&& !getSystemService(UserManager.class).isUserUnlockingOrUnlocked(UserHandle.myUserId())) {throw new IllegalStateException("SharedPreferences in credential encrypted "+ "storage are not available until after user is unlocked");}}sp = new SharedPreferencesImpl(file, mode);cache.put(file, sp);return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// If somebody else (some other process) changed the prefs// file behind our back, we reload it.  This has been the// historical (if undocumented) behavior.sp.startReloadIfChangedUnexpectedly();}return sp;
}
复制代码

通过两步缓存,使得我们在使用一个 SP 的时候能够尽可能不浪费时间,或者也可以在开发中直接使用一个 SharedPreferences 单例。

另外,SP 作为一种数据持久化存储方式,给开发者提供了一种非常便利的数据存取方式,例如取一个整形的时候只需要sp.getInt(String, int)即可,存一个整形的时候直接调用sp.edit().putInt(String, int).apply(),相比于其他的一些如 SQLite 存储、文件存储等都方便了很多,得益于在 SP 内部通过一个 HashMap 缓存了从 XML 文件中读取的键值对,也完成了对应的将一个 HashMap 中的值存储到 XML 文件中的功能。

再看 SP 的构造方法:

@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {mFile = file;mBackupFile = makeBackupFile(file);mMode = mode;mLoaded = false;mMap = null;mThrowable = null;startLoadFromDisk();
}
复制代码
  • mFile,对应的存储在磁盘中的 XML 文件
  • mBackupFile,用于提交更改时的备份文件
  • mMode,当前 XML 的读写权限,私有、公共读、公共写等
  • mLoaded,是否已经将数据从文件加载到内存
  • mMap,内存中保存的数据,取数据都是从这个 Map 中直接取
  • startLoadFromDisk(),加载文件的方法

每次实例化一个 SP 的时候,都会直接调用startLoadFromDisk()将文件中的数据加载到 mMap 中,而后续的诸如getInt(String, int)的方法都是从 mMap 中读取。

取数据

以取一个整形为例:

@Override
public int getInt(String key, int defValue) {synchronized (mLock) {awaitLoadedLocked();Integer v = (Integer)mMap.get(key);return v != null ? v : defValue;}
}@GuardedBy("mLock")
private void awaitLoadedLocked() {if (!mLoaded) {// Raise an explicit StrictMode onReadFromDisk for this// thread, since the real read will be in a different// thread and otherwise ignored by StrictMode.BlockGuard.getThreadPolicy().onReadFromDisk();}while (!mLoaded) {try {mLock.wait();} catch (InterruptedException unused) {}}if (mThrowable != null) {throw new IllegalStateException(mThrowable);}
}
复制代码

SP 的读取操作都是一个同步代码块,这倒不是为了使多线程的读同步,而是为了使多线程的写同步。每当实例化一个 SP 的时候,它首先要做的是从对应的 XML 文件中读取数据到成员变量 mMap 中,这里的同步的目的就是为了让所有的取数据的操作发生在读取文件后面,也就是说只有先将数据从文件中读取到 mMap 中,才能从 mMap 中根据 key 取 value 。

读文件需要时间,所以 SP 中为读取 XML 文件新开了一个线程。

@UnsupportedAppUsage
private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();}}.start();
}
复制代码

那么 SP 的读数据操作是否会阻塞主线程呢?其实要看 SP 是否会阻塞主线程,只需要看是否在主线程中调用了阻塞线程的方法就行了。那么在上面的awaitLoadedLocked()方法中就可以看到,如果我们使用 SP 取数据的时候,文件还没有加载完成,那么此时即便是主线程也会陷入阻塞,直到文件加载完成。所以对于 SP 的使用来说,有两个基本的原则:

  1. 不使用 SP 存储数据量过大的数据
  2. 尽量不要在实例化 SP 之后立刻就调用 SP 的取数据方法

SP 在加载 XML 文件的时候是一次性将文件中的所有数据都加载到 HashMap 中的,所以如果数据量太大,一方面会导致对内存的占用过大,另一方面也需要太多的时间解析 XML 文件。另外,SP 会在实例化的时候调用startLoadFromDisk()方法加载 XML 文件,在加载文件的过程中会阻塞调用诸如getInt(String)的方法,就有可能导致主线程的阻塞,所以应该尽可能的早一些实例化 SP ,只要能够保证在取数据之前完成 XML 文件的加载,就不会造成阻塞。

存数据

SP 中存数据并不是直接由它自身完成,而是通过它的内部类 Editor 完成的,调用方法edit()得到一个 Editor 的实例:

@Override
public Editor edit() {// TODO: remove the need to call awaitLoadedLocked() when// requesting an editor.  will require some work on the// Editor, but then we should be able to do:////      context.getSharedPreferences(..).edit().putString(..).apply()//// ... all without blocking.synchronized (mLock) {awaitLoadedLocked();}return new EditorImpl();
}
复制代码

由于每次调用edit()都会直接实例化一个 Editor 类,所以在使用的过程中应该尽量少地调用edit()方法,如使用某种方法将 Editor 实例保存下来。Editor 的实现类是 EditorImpl ,它有两个成员变量:mModified 和 mClear 。

  • mModified,用于保存更改信息,更改信息不会直接写回 mMap 或文件,只有调用了commit()apply()方法之后,更改才会生效
  • mClear,用于标识是否清除 mMap 的内容

以保存一个整形值为例:

@Override
public Editor putInt(String key, int value) {synchronized (mEditorLock) {mModified.put(key, value);return this;}
}
复制代码

所有的存放的数据都保存在了 mModified 上面,而取数据都是从 mMap 中取的,这就是为什么不调用 Editor 的commit()apply()方法,所有的修改都不会生效的原因。调用了commit()方法之后,才能将所做的修改保存到 XML 文件和 mMap 中去。

@Override
public boolean commit() {long startTime = 0;if (DEBUG) {startTime = System.currentTimeMillis();}MemoryCommitResult mcr = commitToMemory();SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " committed after " + (System.currentTimeMillis() - startTime)+ " ms");}}notifyListeners(mcr);return mcr.writeToDiskResult;
}
复制代码

commit 方法主要完成了两个任务,一个是将修改提交到内存中,也就是提交给 mMap ,第二个任务就是将修改写回到文件中。对应的方法分别是commitToMemory()enqueueDiskWrite(MemoryCommitResult, Runnable)

commitToMemory,写回内存

private MemoryCommitResult commitToMemory() {long memoryStateGeneration;List<String> keysModified = null;Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk;synchronized (SharedPreferencesImpl.this.mLock) {// We optimistically don't make a deep copy until// a memory commit comes in when we're already// writing to disk.if (mDiskWritesInFlight > 0) {// We can't modify our mMap as a currently// in-flight write owns it.  Clone it before// modifying it.// noinspection uncheckedmMap = new HashMap<String, Object>(mMap);}mapToWriteToDisk = mMap;mDiskWritesInFlight++;boolean hasListeners = mListeners.size() > 0;if (hasListeners) {keysModified = new ArrayList<String>();listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}synchronized (mEditorLock) {boolean changesMade = false;if (mClear) {if (!mapToWriteToDisk.isEmpty()) {changesMade = true;mapToWriteToDisk.clear();}mClear = false;}for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();// "this" is the magic value for a removal mutation. In addition,// setting a value to "null" for a given key is specified to be// equivalent to calling remove on that key.if (v == this || v == null) {if (!mapToWriteToDisk.containsKey(k)) {continue;}mapToWriteToDisk.remove(k);} else {if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}mapToWriteToDisk.put(k, v);}changesMade = true;if (hasListeners) {keysModified.add(k);}}mModified.clear();if (changesMade) {mCurrentMemoryStateGeneration++;}memoryStateGeneration = mCurrentMemoryStateGeneration;}}return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);
}
复制代码

commitToMemory()完成了将 mModified 和 mMap 合并的任务,还有对OnSharedPreferencesChangeListener的回调。还有一点,关于 clear 的判断,Editor 有一个clear()方法:

@Override
public Editor clear() {synchronized (mEditorLock) {mClear = true;return this;}
}
复制代码

它只有一个任务,将 mClear 置为 true ,而具体的相关的操作是在commitToMemory()方法中实现的,上述 26 行代码,如果 clear 为 true 就会将 mMap 清空,这就意味着当调用 Editor 的clear()方法的时候,它完成的任务仅仅是将 mMap 中的数据清空,而并不会对 mModified 中的数据造成影响。所以类似于editor.putInt("ARG1", 1).clear();的语句,并不能删除键值对<ARG1, 1>,所以在使用的时候应该明确自己要删除的内容再考虑应该使用clear()

与之相对应的还有一个方法remove(String)

@Override
public Editor remove(String key) {synchronized (mEditorLock) {mModified.put(key, this);return this;}
}
复制代码

这个方法的目的也是为了移除 mMap 中的数据,因为它并没有将这个 key 从 mModified 中移除,而是使用 value=this 对其进行标识,以便在commitToMemory()方法中识别。

commitToMemory()方法返回了一个 MemoryCommitResult 对象,这个对象保存了一些在写文件时需要用到的变量。

enqueueDiskWrite(MemoryCommitResult, Runnable),写回文件

enqueueDiskWrite(MemeryCommitResult, Runnable)负责的是将修改写回到 XML 文件中:

private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {final boolean isFromSyncCommit = (postWriteRunnable == null);final Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {writeToFile(mcr, isFromSyncCommit);}synchronized (mLock) {mDiskWritesInFlight--;}if (postWriteRunnable != null) {postWriteRunnable.run();}}};// Typical #commit() path with fewer allocations, doing a write on// the current thread.if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}if (wasEmpty) {writeToDiskRunnable.run();return;}}QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
复制代码

这个方法提供了两种写文件的方式:直接在当前线程写文件;将写文件的任务放到 QueueWork 中执行。这对应着两种提交方法commit()apply()

首先从commit()方法说起,在这个方法中,一旦调用了enqueueDiskWrite()方法后,便调用了mcr.writtenToDiskLatch.await()方法,这个方法会阻塞当前线程。而在commit()中调用的enqueueDiskWrite()中有两种写方式:同步写和异步写。在某些情况下我们调用commit()也可能会执行异步写操作,但是在commit()方法中会阻塞当前线程直到写操作完成。

目前来说官方推荐使用apply()方法完成 Editor 的提交,原因就是apply()中没有了对上述操作的强行等待:

@Override
public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}if (DEBUG && mcr.wasWritten) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);// Okay to notify the listeners before it's hit disk// because the listeners should always get the same// SharedPreferences instance back, which has the// changes reflected in memory.notifyListeners(mcr);
}
复制代码

上面可以看到,在apply()方法中将commit()中用于等待写操作完成的代码放到了一个 Runnable 中执行,并在enqueueDiskWrite()方法中将这个 Runnable 连同写操作共同组成了一个新的 Runnable 放到了 QueueWork 中执行,此时写操作以及等待写操作的过程都在 QueueWork 对应的子线程中执行,在apply()方法中并没有阻塞线程的操作,这就是apply()方法优于commit()方法的原因。

另外为了避免可能出现某些问题,apply()方法还将这个等待 Runnable 加入了 QueueWork 的 Finisher 队列,一般用于即将结束某个 Activity 或 Service 的时候,要等待完成这里的写文件操作之后才行。

总结来说,就是apply()commit()方法都是为了执行写文件操作而做的一些准备操作,它们分别使用了异步和同步的方式会写,最终都会调用writeToFile(MemoryCommitResult, boolean)完成具体的写操作。

writeToFile()的执行步骤大致如下:

  1. 判断是否需要回写
  2. 将 XML 文件改名为备份文件,然后将数据内容写入 XML 文件
  3. 如果写入成功,则删除备份文件,否则在之后的loadFromDisk()方法中会将备份文件重新改名为 XML 文件

判断是否需要回写利用的是三个 stateGeneration 变量,分别对应着磁盘、内存和 MemoryCommitResult ,只有当前的 state 大于上一次提交到磁盘的 state 并且内存的 state 与 MemoryCommitResult 相同时,才会进行写操作,这能避免诸如sp.edit().apply()这种没有任何更改时不必要的写操作。

writeToFile()方法有三种结果:

  1. 执行写操作,执行成功
  2. 未执行写操作,执行成功(提交成功了但无更改)
  3. 未执行写操作,执行失败

这些结果是通过MemoryCommitResult.setDiskWriteResult(boolean, boolean)返回的:

void setDiskWriteResult(boolean wasWritten, boolean result) {this.wasWritten = wasWritten;writeToDiskResult = result;writtenToDiskLatch.countDown();
}
复制代码

可以看到,无论执行成功还是失败,都会调用writtenToDiskLatch.countDown()方法,与之相对应的就是在commit()apply()方法中的mcr.writtenToDiskLatch.await()方法,这一对方法的使用就是为了阻塞当前线程等待写操作完成。

至此,SP 的写数据操作也基本完成了,大致就是两个步骤:

  1. 将修改写回内存
  2. 将修改写回文件

在写文件方面,分别提供了异步写和同步写两种方法。

总结

SP 是 Android 中一种轻量级的持久化存储方案,为了能够更加有效的使用它,应该有以下几点需要注意:

  1. 不要使用 SP 存储太大/太多的数据
  2. 实例化 SP 之后隔一段时间再取数据
  3. 尽量少调用edit()
  4. 多使用apply()
  5. 正确使用clear()方法

转载于:https://juejin.im/post/5c978330f265da60e346fb82

Android SharedPreferences相关推荐

  1. android SharedPreferences数据存储

    android  SharedPreferences数据存储 很多时候我们开发的软件需要向用户提供软件参数设置功能,例如我们常用的QQ,用户可以设置是否允许陌生人添加自己为好友.对于软件配置参数的保存 ...

  2. android sharedpre,Android SharedPreferences四种操作模式使用详解_Android_脚本之家

    Android  SharedPreferences详解 获取SharedPreferences的两种方式: 1 调用Context对象的getSharedPreferences()方法 2 调用Ac ...

  3. android sharedpreferences工具类

    今天,简单讲讲如何写一个sharedpreferences的工具类. 很简单,把一些重复的操作封装在工具类里,其他地方调用就可以.在网上搜索了比较多的资料,找到一个比较好的工具类. 参考文章:http ...

  4. android SharedPreferences的使用优化

    今天,简单讲讲android如何优化使用SharedPreferences保存数据. 之前,我写代码是都是每次缓存一个键值对就commit一次,后来在完成看了看,发现应该是缓存完所有键值对后,再一次性 ...

  5. android SharedPreferences保存list数据

    今天,简单讲讲如何使用  SharedPreferences保存list数据. 网上找了很多资料,还是觉得这种比较简单.直接上代码: 保存: public static boolean saveA ...

  6. android sharedpreferences 工具类,android sharedpreferences工具类

    释放双眼,带上耳机,听听看~! 今天,简单讲讲如何写一个sharedpreferences的工具类. 很简单,把一些重复的操作封装在工具类里,其他地方调用就可以.在网上搜索了比较多的资料,找到一个比较 ...

  7. android 同步list数据,android SharedPreferences保存list数据

    释放双眼,带上耳机,听听看~! 今天,简单讲讲如何使用SharedPreferences保存list数据. 网上找了很多资料,还是觉得这种比较简单.直接上代码: 保存: public static b ...

  8. android SharedPreferences 存储对象

    原文地址为: android SharedPreferences 存储对象 我们知道SharedPreferences只能保存简单类型的数据,例如,String.int等. 如果想用SharedPre ...

  9. android: SharedPreferences存储

    不同于文件的存储方式,SharedPreferences 是使用键值对的方式来存储数据的.也就是 说当保存一条数据的时候,需要给这条数据提供一个对应的键,这样在读取数据的时候就可 以通过这个键把相应的 ...

  10. 大佬教你极简方法来处理Android SharedPreferences设计与实现

    起源 就在前几日,有幸拜读到 HiDhl 的文章,继腾讯开源类似功能的MMKV之后,Google官方维护的 Jetpack DataStore 组件横空出世--这是否意味着无论是腾讯三方还是Googl ...

最新文章

  1. js 动态加载select触发事件
  2. 图片轮播点击轮播(二)
  3. android 等待回调再返回结果_震惊!!只剩你还不懂Java回调机制了!
  4. 你总是不要等到完全准备好了再去做事情
  5. 浅入深出Vue:文章编辑
  6. html的属性与css的属性,HTML的属性和css基础
  7. java 点对点_[java] java消息服务 (二) 点对点模型P2P
  8. win10 当前操作环境不支持支付宝控件 完美解决办法
  9. linux中级之lvs配置(命令)
  10. document.documentElement.clientHeight与document.body.clientHeight的区别
  11. 参考文献,bib文件格式
  12. 用c语言编程小鸭子,小鸭子
  13. java跨平台是什么意思_java的跨平台性指的什么
  14. SQL语法INSERT INTO_大数据培训
  15. 解决某些MySQL数据库的表没了,建数据库也建不了的情况
  16. Matlab曲线拟合(2)(自用笔记)
  17. python网易云爬虫_使用python进行爬虫下载网易云音乐
  18. Vector CANoe修改Panel的名字
  19. 会员等级进度功能前端实现
  20. uTools插件-Excalidraw轻量的在线白板绘图工具

热门文章

  1. sqoop导入/导出
  2. Spring RestTemplate的使用示例
  3. 使用JSONP实现跨域通信
  4. MySQL 5.5.31 procedure 的语法规则细节
  5. 数据库 -- 单表的数据查询
  6. BZOJ3714: [PA2014]Kuglarz 最小生成树
  7. 【远程重启】使用windows自带的shutdown命令远程重启服务器(测试不行,此文作废)...
  8. HDU 5908 Abelian Period 可以直接用multiset
  9. iOS之UI--CAShapeLayer
  10. 常见前端开发的题目,可能对你有用