当前位置:   article > 正文

SharePreference源码学习和多进程的场景

SharePreference源码学习和多进程的场景

复习了下SharePreference的使用,以及了解下SharePreference的源码实现,解决多进程情况下的SharePreference问题,做下笔记。

参考文章:

源码分析:

www.jianshu.com/p/8eb2147c3…

www.jianshu.com/p/3b2ac6201…

SharePreference的多进程解决方案:

juejin.im/entry/59083…

SharePreference

Android平台中一个轻量级的存储库,用来保存应用程序的各种配置信息。本质是一个以“key-value”键值对的方式保存数据的xml文件。

文件保存地址:在/data/data/package name/shared_prefs目录下就可以查看到这个文件了

简单使用例子

  1. //获取得到SharePreference,第一个参数是文件名称,第二个参数是操作模式
  2. //一般是MODE_PRIVATE模式,指定该SharedPreferences数据只能被本应用程序读、写
  3. SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE);
  4. //创建Editor对象
  5. SharedPreferences.Editor editor=sharedPreferences.edit();
  6. //保存数据
  7. editor.putString("name","donggua");
  8. //editor.commit();
  9. editor.apply();
  10. //读取数据
  11. String result=sharedPreferences.getString("name","默认值");
  12. 复制代码

commit和apply的区别

当使用commit去提交数据的时候,发现IDE提示让我们使用apply方法。

  • commit:同步提交,commit将同步的把数据写入磁盘和内存缓存,并且有返回值。
  • apply:异步提交,会把数据同步写入内存缓存,然后异步保存到磁盘,可能会失败,失败不会收到错误回调。

两者的区别:

  • commit的效率会比apply慢一点。在一个进程中,如果在不关心提交结果是否成功的情况下,优先考虑apply方法。
  • 都是原子性操作,但是原子的操作不同。commit的从数据提交到保存到内存后再保存到磁盘中,中间不可打断。而apply方法是将数据保存到内存后就可以返回了,异步执行保存到磁盘的操作,

源码分析

获取SharePreference对象

利用Context获取到SharePreference实例,ContextImpl是Context的实现类,实现了getSharedPreferences方法。

  • 因为SharedPreferences是支持自定义文件名的,所以这里利用了ArrayMap<File, SharedPreferencesImpl>来缓存不同文件对应的SharedPreferencesImpl对象。一个File文件对应一个SharePreference对象。
  • getSharedPreferencesCacheLocked(),获取缓存的ArrayMap<File, SharedPreferencesImpl>对象,没有则创建一个。
  1. @Override
  2. public SharedPreferences getSharedPreferences(File file, int mode) {
  3. SharedPreferencesImpl sp;
  4. synchronized (ContextImpl.class) {
  5. //获取缓存的map
  6. final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
  7. //拿到对应的文件的SharePreference
  8. sp = cache.get(file);
  9. if (sp == null) {
  10. checkMode(mode);
  11. if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {
  12. if (isCredentialProtectedStorage()
  13. && !getSystemService(UserManager.class)
  14. .isUserUnlockingOrUnlocked(UserHandle.myUserId())) {
  15. throw new IllegalStateException("SharedPreferences in credential encrypted "
  16. + "storage are not available until after user is unlocked");
  17. }
  18. }
  19. //没有缓存,创建SharedPreferencesImpl对象,并保存到缓存中
  20. sp = new SharedPreferencesImpl(file, mode);
  21. cache.put(file, sp);
  22. return sp;
  23. }
  24. }
  25. if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
  26. getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
  27. // If somebody else (some other process) changed the prefs
  28. // file behind our back, we reload it. This has been the
  29. // historical (if undocumented) behavior.
  30. sp.startReloadIfChangedUnexpectedly();
  31. }
  32. return sp;
  33. }
  34. @GuardedBy("ContextImpl.class")
  35. private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
  36. if (sSharedPrefsCache == null) {
  37. sSharedPrefsCache = new ArrayMap<>();
  38. }
  39. final String packageName = getPackageName();
  40. ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
  41. if (packagePrefs == null) {
  42. packagePrefs = new ArrayMap<>();
  43. sSharedPrefsCache.put(packageName, packagePrefs);
  44. }
  45. return packagePrefs;
  46. }
  47. 复制代码

