当前位置:   article > 正文

SharePreference原理

sharepreference

SharedPreferences是Android提供的数据持久化的一种手段,适合单进程、小批量的数据存储与访问。因为SharedPreferences的实现是基于单个xml文件实现的,并且,所有持久化数据都是一次性加载到内存,如果数据过大,是不合适采用SharedPreferences存放的。而适用的场景是单进程的原因同样如此,由于Android原生的文件访问并不支持多进程互斥,所以SharePreferences也不支持,如果多个进程更新同一个xml文件,就可能存在同不互斥问题。

SharedPreferences的实现原理之:持久化数据的加载

首先,从基本使用简单看下SharedPreferences的实现原理:

  1. mSharedPreferences = context.getSharedPreferences("test", Context.MODE_PRIVATE);
  2. SharedPreferences.Editor editor = mSharedPreferences.edit();
  3. editor.putString(key, value);
  4. editor.apply();

context.getSharedPreferences其实就是简单的调用ContextImpl的getSharedPreferences,具体实现如下:

  1. @Override
  2. public SharedPreferences getSharedPreferences(String name, int mode) {
  3. SharedPreferencesImpl sp;
  4. synchronized (ContextImpl.class) {
  5. if (sSharedPrefs == null) {
  6. sSharedPrefs = new ArrayMap<String, ArrayMap<String, SharedPreferencesImpl>>();
  7. }
  8. final String packageName = getPackageName();
  9. ArrayMap<String, SharedPreferencesImpl> packagePrefs = sSharedPrefs.get(packageName);
  10. if (packagePrefs == null) {
  11. packagePrefs = new ArrayMap<String, SharedPreferencesImpl>();
  12. sSharedPrefs.put(packageName, packagePrefs);
  13. }
  14. sp = packagePrefs.get(name);
  15. if (sp == null) {
  16. <!--读取文件-->
  17. File prefsFile = getSharedPrefsFile(name);
  18. sp = new SharedPreferencesImpl(prefsFile, mode);
  19. <!--缓存sp对象-->
  20. packagePrefs.put(name, sp);
  21. return sp;
  22. }
  23. }
  24. <!--跨进程同步问题-->
  25. if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
  26. getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
  27. sp.startReloadIfChangedUnexpectedly();
  28. }
  29. return sp;
  30. }

以上代码非常简单,直接描述下来就是先去内存中查询与xml对应的SharePreferences是否已经被创建加载,如果没有那么该创建就创建,该加载就加载,在加载之后,要将所有的key-value保存到内幕才能中去,当然,如果首次访问,可能连xml文件都不存在,那么还需要创建xml文件,与SharePreferences对应的xml文件位置一般都在/data/data/包名/shared_prefs目录下,后缀一定是.xml,数据存储样式如下:

这里面数据的加载的地方需要看下,比如,SharePreferences数据的加载是同步还是异步?数据加载是new SharedPreferencesImpl对象时候开始的,

  1. SharedPreferencesImpl(File file, int mode) {
  2. mFile = file;
  3. mBackupFile = makeBackupFile(file);
  4. mMode = mode;
  5. mLoaded = false;
  6. mMap = null;
  7. startLoadFromDisk();
  8. }

