概述 注:本文基于Android 9源码,为了文章的简洁性,引用源码的地方可能有所删减。
SharedPreference 是 Android 提供的一种简单易用的轻量级存储方式,本质是一个以 key-value 方式保存数据的xml文件,存储路径为: /data/data/$pkg/shared_prefs
。但是 SharedPreference 存在着一些缺陷,如存在多进程的安全问题,以及 ANR(即使 apply 也可能会 ANR) 等。目前可供替换的方案有腾讯的第三方库–MMKV ,以及官方的 Jetpack DataStore 组件。
创建实例 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 public SharedPreferences getSharedPreferences (String name, int mode) { 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); } public File getSharedPreferencesPath (String name) { return makeFilename(getPreferencesDir(), name + ".xml" ); } 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 ) { sp = new SharedPreferencesImpl(file, mode); cache.put(file, sp); return sp; } } return sp; }
SharedPreferences 只是一个接口,具体实现是 SharedPreferencesImpl, 我们看看 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 SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false ; mMap = null ; mThrowable = null ; startLoadFromDisk(); } 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); } } Map<String, Object> map = null ; StructStat stat = null ; Throwable thrown = null ; try { stat = Os.stat(mFile.getPath()); } catch (ErrnoException e) { } catch (Throwable t) { thrown = t; } synchronized (mLock) { mLoaded = true ; mThrowable = thrown; try { } catch (Throwable t) { mThrowable = t; } finally { mLock.notifyAll(); } } }
SP 实例化时会将 xml 文件中的数据放入内存中的 mMap 中,这样以后的每次读数据都是从这个 Map 中读取,而不需要每次都读文件。然而当 xml 中数据量比较大时,可能会产生高内存占用 的风险,然而实际上,如果 SP 中保存的只是一些基础类型数据如 int, boolean 等,则很难有这么大的数据量,而如果存储的是一些复杂数据序列化后的字符串,当然容易占用过高的内存,但这也违背了 SharedPreferences 设计的初衷 – 轻量级存储,这种大容量的数据本身就不应该用 SP 来持久化!
读操作 以 getString 为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public String getString (String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } } private void awaitLoadedLocked () { while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } if (mThrowable != null ) { throw new IllegalStateException(mThrowable); } }
awaitLoadedLocked 方法作用是在同步块中等待 SP 加载完成,那么如果 xml 文件很大,加载 SP 比较慢,那么 getXXX 方法就会一直阻塞当前线程!
写操作 首先看下 SP.edit 方法:
1 2 3 4 5 6 public Editor edit () { synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
然后看下 EditorImpl.putString/remove/clear 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public Editor putString (String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this ; } } @Override public Editor remove (String key) { synchronized (mEditorLock) { mModified.put(key, this ); return this ; } } @Override public Editor clear () { synchronized (mEditorLock) { mClear = true ; return this ; } }
commitToMemory 在看 apply 和 commit 方法之前先看看 commitToMemory 方法,这个方法加了两级锁: SharedPreferencesImpl.mLock 和 EditorImpl.mEditorLock 锁,因此在 commit 或 apply 时任何 getXXX 方法都会 block。
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 private MemoryCommitResult commitToMemory () { synchronized (SharedPreferencesImpl.this .mLock) { if (mDiskWritesInFlight > 0 ) { mMap = 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(); 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); }
apply 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void apply () { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run () { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run () { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this .enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }
commit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean commit () { long startTime = 0 ; MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this .enqueueDiskWrite(mcr, null ); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false ; } finally { } notifyListeners(mcr); return mcr.writeToDiskResult; }
enqueueDiskWrite 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 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(); } } try { FileOutputStream str = createFileOutputStream(mFile); if (str == null ) { mcr.setDiskWriteResult(false , false ); return ; } return ; } catch (XmlPullParserException e) { } catch (IOException e) { } mcr.setDiskWriteResult(false , false ); } void setDiskWriteResult (boolean wasWritten, boolean result) { this .wasWritten = wasWritten; writeToDiskResult = result; writtenToDiskLatch.countDown(); }
小结 当每次调用 putXXX 时,都是将数据存入到内存里的 Map 中,等到调用 apply 或 commit 的时候,才会将 Map 中的数据持久化到 xml 中。另外,如果开发者只调用 putXXX 方法往 Map 中存入数据,而没有调用 apply/commit 方法持久化,为了防止 Map 中的数据被 getXXX 方法获取,设计者在 Editor 类中也使用了一个 Map 对象来存储这些 putXXX 存入的值,直到 apply/commit 被调用后,才将 Editor 中的 Map 合入 SP 中的 Map, 然后持久化。
当由于一些原因导致 SP 的写操作异常终止时,xml 文件可能已经被破坏了,因此 SP 采用了文件备份机制来处理这种情况:SharedPreferences 写操作执行之前会对文件进行备份(.bak),当写操作执行成功后会删除这个 bak 文件,反之若发生异常,则在下次实例化 SP 时会检查是否存在 bak 文件,若存在则直接将备份文件重命名为原文件。
线程安全 为了保证线程安全,SharedPreferences 一共使用了三把锁:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 final class SharedPreferencesImpl implements SharedPreferences { private final Object mLock = new Object(); private final Object mWritingToDiskLock = new Object(); @GuardedBy("mLock") private Map<String, Object> mMap; @GuardedBy("mWritingToDiskLock") private long mDiskStateGeneration; public final class EditorImpl implements Editor { private final Object mEditorLock = new Object(); @GuardedBy("mEditorLock") private final Map<String, Object> mModified = new HashMap<>(); } }
读操作的锁:
读操作的原理是读取内存中 mMap 的值并返回,因此只需要加一把锁保证 mMap 的线程安全即可:
1 2 3 4 5 6 7 public String getString (String key, @Nullable String defValue) { synchronized (mLock) { awaitLoadedLocked(); String v = (String)mMap.get(key); return v != null ? v : defValue; } }
写操作的锁:
首先对于 Editor 的 put 操作而言,需要一把锁来保证线程安全:
1 2 3 4 5 6 public Editor putString (String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this ; } }
然后在执行 apply 时,需要加锁保证 mEditorMap 和 mMap 合并的安全:
1 2 3 4 5 6 7 8 private MemoryCommitResult commitToMemory () { synchronized (SharedPreferencesImpl.this .mLock) { synchronized (mEditorLock) { } } }
最后将 mMap 写入 xml 文件也需要加锁保证安全:
1 2 3 4 5 6 private void enqueueDiskWrite (final MemoryCommitResult mcr, final Runnable postWriteRunnable) { synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); } }
进程安全 SharedPreferences 是进程不安全的,对于如何保证 SharedPreferences 的进程安全性,可以通过一些方法:
文件锁:java.nio.channels.FileLock 可以获取文件指定部分的锁(独占或共享)。读文件时使用共享锁,写文件时使用独占锁。参考 https://blog.csdn.net/qq_27512671/article/details/101445642 ;
使用 ContentProvider 实现 SharedPreferences 跨进程共享数据,可以用 ContentProvider 的 update 或 insert 方法实现 putXXX 方法,用 delete 实现 clean 和 remove 方法,用 getType 或者 query 实现 get 和 getAll 方法;
等等
apply引起的ANR ANR产生原因 在阅读 SharedPreferences apply 写操作时,有以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public void apply () { final long startTime = System.currentTimeMillis(); final MemoryCommitResult mcr = commitToMemory(); final Runnable awaitCommit = new Runnable() { @Override public void run () { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.addFinisher(awaitCommit); Runnable postWriteRunnable = new Runnable() { @Override public void run () { awaitCommit.run(); QueuedWork.removeFinisher(awaitCommit); } }; SharedPreferencesImpl.this .enqueueDiskWrite(mcr, postWriteRunnable); notifyListeners(mcr); }
这里将 awaitCommit 添加到了 QueuedWork 的 Finishers 中,然后在 enqueueDiskWrite 方法中有如下代码(只留下关键代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void enqueueDiskWrite (final MemoryCommitResult mcr, final Runnable postWriteRunnable) { final Runnable writeToDiskRunnable = new Runnable() { @Override public void run () { synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); } if (postWriteRunnable != null ) { postWriteRunnable.run(); } } }; QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
然后我们看一下 QueuedWork 在 queue 后是怎么工作的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class QueuedWork { private static final long DELAY = 100 ; private static boolean sCanDelay = true ; public static void queue (Runnable work, boolean shouldDelay) { Handler handler = getHandler(); synchronized (sLock) { sWork.add(work); if (shouldDelay && sCanDelay) { handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY); } else { handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN); } } } }
queued-work-looper 线程 handler 接收到 MSG_RUN 后会调用 processPendingWork 方法来依次执行 sWork 中的任务。按照这个逻辑,handler处理了消息后,执行 writeToFile 写入操作完成后会释放 writtenToDiskLatch 锁,然后调用 writtenToDiskLatch.await
的等待操作不会阻塞当前线程(queued-work-looper),也不会产生 ANR。
注意到上面 QueuedWork.addFinisher(awaitCommit)
将 awaitCommit 加入了 Finishers 列表,那么这个 Finisher 是什么时候会被执行呢?
1 2 3 4 5 public static void addFinisher (Runnable finisher) { synchronized (sLock) { sFinishers.add(finisher); } }
接着看 QueuedWork 的源码,发现 sFinishers 中的任务被执行是在 QueuedWork.waitToFinish 方法中,这个方法的注释中有如下片段: Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive, after Service command handling, etc. (so async work is never lost)
。即在上面这些场景中,会回调 waitToFinish 方法,我们看下它的源码:
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 public static void waitToFinish () { boolean hadMessages = false ; Handler handler = getHandler(); synchronized (sLock) { if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) { handler.removeMessages(QueuedWorkHandler.MSG_RUN); } sCanDelay = false ; } processPendingWork(); try { while (true ) { Runnable finisher; synchronized (sLock) { finisher = sFinishers.poll(); } if (finisher == null ) { break ; } finisher.run(); } } finally { sCanDelay = true ; } }
可以在 ActivityThread 中看到在 handleServiceArgs, handleStopService, handlePauseActivity, handleStopActivity, handleSleeping 中都有调用到 waitToFinish 方法,而这些方法都会分别调用到 Service.onStartCommand, Service.onDestroy, Activity.onPause, Activity.onStop 等生命周期。再根据 waitToFinish 方法的其它注释,大概可以猜到这样设计的原因是当 APP 发生崩溃等异常时,尽可能保证数据持久化成功 , waitToFinish 方法会在当前线程立即执行 sWork 中的任务,然后依次执行 sFinishers 中的任务。即在主线程会执行 waitToFinish 方法,自然会有产生 ANR 的可能!
避免ANR 从上面 ANR 的原因分析可以知道,此类 ANR 都是在主线程调用 QueuedWork.waitToFinish() 触发的,因此可以在调用此函数之前,将 QueuedWork 中的 sFinishers 列表清空。
结合 Android-Activity启动源码解读 , Android-Service启动源码解读 , Android-Broadcast机制原理 , Android-ContentProvider源码解读 可以知道,上面 handlePauseActivity, handleStopActivity 等都是通过 ClientTransaction机制 调用的,可以从这里出发,Hook 相关的逻辑,在调用 handlePauseActivity 等方法之前清除 sFinishers 队列。剖析SharedPreference apply引起的ANR问题-字节跳动技术团队 上的解决方法在 Android 9 上已经过时了,Android 9 上 AMS 回调 Activity 生命周期不再通过 H 类来直接调用。
示例如下:
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 fun hook () { val atCls = Class.forName("android.app.ActivityThread" ) atCls.getDeclaredMethod("currentActivityThread" )?.let { currentActivityThread -> currentActivityThread.invoke(null )?.let { at -> val mH = atCls.getDeclaredField("mH" ) mH.isAccessible = true val cb = Handler::class .java.getDeclaredField("mCallback" ) cb.isAccessible = true cb.set (mH.get (at), object : Handler.Callback { override fun handleMessage (msg: Message ) : Boolean { when (msg.what) { 159 -> { handler(msg.obj) } } return false } }) } } } private fun handler (transaction: Any ) { val transCls = Class.forName("android.app.servertransaction.ClientTransaction" ) val lifecycleStateRequestM = transCls.getDeclaredMethod("getLifecycleStateRequest" ) val lifecycleStateRequest = lifecycleStateRequestM.invoke(transaction) val pauseActivityItem = Class.forName("android.app.servertransaction.PauseActivityItem" ) if (pauseActivityItem.isAssignableFrom(lifecycleStateRequest.javaClass)) { } }
总结 SharedPreferences 的坑:
getXXX 方法可能会导致主线程阻塞
不能保证类型安全(血泪…)
加载的数据一直存在内存中
apply 方法可能造成 ANR
跨进程不安全