SharedPreferencesImpl是SharedPreferences接口的实现类,实现了commit和apply方法。先看看SharedPreferencesImpl的构造方法。会异步调用一个startLoadFromDisk的方法,作用是从磁盘中把SharePreference文件里面保存的xml信息读取到内存中,并保存到Map里面。

  1. SharedPreferencesImpl(File file, int mode) {
  2. mFile = file;
  3. mBackupFile = makeBackupFile(file);
  4. mMode = mode;
  5. mLoaded = false;
  6. mMap = null;
  7. mThrowable = null;
  8. startLoadFromDisk();
  9. }
  10. private void startLoadFromDisk() {
  11. synchronized (mLock) {
  12. mLoaded = false;
  13. }
  14. new Thread("SharedPreferencesImpl-load") {
  15. public void run() {
  16. loadFromDisk();
  17. }
  18. }.start();
  19. }
  20. private void loadFromDisk(){
  21. //省略部分代码
  22. try {
  23. stat = Os.stat(mFile.getPath());
  24. if (mFile.canRead()) {
  25. BufferedInputStream str = null;
  26. try {
  27. str = new BufferedInputStream(
  28. new FileInputStream(mFile), 16 * 1024);
  29. //进行xml解析
  30. map = (Map<String, Object>) XmlUtils.readMapXml(str);
  31. } catch (Exception e) {
  32. Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
  33. } finally {
  34. IoUtils.closeQuietly(str);
  35. }
  36. }
  37. } catch (ErrnoException e) {
  38. // An errno exception means the stat failed. Treat as empty/non-existing by
  39. // ignoring.
  40. } catch (Throwable t) {
  41. thrown = t;
  42. }
  43. }
  44. //省略部分代码。
  45. //将解析结果保存的map进行赋值
  46. if (map != null) {
  47. mMap = map;
  48. mStatTimestamp = stat.st_mtim;
  49. mStatSize = stat.st_size;
  50. }
  51. 复制代码

读取数据

例如SharedPreferencesImpl的实现getString()方法,是直接从内存中的mMap直接就把数据读取出来,并没有涉及到磁盘操作。(恍然大悟,以前以为读取数据也要去读取file文件)

  1. @Override
  2. @Nullable
  3. public String getString(String key, @Nullable String defValue) {
  4. synchronized (mLock) {
  5. awaitLoadedLocked();
  6. String v = (String)mMap.get(key);
  7. return v != null ? v : defValue;
  8. }
  9. }
  10. 复制代码

保存数据

EditorImpl类实现了Editor接口。apply和commit都会调用commitToMemory方法,将数据保存到内存中,后面调用enqueueDiskWrite将数据保存到磁盘中。

  1. //临时缓存多个key的数据,后面提交数据的时候,就遍历这个map就行
  2. private final Map<String, Object> mModified = new HashMap<>();
  3. //CountDownLatch,等待直到保存到磁盘的操作完成。
  4. final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
  5. //在当前线程直接写文件,调用await,同步等待,最后返回操作的结果result
  6. @Override
  7. public boolean commit() {
  8. long startTime = 0;
  9. if (DEBUG) {
  10. startTime = System.currentTimeMillis();
  11. }
  12. //保存到内存中
  13. MemoryCommitResult mcr = commitToMemory();
  14. //保存到磁盘
  15. SharedPreferencesImpl.this.enqueueDiskWrite(
  16. mcr, null /* sync write on this thread okay */);
  17. try {
  18. mcr.writtenToDiskLatch.await();
  19. } catch (InterruptedException e) {
  20. return false;
  21. } finally {
  22. if (DEBUG) {
  23. Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
  24. + " committed after " + (System.currentTimeMillis() - startTime)
  25. + " ms");
  26. }
  27. }
  28. notifyListeners(mcr);
  29. return mcr.writeToDiskResult;
  30. }
  31. //异步等待保存操作,无法获取操作的结果
  32. @Override
  33. public void apply() {
  34. final long startTime = System.currentTimeMillis();
  35. //保存到内存中
  36. final MemoryCommitResult mcr = commitToMemory();
  37. final Runnable awaitCommit = new Runnable() {
  38. @Override
  39. public void run() {
  40. try {
  41. mcr.writtenToDiskLatch.await();
  42. } catch (InterruptedException ignored) {
  43. }
  44. if (DEBUG && mcr.wasWritten) {
  45. Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
  46. + " applied after " + (System.currentTimeMillis() - startTime)
  47. + " ms");
  48. }
  49. }
  50. };
  51. QueuedWork.addFinisher(awaitCommit);
  52. Runnable postWriteRunnable = new Runnable() {
  53. @Override
  54. public void run() {
  55. awaitCommit.run();
  56. QueuedWork.removeFinisher(awaitCommit);
  57. }
  58. };
  59. SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
  60. // Okay to notify the listeners before it's hit disk
  61. // because the listeners should always get the same
  62. // SharedPreferences instance back, which has the
  63. // changes reflected in memory.
  64. notifyListeners(mcr);
  65. }
  66. 复制代码

