SharedPreferences
是系统提供的一种简易数据持久化的手段,适合单进程、小批量 的数据存储与访问。以键值对的形式存储在xml
文件中。 文件存储路径为data/data/package_name/shared_prefs/
目录。
源码解析
获取SharedPerferences对象
获取方法从getSharedPreferences(name,mode)
开始,此时就需要去加载对应name的xml
文件
1 2 3 4 5 6 7 8 class MainActivity : AppCompatActivity() { lateinit var sharedPreferences: SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) sharedPreferences = getSharedPreferences("test", MODE_PRIVATE); } }
test
表示生成的xml文件名为test.xml
mode
对应的是xml文件的访问权限以及数据的写入方式
权限控制格式
作用
备注
Context.MODE_PRIVATE
代表该文件是私有数据,只能被当前应用访问。 写入的内容会覆盖源文件的内容。
默认操作模式
Context.MODE_WORLD_READABLE
表示当前文件可以被其他应用读取
Context.MODE_WORLE_WRITEABLE
表示当前文件可以被其他应用写入
Context.MODE_APPEND
会检查当前是否有文件存在? 在在后面追加内容 不存在则去创建新文件
Context.MODE_MULTI_PROCESS
部分支持跨进程使用
原理就是重新读取xml文件内容
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 private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;private ArrayMap<String, File> mSharedPrefsPaths; 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); } @Override 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 ) { 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; }
主要执行了三步:
根据传入的name
在对应路径下生成对应的xml文件,并存入mSharedPrefsPaths
进行缓存。
创建文件完毕后,再去创建对应的SharedPreferencesImpl
对象,创建完成后缓存到cache
中。每一个xml文件都会对应一个SP对象
若设置了mode
为Context.MODE_MULTI_PROCESS
,就需要重新去加载一次xml
文件。
初始化 SP对象最后都是由SharedPreferencesImpl
进行构建
1 2 3 4 5 6 7 8 9 10 11 12 SharedPreferencesImpl(File file, int mode) { mFile = file; mBackupFile = makeBackupFile(file); mMode = mode; mLoaded = false ; mMap = null ; mThrowable = null ; startLoadFromDisk(); }
加载文件
开启一个异步线程去加载xml文件,防止阻塞主线程
1 2 3 4 5 6 7 8 9 10 11 private void startLoadFromDisk () { synchronized (mLock) { mLoaded = false ; } new Thread("SharedPreferencesImpl-load" ) { public void run () { loadFromDisk(); } }.start(); }
loadFromDisk()
时,需要判断当前是否存在备份文件,若存在备份文件就意味着上一次写入文件的过程出现了异常,导致写入失败
。
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 private void loadFromDisk () { synchronized (mLock) { if (mLoaded) { return ; } if (mBackupFile.exists()) { mFile.delete(); mBackupFile.renameTo(mFile); } } ... synchronized (mLock) { mLoaded = true ; mThrowable = thrown; try { if (thrown == null ) { if (map != null ) { mMap = map; mStatTimestamp = stat.st_mtim; mStatSize = stat.st_size; } else { mMap = new HashMap<>(); } } } catch (Throwable t) { mThrowable = t; } finally { mLock.notifyAll(); } } }
这里就表现了SP的文件损坏时的备份机制 ,当文件写入异常时,启用备份文件保证之前的数据不会出现异常。
获取数据
1 2 ... 初始化完成 sp对象 sp.getString("a" ,"b" );
获取数据
支持部分数据类型,例如int
、long
、String
等
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 String getString (key) {}public Set<String> getStringSet (key) {}public int getInt (key) {}public long getLong (key) {}public float getFloat (key) {}public boolean getBoolean (key) {} 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); } }
getXX()
都是运行在主线程的,并且想要获取数据就必须等待加载文件
这一步完成。等待mLock.notifyAll()
才可以继续向下执行。
如果需要读取一个很大的文件,在调用getXX()
之后,就需要一直进行等待,而导致主线程发生阻塞。
xml文件加载完毕后,getXX()
从mMap
获取数据,就不需要重新读取文件。
获取数据异常 SP中进行存储时,可能会导致同一个key存储不同类型的值,导致获取数据的时候抛出ClassCastException
异常。
写入数据
1 2 3 4 ... 初始化完成 sp对象 SharedPreferences.Editor mEditor = sharedPreferences.edit(); mEditor.putString("a" ,"c" );
写入数据
同样支持部分数据类型。但是不是通过SP对象,而是通过Editor
对象进行数据的写入
Editor对象 写入数据的相关操作都要通过Editor
,本体是一个接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public interface Editor { Editor putString (String key, @Nullable String value) ; Editor putStringSet (String key, @Nullable Set<String> values) ; Editor putInt (String key, int value) ; Editor putLong (String key, long value) ; Editor putFloat (String key, float value) ; Editor putBoolean (String key, boolean value) ; Editor remove (String key) ; Editor clear () ; boolean commit () ; void apply () ; }
Editor
只是一个接口,EditorImpl
才是具体的实现类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public final class EditorImpl implements Editor { private final Object mEditorLock = new Object(); @GuardedBy ("mEditorLock" ) 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 ; } } }
mModified
保存的是用户通过putXX()
新增的数据,数据有效期位于第一次putXX 到 commit()/apply()
.
提交数据 上面写入数据
完毕后,最后要调用一次commit()/apply()
准备把数据写入到对应xml文件中。
“半同步”提交数据——commit
1 2 3 4 5 6 ... 初始化完成 sp对象 SharedPreferences.Editor mEditor = sharedPreferences.edit(); mEditor.putString("a" ,"c" ); mEditor.commit();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public boolean commit () { long startTime = 0 ; if (DEBUG) { startTime = System.currentTimeMillis(); } MemoryCommitResult mcr = commitToMemory(); SharedPreferencesImpl.this .enqueueDiskWrite( mcr, null ); try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException e) { return false ; } finally { } notifyListeners(mcr); return mcr.writeToDiskResult; }
commit()
先后调用了commitToMemory()/*写入数据到mMap中,等待写入磁盘*/
、enqueueDiskWrite()/*将数据写入到磁盘中*/
,通过writeToFile()
写入到磁盘中并会返回对应写入结果。
commit()
如果当前没有线程在写入文件时,就会直接在当前线程开启写入磁盘任务,导致主线程阻塞(可能发生ANR ),等待线程执行完毕。如果在写入文件,就会通过QueuedWork
开启异步执行。(这就是半同步的原因 )
commit()
执行都是同步的,而且每次都是写入全量的数据,会导致主线程阻塞。
异步提交数据——apply
1 2 3 4 5 6 ... 初始化完成 sp对象 SharedPreferences.Editor mEditor = sharedPreferences.edit(); mEditor.putString("a" ,"c" ); mEditor.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 25 26 27 28 29 30 @Override 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); }
apply()
也先调用commitToMemory()
将更改提交到内存,之后调用enqueueDiskWriter()
开启写入磁盘任务。
apply()
是提交任务到线程池后,就直接通知写入成功,不需要等待线程执行完成。
虽然apply()是异步执行的,但是存在某些场景下也会发生ANR。后文会分析
缓存写入内存
将缓存文件写入内存
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 private MemoryCommitResult commitToMemory () { long memoryStateGeneration; List<String> keysModified = null ; Set<OnSharedPreferenceChangeListener> listeners = null ; Map<String, Object> mapToWriteToDisk; synchronized (SharedPreferencesImpl.this .mLock) { if (mDiskWritesInFlight > 0 ) { mMap = new HashMap<String, Object>(mMap); } mapToWriteToDisk = mMap; mDiskWritesInFlight++; 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 ; } mModified.clear(); if (changesMade) { mCurrentMemoryStateGeneration++; } } } return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk); }
将mModified
的值写入到mapToWriteToDisk
,其实mMap
中也是一样的内容,然后清空mModified
的数据,拼接得到一个MemoryCommitResult
对象,里面持有的就是要写入xml
文件的内容。
内存写入磁盘
把存入内存的数据mapToWriteToDisk
写入到对应的xml
文件。
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 private void enqueueDiskWrite (final MemoryCommitResult mcr, //commit() 传入为null apply () 传入不为null 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); }
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 private void writeToFile (MemoryCommitResult mcr, boolean isFromSyncCommit) { if (!backupFileExists) { if (!mFile.renameTo(mBackupFile)) { Log.e(TAG, "Couldn't rename file " + mFile + " to backup file " + mBackupFile); mcr.setDiskWriteResult(false , false ); return ; } } else { mFile.delete(); } ... } void setDiskWriteResult (boolean wasWritten, boolean result) { this .wasWritten = wasWritten; writeToDiskResult = result; if (mDiskStateGeneration < mcr.memoryStateGeneration) { if (isFromSyncCommit) { needsWrite = true ; } else { synchronized (mLock) { if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) { needsWrite = true ; } } } } if (!needsWrite) { mcr.setDiskWriteResult(false , true ); return ; } writtenToDiskLatch.countDown(); }
写入到xml
文件之前,会把原有的数据保存在.bak
文件进行备份,用于写入磁盘过程中发生任何异常都可以恢复原有数据。
根据上述流程commit()/apply()提交数据 -> 写入内存 -> 写入硬盘
,每次调用都会走一遍完整流程,导致频繁的IO使用。
官方更建议将数据的更新合并到一次写操作中,即多次写入一次提交。
commit与apply的比较
apply()
没有返回值,commit()
有返回值可以知道文件是否写入成功
apply()
将修改提交内存,再异步写入文件;commit()
同步写入文件。
并发commit()
时,需要等待正在执行的数据写入到文件后才会继续往下执行;apply()
先更新到内存,后面再次调用会覆盖原有的内存数据,接下来再异步写入文件即可。
删除数据 1 2 3 4 5 6 7 8 9 10 11 12 SharedPreferences sp = getSharedPreferences("a" ,MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putString("a" ,"b" ); editor.remove("a" ); editor.commit(); editor.clear(); editor.commit();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private MemoryCommitResult commitToMemory () { Map<String, Object> mapToWriteToDisk; ... synchronized (SharedPreferencesImpl.this .mLock) { mapToWriteToDisk = mMap; ... synchronized (mEditorLock) { boolean changesMade = false ; if (!mapToWriteToDisk.isEmpty()) { changesMade = true ; mapToWriteToDisk.clear(); } mClear = false ; } } }
数据改变监听 SP支持监听数据的改变,返回的是修改的内容
1 2 3 4 5 6 7 8 9 10 11 12 SharedPreferences sp = getSharedPreferences("a" ,MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); SharedPreferences.OnSharedPreferenceChangeListener listener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged (SharedPreferences sharedPreferences, String key) { } }; sp.unregisterOnSharedPreferenceChangeListener(listener);
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 public void registerOnSharedPreferenceChangeListener (OnSharedPreferenceChangeListener listener) { synchronized (mLock) { mListeners.put(listener, CONTENT); } } private MemoryCommitResult commitToMemory () { ... boolean hasListeners = mListeners.size() > 0 ; if (hasListeners) { keysModified = new ArrayList<String>(); listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet()); } ... return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners, mapToWriteToDisk); } private void notifyListeners (final MemoryCommitResult mcr) { if (mcr.listeners == null || mcr.keysModified == null || mcr.keysModified.size() == 0 ) { return ; } if (Looper.myLooper() == Looper.getMainLooper()) { for (int i = mcr.keysModified.size() - 1 ; i >= 0 ; i--) { final String key = mcr.keysModified.get(i); for (OnSharedPreferenceChangeListener listener : mcr.listeners) { if (listener != null ) { listener.onSharedPreferenceChanged(SharedPreferencesImpl.this , key); } } } } else { ActivityThread.sMainThreadHandler.post(() -> notifyListeners(mcr)); } }
commit()
需要在数据写入文件后,才可以回调到notifyListeners()
通知数据发生变化。
apply()
只要数据在写入内存后,就会直接回调。
QueuedWork
系统提供的异步工具类,内部通过HandlerThread
作为工作线程,用于跟踪那些未完成或尚未结束的全局任务 。
初始化 1 2 3 4 5 6 7 8 9 10 11 12 13 private static Handler getHandler () { synchronized (sLock) { if (sHandler == null ) { HandlerThread handlerThread = new HandlerThread("queued-work-looper" , Process.THREAD_PRIORITY_FOREGROUND); handlerThread.start(); sHandler = new QueuedWorkHandler(handlerThread.getLooper()); } return sHandler; } }
queue() 向QueuedWork中添加任务
1 2 3 4 5 6 7 8 9 10 11 12 13 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); } } }
commit()
时,shouldDelay
为false,直接发送消息
apply()
时,shouldDelay
为true,需要延迟100ms再发送消息,避免频繁的磁盘写入操作
addFinisher() 添加完成任务完成回调
1 2 3 4 5 6 7 8 9 @GuardedBy ("sLock" )private static final LinkedList<Runnable> sFinishers = new LinkedList<>();public static void addFinisher (Runnable finisher) { synchronized (sLock) { sFinishers.add(finisher); } }
processPendingWork() 执行写入磁盘任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 private static void processPendingWork () { long startTime = 0 ; synchronized (sProcessingWork) { LinkedList<Runnable> work; synchronized (sLock) { work = (LinkedList<Runnable>) sWork.clone(); sWork.clear(); getHandler().removeMessages(QueuedWorkHandler.MSG_RUN); } if (work.size() > 0 ) { for (Runnable w : work) { w.run(); } } } }
* waitToFinish() 等待任务完成。这里也就是ANR
发生的根本原因
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 public static void waitToFinish () { long startTime = System.currentTimeMillis(); boolean hadMessages = false ; Handler handler = getHandler(); try { processPendingWork(); } finally { StrictMode.setThreadPolicy(oldPolicy); } try { while (true ) { Runnable finisher; synchronized (sLock) { finisher = sFinishers.poll(); } if (finisher == null ) { break ; } finisher.run(); } } finally { sCanDelay = true ; } } apply(){ final Runnable awaitCommit = new Runnable() { @Override public void run () { try { mcr.writtenToDiskLatch.await(); } catch (InterruptedException ignored) { } } }; QueuedWork.addFinisher(awaitCommit); }
调用waitToFinish()
时,会主动调用processPendingWork()
去执行任务,在HandlerThread执行写入磁盘任务。
waitToFinish()
会一直等待写入任务执行完毕,其他什么都不做,当存在很多写入任务时,会依次执行,文件很大时效率很低,就有可能导致ANR。
线程安全
SP的线程安全分为两部分分析
读线程安全 1 2 3 4 5 6 7 8 9 10 @GuardedBy ("mLock" ) private Map<String, Object> mMap;public String getString (String key, @Nullable String defValue) { synchronized (mLock) { String v = (String)mMap.get(key); return v != null ? v : defValue; } }
读操作
主要是从mMap
读取缓存的值,避免其他线程执行写操作
导致线程不安全,通过mLock
保证线程安全。
写线程安全 写操作,主要分为三步,每一步都有不同的锁进行控制。
写入对象 1 2 3 4 5 6 7 8 9 10 @GuardedBy ("mEditorLock" )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 ; } }
第一把锁mEditorLock
保证写入到mModified
线程安全
写入内存 1 2 3 4 5 6 7 8 9 10 11 12 13 14 synchronized (SharedPreferencesImpl.this .mLock) { if (mDiskWritesInFlight > 0 ) { mMap = new HashMap<String, Object>(mMap); } mapToWriteToDisk = mMap; synchronized (mEditorLock) { boolean changesMade = false ; for (Map.Entry<String, Object> e : mModified.entrySet()) { mapToWriteToDisk.put(k, v); } }
写入内存时,需要把mModify
待添加的数据合并到mapToWriteToDisk
中,这时需要通过两把锁保证线程安全。
保证mapToWriteToDisk
赋值时数据正确
保证mModified
合并到mapToWriteToDisk
时线程安全
写入硬盘 1 2 3 4 synchronized (mWritingToDiskLock) { writeToFile(mcr, isFromSyncCommit); }
写入硬盘时,保证写入时内容不会发生改变。
进程安全
SharedPreferences不是进程安全的。
MODE_MULTI_PROCESS 1 2 3 4 5 if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { sp.startReloadIfChangedUnexpectedly(); }
唯一的作用就是 切换进程时重新加载XML文件内容。
当在频繁跨进程读写时就会有数据丢失的可能。
ContentProvider(官方推荐) ContentProvider
是Android提供的跨进程组件,可以替换其底层实现为SP,来保证SP的进程安全。
//TODO 实现待添加
文件锁 SharedPreferences 本质是对xml文件的读写,可以通过对xml文件添加文件锁,就能保证进程安全。
FileLock(文件锁)用来表示文件区域锁定标记,可以通过对一个可写文件加锁,保证同时只有一个进程可以拿到文件的锁,这个进程就可以对文件进行访问;其他拿不到锁的进程要么选择被挂起等待,要么去做一些其他的事情。
可以保证众进程可以顺序访问文件,并且可以通过FileLock
进行并发控制,保证进程的顺序执行。
获取锁
FileChannel.lock():阻塞直至获得文件锁。默认锁定整个文件
FileChannel.lock(position,size,shared):阻塞直至获取文件的部分数据的文件锁
FileChannel.tryLock():立即返回,要么返回锁,要么返回null(获取锁失败)
释放锁
FileLock.release():释放当前文件锁
检测锁
FileLock.isValid():检测文件锁的有效性
//TODO 实现待添加
ANR分析
在上面有提到QueuedWork.waitToFinish()
是要在写入文件的操作完成后才会结束,且这个方法会运行在当前线程,极有可能导致阻塞/ANR。
waitToFinish()
调用场景通过全局搜索QueuedWork.waitToFinish()
找到在ActivityThread
使用的较多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void handleStopActivity (IBinder token, boolean show, int configChanges, PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) { ... if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } } private void handleSleeping (IBinder token, boolean sleeping) { ... if (sleeping) { if (!r.stopped && !r.isPreHoneycomb()) { callActivityOnStop(r, true , "sleeping" ); } if (!r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } }
会在onStop()
时调用QueuedWork.waitToFinish()
等待当前未执行完毕的写入任务结束,才可以释放锁。此时就会阻塞主线程,可能导致ANR。
解决方案
反射在ActivityThread
中的H
变量添加一个callback
,可以拦截Handler的事件分发。在几个关键的节点例如stop
、pause
及时通过反射清理QueuedWork
中的sFinishers
请求等待队列。
开启一个异步线程在其内部调用commit()
去写入数据
使用注意事项
建议不要在SP里存储特别大的key/value,因为内容都是一次性加载到内存中,过大会导致卡顿/ANR。
不要频繁调用commit()/apply()
,SP的数据每次都是全量写入文件,尤其是commit()
直接同步操作,更容易卡顿。建议批量写一次提交
MODE_MULTI_PROCESS
是在每次getSharedPreferences
时检查磁盘上配置文件上次修改时间和文件大小,一旦所有修改则会重新从磁盘加载文件,所以并不能保证多进程数据的实时同步。
高频写操作的key与高频读操作的key可以适当的拆分文件,减少同步锁的竞争。
最好写入轻量级的数据,不要存储大量的数据。
替换方案 MMKV
通过mmap
内存映射文件,提供一段可随时写入的内存块,APP只管往里写数据,由操作系统负责将内存回写到文件,而不必担心Crash导致数据丢失。
写入的数据格式为 Protobuf
Github-MMKV
MMKV原理
MMKV for Android 多进程设计与实现
参考链接 全面解析SharedPReferences
SharedPreferences的设计与实现
Jetpack DataStore 分析
剖析 SharedPreference apply 引起的 ANR 问题
Android 源码仓库