startLoadFromDisk很简单,就是读取xml配置,如果其他线程想要在读取之前就是用的话,就会被阻塞,一直wait等待,直到数据读取完成。

  1. private void loadFromDiskLocked() {
  2. ...
  3. Map map = null;
  4. StructStat stat = null;
  5. try {
  6. stat = Os.stat(mFile.getPath());
  7. if (mFile.canRead()) {
  8. BufferedInputStream str = null;
  9. try {
  10. <!--读取xml中配置-->
  11. str = new BufferedInputStream(
  12. new FileInputStream(mFile), 16*1024);
  13. map = XmlUtils.readMapXml(str);
  14. }...
  15. mLoaded = true;
  16. ...
  17. <!--唤起其他等待线程-->
  18. notifyAll();
  19. }

可以看到其实就是直接使用xml解析工具XmlUtils,直接在当前线程读取xml文件,所以,如果xml文件稍大,尽量不要在主线程读取,读取完成之后,xml中的配置项都会被加载到内存,再次访问的时候,其实访问的是内存缓存。

SharedPreferences的实现原理之:持久化数据的更新

通常更新SharedPreferences的时候是首先获取一个SharedPreferences.Editor,利用它缓存一批操作,之后当做事务提交,有点类似于数据库的批量更新:

  1. SharedPreferences.Editor editor = mSharedPreferences.edit();
  2. editor.putString(key1, value1);
  3. editor.putString(key2, value2);
  4. editor.putString(key3, value3);
  5. editor.apply();//或者commit

Editor是一个接口,这里的实现是一个EditorImpl对象,它首先批量预处理更新操作,之后再提交更新,在提交事务的时候有两种方式,一种是apply,另一种commit,两者的区别在于:何时将数据持久化到xml文件,前者是异步的,后者是同步的。Google推荐使用前一种,因为,就单进程而言,只要保证内存缓存正确就能保证运行时数据的正确性,而持久化,不必太及时,这种手段在Android中使用还是很常见的,比如权限的更新也是这样,况且,Google并不希望SharePreferences用于多进程,因为不安全,手下卡一下apply与commit的区别

  1. public void apply() {
  2. <!--添加到内存-->
  3. final MemoryCommitResult mcr = commitToMemory();
  4. final Runnable awaitCommit = new Runnable() {
  5. public void run() {
  6. try {
  7. mcr.writtenToDiskLatch.await();
  8. } catch (InterruptedException ignored) {
  9. }
  10. }
  11. };
  12. QueuedWork.add(awaitCommit);
  13. Runnable postWriteRunnable = new Runnable() {
  14. public void run() {
  15. awaitCommit.run();
  16. QueuedWork.remove(awaitCommit);
  17. }
  18. };
  19. <!--延迟写入到xml文件-->
  20. SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  21. <!--通知数据变化-->
  22. notifyListeners(mcr);
  23. }
  24. public boolean commit() {
  25. MemoryCommitResult mcr = commitToMemory();
  26. SharedPreferencesImpl.this.enqueueDiskWrite(
  27. mcr, null /* sync write on this thread okay */);
  28. try {
  29. mcr.writtenToDiskLatch.await();
  30. } catch (InterruptedException e) {
  31. return false;
  32. }
  33. notifyListeners(mcr);
  34. return mcr.writeToDiskResult;
  35. }

从上面可以看出两者最后都是先调用commitToMemory,将更改提交到内存,在这一点上两者是一致的,之后又都调用了enqueueDiskWrite进行数据持久化任务,不过commit函数一般会在当前线程直接写文件,而apply则提交一个事务到已给线程池,之后直接返回,实现如下:

  1. private void enqueueDiskWrite(final MemoryCommitResult mcr,
  2. final Runnable postWriteRunnable) {
  3. final Runnable writeToDiskRunnable = new Runnable() {
  4. public void run() {
  5. synchronized (mWritingToDiskLock) {
  6. writeToFile(mcr);
  7. }
  8. synchronized (SharedPreferencesImpl.this) {
  9. mDiskWritesInFlight--;
  10. }
  11. if (postWriteRunnable != null) {
  12. postWriteRunnable.run();
  13. }
  14. }
  15. };
  16. final boolean isFromSyncCommit = (postWriteRunnable == null);
  17. if (isFromSyncCommit) {
  18. boolean wasEmpty = false;
  19. synchronized (SharedPreferencesImpl.this) {
  20. wasEmpty = mDiskWritesInFlight == 1;
  21. }
  22. <!--如果没有其他线程在写文件,直接在当前线程执行-->
  23. if (wasEmpty) {
  24. writeToDiskRunnable.run();
  25. return;
  26. }
  27. }
  28. QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
  29. }

不过如果有线程在写文件,那么就不能直接写,这个时候就跟apply函数一致了,但是,如果直观说两者的区别的话,直接说commit同步,而apply异步应该也是没有多大问题的

SharePreferences多进程使用问题

SharePreferences在新建的有个mode参数,可以指定它的加载模式,MODE_MULTI_PROCESS是Google提供的一个多进程模式,但是这种模式并不是我们说的支持多进程同步更新等,它的作用只会在getSharedPreferences的时候,才会重新从xml重加载,如果我们在一个进程中更新xml,但是没有通知另一个进程,那么另一个进程的SharePreferences是不会自动更新的。

  1. @Override
  2. public SharedPreferences getSharedPreferences(String name, int mode) {
  3. SharedPreferencesImpl sp;
  4. ...
  5. if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
  6. getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
  7. // If somebody else (some other process) changed the prefs
  8. // file behind our back, we reload it. This has been the
  9. // historical (if undocumented) behavior.
  10. sp.startReloadIfChangedUnexpectedly();
  11. }
  12. return sp;
  13. }

也就是说MODE_MULTI_PROCESS只是个鸡肋Flag,对于多进程的支持几乎为0,下面是Google文档,简而言之,就是:不要用

MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as ContentProvider。

响应的Google为多进程提供了一个数据同步互斥方案,那就是基于Binder实现的ContentProvider,关于ContentProvider后文分析。

SharedPreferences的mode解释

Context.MODE_PRIVATE:只能被本应用程序读写

Context.MODE_WORLD_READBALE:能被其他程序读取、但是不能写

Context.MODE_WORLD_WRITEABLE:能被其他程序读、写

Context.MODE_APPEND:如果文件已存在,然后将数据写入现有文件的末尾,而不是擦除它。

Context.MODE_MULTI_PROCESS:弃用,无法可靠工作在某些版本的Android,而且不提供任何

协调跨进程。应用程序不应该尝试使用它

Context.MODE_ENABLE_WRITE_AHEAD_LOGGING:设置后,数据库将以预写方式打开,默认情况下启用日志记录

Context.MODE_NO_LOCALIZED_COLLATORS:设置后,打开数据库时不支持本地化拼贴器。

总结

1. SharePreferences是Android基于xml实现的一种数据持久化手段。

2. sp不适合存储过大的数据,因为它一直保存在内存中,数据过大容易造成内存溢出。

3. SharePreferences的commit与apply一个是同步一个是异步(大部分场景下),sp的commit方法是直接在当前线程执行文件写入      操作,而apply方法是在工作线程执行文件写入,尽可能使用apply,因为不会阻塞当前线程。

4. sp并不支持跨进程,因为它不能保证更新本地数据后被另一个进程所知道,而且跨进程的操作标记已经被弃用。

5. sp批量更改数据时,只需要保留最后一个apply即可,避免添加多余的写文件任务。

6. 每个sp存储的键值对不宜过多,否则在加载文件数据到内存时会耗时过长,而阻塞sp的相关getput方法,造成ui卡顿。

7. 频繁更改的配置项和不常更改的配置项应该分开为不同的sp存放,避免不必要的io操作。

转载地址:SharePreference原理及跨进程数据共享的问题 - 简书

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/308771
推荐阅读
相关标签
  

闽ICP备14008679号