多进程中的SharePreference

上面讲的默认是单进程中的SharePreference,读取操作是直接从内存中的Map读取的,不涉及IO操作。如果是在多进程中的话,不同进程之间的内存并不是共享的,这个时候读写同一个SharePreference就会出现问题了。比如多个进程对同一个sharedpreference进行修改,总会有一个进程获取到的结果不是实时修改后的结果。

解决方法:推荐使用ContentProvider来处理多进程间的文件共享。

ContentProvider的特点:

  • ContentProvider内部的同步机制会防止多个进程同时访问,避免数据冲突。
  • ContentProvider的数据源,并不是只能选择数据库,其实核心操作就在update()和query()这两个操作,里面操作存取的数据源其实可以根据我们需要,替换成文件,也可以换成SharedPreferences。

所以我们可以使用ContentProvider做了一下中间媒介,让它帮我们实现多进程同步机制,里面操作的数据改成SharedPreferences来实现。这样的话就可以实现了跨进程访问SharePreference。

下面简单地写一个demo,读取的时候只需要传进相应的uri就行了。比如下面的代码,path字段的第二个是fileName,第三个是key值。

  1. public class MultiProcessSharePreference extends ContentProvider{
  2. @Nullable
  3. @Override
  4. public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
  5. //获取xml的文件名,默认取path字段的第一个
  6. Log.d(TAG, "query: uri:" + uri);
  7. String tableName = uri.getPathSegments().get(0);
  8. String name = uri.getPathSegments().get(1);
  9. String key = uri.getPathSegments().get(2);
  10. Log.d(TAG, "query: tableName:" + tableName);
  11. Log.d(TAG, "query: fileName:" + name);
  12. Log.d(TAG, "query: key:" + key);
  13. //创建sharedPreferences对象
  14. SharedPreferences sharedPreferences = getContext().getSharedPreferences(name, Context.MODE_PRIVATE);
  15. //创建一个cursor对象
  16. MatrixCursor cursor = null;
  17. switch (uriMatcher.match(uri)) {
  18. case CODE_PREFERENCE_STRING:
  19. String value = sharedPreferences.getString(key, "默认值");
  20. cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);
  21. MatrixCursor.RowBuilder rowBuilder = cursor.newRow();
  22. rowBuilder.add(value);
  23. break;
  24. default:
  25. Log.d(TAG, "query: Uri No Match");
  26. }
  27. return cursor;
  28. }
  29. }
  30. 复制代码
  • MatrixCursor: 如果需要一个cursor而没有一个现成的cursor的话,那么可以使用MatrixCursor实现一个虚拟的表。MatrixCursor.RowBuilder是用来添加Row数据的,通过rowBuilder的add方法,就可以把数值添加到行里面了。使用场景:比如ContentProvider的query方法是返回一个cursor类型的数据,而数据源用的是SharePreference,这个时候就可以利用MatrixCursor。MartixCursor本质上是用一个一位数据来模拟一个二维数据,根据行值和列值就可以找到对应的数据了。

MatrixCursor的源码解析:blog.csdn.net/zhang_jun_l…

  1. //定义每一列的字段名字
  2. public static final String COLUMN_VALUE = "value";
  3. //创建一个字符数组,字符数组的值对应着表的字段
  4. private static String[] PREFERENCE_COLUMNS = {COLUMN_VALUE};
  5. //构造一个MatrixCursor对象
  6. MatrixCursor cursor = new MatrixCursor(PREFERENCE_COLUMNS, 1);
  7. //通过matrixCursor的addRow方法添加一行值
  8. MatrixCursor.RowBuilder rowBuilder = cursor.newRow();
  9. rowBuilder.add(value);
  10. 复制代码
  • 优化一下的思路:
    • 在ContentProvider里面加一个HashMap<String,SharePreference>进行一下缓存,key值是文件名,value是对应的SharePreference对象,这样的话,就不用每次都去加载SharePreference对象了。
    • 在ContentProvider里面实现回调listener,在key值有变化的时候,进行通知订阅者。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Cpp五条/article/detail/308802
推荐阅读
  

闽ICP备14008679号