SharedPreferences 是 Android 系统提供的一种数据持久化的方式,用于保存简单的数据。
SharedPreferences 是一个接口,实现类是 SharedPreferencesImpl。使用 synchronized 对象锁保证线程安全,非进程安全
。
源码基于 sdk-30/11.0/R.
获取 SharedPreferences 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @Override public SharedPreferences getSharedPreferences (String name, int mode) { 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); 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) { sp.startReloadIfChangedUnexpectedly(); } return sp; } private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;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; }
系统缓存了所有应用的 SP 对象。在同一个进程中,同一个文件只有一个 SP 实例,和 xml 文件一一对应;
如果获取 SP 对象时 mode 指定了 Context.MODE_MULTI_PROCESS,会先重新加载 xml 文件,再获取值。
SharedPreferencesImpl 初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false ; mMap = null ; mThrowable = null ; startLoadFromDisk(); } void startReloadIfChangedUnexpectedly () { synchronized (mLock) { if (!hasFileChangedUnexpectedly()) { return ; } startLoadFromDisk(); } } private boolean hasFileChangedUnexpectedly () { synchronized (mLock) { if (mDiskWritesInFlight > 0 ) { if (DEBUG) Log.d(TAG, "disk write in flight, not unexpected." ); return false ; } } final StructStat stat; try { BlockGuard.getThreadPolicy().onReadFromDisk(); stat = Os.stat(mFile.getPath()); } catch (ErrnoException e) { return true ; } synchronized (mLock) { return !stat.st_mtim.equals(mStatTimestamp) || mStatSize != stat.st_size; } } private void startLoadFromDisk () { synchronized (mLock) { mLoaded = false ; } new Thread ("SharedPreferencesImpl-load" ) { public void run () { loadFromDisk(); } }.start(); } private void loadFromDisk () { synchronized (mLock) { if (mLoaded) { return ; } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } if (mFile.exists() && !mFile.canRead()) { Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission" ); } Map<String, Object> map = null ; if (mFile.canRead()) { BufferedInputStream str = null ; try { str = new BufferedInputStream ( new FileInputStream (mFile), 16 * 1024 ); map = (Map<String, Object>) XmlUtils.readMapXml(str); } catch (Exception e) { Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e); } finally { IoUtils.closeQuietly(str); } } synchronized (mLock) { mLoaded = true ; try { if (thrown == null ) { if (map != null ) { mMap = map; } else { mMap = new HashMap <>(); } } } catch (Throwable t) { mThrowable = t; } finally { mLock.notifyAll(); } } }
SharedPreferences 初始化时开启线程加载本地 xml 文件,如果文件较大,比较耗时,可能主线程读取值时因为获取不到锁,一直等待,造成卡顿或 ANR。
保存数据 1 2 3 SharedPreferences preferences = getSharedPreferences("sp_name" , Context.MODE_PRIVATE);preferences.edit().putString("key" , "value" ).commit(); preferences.edit().putString("key" , "value" ).apply();
提交数据的两种方式:
**commit()**:同步提交,会阻塞调用它的线程,并且这个方法会返回 boolean 值告知保存是否成功。
**apply()**:异步提交,无返回值,使用 HandlerThread 异步保存到文件;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public Editor edit () { synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl (); } public final class EditorImpl implements Editor { private final Map<String, Object> mModified = new HashMap <>(); @Override public Editor putString (String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this ; } } }
edit()
时会创建 EditorImpl 对象,包含 mModified 的 Map 集合,用来保存修改的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public final class EditorImpl implements Editor { @Override public boolean commit () { MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this .enqueueDiskWrite(mcr, null ); mcr.writtenToDiskLatch.await(); notifyListeners(mcr); return mcr.writeToDiskResult; } @Override public void apply () { final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable () { @Override public void run () { mcr.writtenToDiskLatch.await(); } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable () { @Override public void run () { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this .enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); } }
同步提交时,先提交到内存,接着在调用者线程执行写文件任务;
异步提交时,先提交到内存,把任务添加到队列,使用 HandlerThread 串行执行写文件任务;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 private MemoryCommitResult commitToMemory () { Map<String, Object> mapToWriteToDisk; synchronized (SharedPreferencesImpl.this .mLock) { synchronized (mEditorLock) { mapToWriteToDisk = mMap; if (mClear) { if (!mapToWriteToDisk.isEmpty()) { mapToWriteToDisk.clear(); } } for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); 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); } } mModified.clear(); } } return new MemoryCommitResult (memoryStateGeneration, keysCleared, keysModified, listeners, mapToWriteToDisk); }
根据修改的集合,更新老集合。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 private void enqueueDiskWrite (final MemoryCommitResult mcr, final Runnable postWriteRunnable) { final boolean isFromSyncCommit = (postWriteRunnable == null ); final Runnable writeToDiskRunnable = new Runnable () { @Override public void run () { synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); } synchronized (mLock) { mDiskWritesInFlight--; } if (postWriteRunnable != null ) { postWriteRunnable.run(); } } }; if (isFromSyncCommit) { boolean wasEmpty = false ; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1 ; } if (wasEmpty) { writeToDiskRunnable.run(); return ; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); } private void writeToFile (MemoryCommitResult mcr, boolean isFromSyncCommit) { boolean fileExists = mFile.exists(); if (fileExists) { boolean backupFileExists = mBackupFile.exists(); if (!backupFileExists) { if (!mFile.renameTo(mBackupFile)) { mcr.setDiskWriteResult(false , false ); return ; } } else { mFile.delete(); } } FileOutputStream str = createFileOutputStream(mFile); XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); str.close(); final StructStat stat = Os.stat(mFile.getPath()); synchronized (mLock) { mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } mBackupFile.delete(); }
写入文件时,如果备份文件不存在,先把备份文件;
将内存中的数据保存到文件;
重新计算时间戳和文件大小,删除备份文件;
当一个进程正处于写文件的过程中的时候,如果另一个进程读文件,才会看到 mBackupFile, 这时候读进程会将 mBackupFile 重命名为 mFile, 这样读结果是,读进程只能都到修改前的文件,同时,由于 mBackupFile 重命名为了 mFile, 所以写进程写那个文件就没有文件名引用了,因此其写入的内容无法再被任何进程访问到。所以其内容丢失了,可认为写入失败了,而 SharedPreferences 对这种失败情况没有任何重试机制,所以就可能出现数据丢失的情况。
MODE_MULTI_PROCESS 模式下,每次获取 SharedPreferences 都会检测文件是否改变,只要读的时候另一进程在写,就会导致写丢失。
获取数据 1 2 SharedPreferences preferences = getSharedPreferences("sp_name" , Context.MODE_PRIVATE);String result = preferences.getString("key" , null );
1 2 3 4 5 6 7 8 @Override public String getString (String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
多进程问题 如果 SP 对象的 mode 指定了 Context.MODE_MULTI_PROCESS,那么当前进程会先从 xml 文件中重新加载,再读取值。
如果前一个进程由于数据比较大,写入文件比较耗时,当前的进程读取发生在前一个进程写成功之前,即使当前进程指定了 MODE_MULTI_PROCESS 标记,也是无法读取到值的。所以 SharedPreferences 是非进程安全的
,而且 MODE_MULTI_PROCESS 标记已经废弃,推荐使用 ContentProvider 等支持跨进程的方式。
1 2 3 4 5 6 7 8 9 10 @Override public SharedPreferences getSharedPreferences (File file, int mode) { if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { sp.startReloadIfChangedUnexpectedly(); } return sp; }
多进程访问主要有两种情况:
不同 apk(不同 packageName,且不具有相同 uid)的多进程: 由于 linux 文件权限是根据 uid 设置访问权限,因此必须设置 mode 为 MODE_WORLD_READABLE 或 MODE_WORLD_WRITABLE,取决于别的应用是否有需要该 SharedPreferences,由于这种方式需要修改文件权限,且会让所有人都具访问权限,无法只对某个应用授权,所以非常危险,android N 上对 targetsdk >= 24(7.0) 的应用已明确禁止这两个 mode。
同一个 packageName 或具有相同 uid 的 package 里面存在多个进程: 这种情况下多个进程具有相同的 uid,因此不需要修改文件权限,没有权限安全性问题。目前很多 apk 都支持多进程,例如后台服务与前台页面运行在独立的进程。
总结
SharedPreferences 每次写入操作,首先是将源文件备份;
写入文件成功后才删除备份文件。通过 HandlerThread 写文件;
如果写入过程中进程被杀或关机等非正常情况发生,进程再次启动后如果发现该 SharedPreferences 存在 Backup 文件,就将 Backup 文件重名为源文件,原本未完成写入的文件就直接丢弃,这样就能保证之前数据的正确。
参考 [1] Android 之不要滥用 SharedPreferences(上) [2] Android 之不要滥用 SharedPreferences(下) [3] Tencent/MMKV [4] SharedPreferences 多进程问题