Android源码解析:SharedPreferences的解析
导言
SharedPreferences是Android中的一种轻量的数据持久化手段,可能也是我们在学习Android时接触到的第一种特殊的本地数据持久化手段,本篇文章就将从源码角度分析SharedPreferences的原理。
源码分析
一般我们使用SharedPreferences是这样使用的:
//sp的使用--写入数据
val sp = getPreferences(Context.MODE_PRIVATE)
val editor = sp.edit()
editor.putString("cc","123")
editor.apply()
//读取数据
val ans = sp.getString("cc","null")
Toast.makeText(this, ans, Toast.LENGTH_SHORT).show()
我们接下来就以这段程序为例分析SharedPreferences的原理。
获取Preferences对象
我们可以有多种方法可以获得Preferences对象:
getPreferences(int mode)
getDefaultSharedPreferences(context context)
getSharedPreferences(String key,int mode)
这段示例中我们以getPreferences
方法为例,实际上这个方法的完整显示应该是getActivity().getPreferences()
,也就是说必须在Activity上调用该方法,我们来看该方法:
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {return getSharedPreferences(getLocalClassName(), mode);
}
可以看到该方法最终还是会调用到getSharedPreferences(String key,int mode)
方法,只不过此处以本Activity的类名为关键字传递到第一个参数中,接下来我们继续看跳转到的第二个方法中:
public SharedPreferences getSharedPreferences(File file, int mode) {return mBase.getSharedPreferences(file, mode);
}
这最终就调用到了与Activity相关联的Context
的方法中,这个mBase
不出所料应该是ContextImpl,我们来看这个方法:
public SharedPreferences getSharedPreferences(String name, int mode) {......File file;//同步代码块,以ContextImpl类为锁进行锁定synchronized (ContextImpl.class) {//当文件路径还没加载时if (mSharedPrefsPaths == null) { //创建一个Map来存储文件路径mSharedPrefsPaths = new ArrayMap<>();}//尝试从文件路径存储中查找路径file = mSharedPrefsPaths.get(name);//若查找不到具体的文件,说明文件还没有被创建if (file == null) {//调用getSharedPreferencesPath方法file = getSharedPreferencesPath(name);//将新创建出来的文件路径放入Map中mSharedPrefsPaths.put(name, file);}}// 跳转到另一个重载的方法中return getSharedPreferences(file, mode);}
重要的代码部分我已经加上了注释,此处的方法就是创建出一个Map来存储同一个Context下的Sp对象(路径),若目标的Sp对象不存在还要创建一个Sp对象然后将其存储到Map中,我们具体先来看getSharedPreferencesPath
方法:
public File getSharedPreferencesPath(String name) {return makeFilename(getPreferencesDir(), name + ".xml");
}
可以看到这个方法实际上就是创建出来了一个新的文件,父路径为getPreferencesDir()
的值,子路径为name
+xml,意思就是创建出来的是一个xml文件,也就是说Sp实际上是通过xml文件来存储具体数据的。
然后我们来看最后跳转到的另一个方法中:
public SharedPreferences getSharedPreferences(File file, int mode) {//实际的Sp实现类SharedPreferencesImpl sp;//仍然是以ContextImpl为锁进行同步synchronized (ContextImpl.class) {//获取Sp缓存final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();//获得Sp的具体实例sp = cache.get(file);//当不能成功从缓存中获取Sp时if (sp == null) {checkMode(mode);if (getApplicationInfo().targetSdkVersion >= android.os.Build.VERSION_CODES.O) {if (isCredentialProtectedStorage()&& !getSystemService(UserManager.class).isUserUnlockingOrUnlocked(UserHandle.myUserId())) {throw new IllegalStateException("SharedPreferences in credential encrypted "+ "storage are not available until after user is unlocked");}}//创建一个新的Sp实例sp = new SharedPreferencesImpl(file, mode);//加入到缓存中cache.put(file, sp);//返回Sp实例return sp;}}if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {// If somebody else (some other process) changed the prefs// file behind our back, we reload it. This has been the// historical (if undocumented) behavior.sp.startReloadIfChangedUnexpectedly();}return sp;
}
这段代码的重要逻辑我也已经标注出来了,我们需要额外看的可能就是getSharedPreferencesCacheLocked()
获取缓存的过程:
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实例的。
所以总结下来就是当缓存中没有对应的Sp实例时创建一个Sp实例塞入缓存中,如果缓存中有就直接返回对应Sp实例。
Commit提交修改
首先我们要找到这个方法需要来到Sp的具体实现类SharedPreferencesImpl
中的内部类EditorImpl
,不过在分析该方法之前我们还需要先看一下另一个方法commitToMemory
,它也是EditorImpl
中的方法:
private MemoryCommitResult commitToMemory() {long memoryStateGeneration;boolean keysCleared = false;List<String> keysModified = null;Set<OnSharedPreferenceChangeListener> listeners = null;Map<String, Object> mapToWriteToDisk;//以当前的Sp实例(持有的外部类实例)为锁来同步synchronized (SharedPreferencesImpl.this.mLock) {//当还有未完成的磁盘写入时if (mDiskWritesInFlight > 0) {//更新Map,将之前的Map内容也写入到当前Map中mMap = new HashMap<String, Object>(mMap);}//更新要写入磁盘的MapmapToWriteToDisk = mMap;//标记正在写入的标记值+mDiskWritesInFlight++;//判断是否有监听器boolean hasListeners = mListeners.size() > 0;//如果存在监听器的话if (hasListeners) {keysModified = new ArrayList<String>();listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());}//等待编辑锁synchronized (mEditorLock) {boolean changesMade = false;//如果Clear位为trueif (mClear) {//写入磁盘的map不为空if (!mapToWriteToDisk.isEmpty()) {//修改位置为truechangesMade = true;mapToWriteToDisk.clear();}keysCleared = true;mClear = false;}//遍历需要修改的Map中的数据for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();Object v = e.getValue();// v == this 时 或者 v 为空时// v == this 对应的是 remove方法if (v == this || v == null) {// 需要写入磁盘的map中不包含当前key时,直接跳过本次循环if (!mapToWriteToDisk.containsKey(k)) {continue;}//将其从需要写入磁盘的map中移除mapToWriteToDisk.remove(k);} else {//当无修改的时候直接跳过if (mapToWriteToDisk.containsKey(k)) {Object existingValue = mapToWriteToDisk.get(k);if (existingValue != null && existingValue.equals(v)) {continue;}}//否则将其写入需要写入磁盘的mapmapToWriteToDisk.put(k, v);}//将changesMade标志位置为truechangesMade = true;//如果有监听器的话if (hasListeners) {//将需要修改的键值对的key值写入keysModified中keysModified.add(k);}}//清除mModified这个mapmModified.clear();//如果有修改要提交到磁盘中去if (changesMade) {//自增相当于是一个版本号mCurrentMemoryStateGeneration++;}memoryStateGeneration = mCurrentMemoryStateGeneration;}}//返回一个对象,这个对象描述的就是需要写入磁盘中的数据的相关信息return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,listeners, mapToWriteToDisk);
}
该方法的一些注释已经写在上面了,这主要是将Editor之前的操作,比如在执行commit
之前调用到的putString
等操作封装成一个MemoryCommitResult
对象,这个对象就是用来描述需要写入磁盘中的数据的相关信息。
看完了commitToMemory
方法,我们接下来再来看commit
方法:
public boolean commit() {long startTime = 0;if (DEBUG) {startTime = System.currentTimeMillis();}//将之前的操作提交形成一个提交对象MemoryCommitResult mcr = commitToMemory();// 加入到外部类的磁盘写队列中SharedPreferencesImpl.this.enqueueDiskWrite(mcr, null /* sync write on this thread okay */);try {//等待写入完成mcr.writtenToDiskLatch.await();} catch (InterruptedException e) {return false;} finally {if (DEBUG) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " committed after " + (System.currentTimeMillis() - startTime)+ " ms");}}//写入完成之后唤醒监听器notifyListeners(mcr);//返回是否写入成功return mcr.writeToDiskResult;
}
主要的注释也已经写入在代码中了,commit
的整个流程还是很好懂的,首先就是通过我们之前介绍过的commitToMemory
方法将之前的操作封装成一个提交信息,然后将其添加到SP的任务队列中,等待其写入完成,最后返回结果即可。
接着我们来看加入到任务队列中的过程,具体来说就是SharedPreferencesImpl.this.enqueueDiskWrite( mcr, null );
这一句:
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {//判断是否是同步提交的(postWriteRunnable == null 时说明是同步提交的)final boolean isFromSyncCommit = (postWriteRunnable == null);//将写任务包装成一个Runnablefinal Runnable writeToDiskRunnable = new Runnable() {@Overridepublic void run() {synchronized (mWritingToDiskLock) {//同步执行writeToFile方法,也就是写入磁盘中,具体来说是每个Sp对应的xml文件writeToFile(mcr, isFromSyncCommit);}//将正在写入的任务数--synchronized (mLock) {mDiskWritesInFlight--;}//异步写入的时候会添加一个postWriteRunnable任务,在此处执行if (postWriteRunnable != null) {postWriteRunnable.run();}}};//当操作为同步提交if (isFromSyncCommit) {boolean wasEmpty = false;synchronized (mLock) {wasEmpty = mDiskWritesInFlight == 1;}//当只有当前这一个任务需要提交的时候if (wasEmpty) {//当empyt标志位为true,直接执行我们上面包装好的RunnablewriteToDiskRunnable.run();return;}}//若是异步或者同步提交前有其他任务才会将其添加到工作队列中执行,第二个参数为shouldDelay//标志位,即需不需要进行延时100ms,可以看到当同步时不需要延时而异步时需要延时QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
这里相关的代码逻辑也已经在方法中标注出来了。此处的特殊处理在于同步异步时的处理,当为同步提交且当前任务是唯一的任务时将直接执行当前任务而不需要经过任务队列
,否则将通过任务队列处理。同步时提交到任务队列执行时不需要进行延时,而异步提交时需要进行100ms的延时。为什么是100ms
的延时呢?我们等等再来看这一部分的源码。
Apply提交修改
看完了同步提交,我们接下来再来看异步提交。首先我们紧接着上面关于任务队列的操作,紧接上面的QueuedWork.queue
方法:
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);}}
}
可以看到这个方法很短,其实具体还是通过Handler机制来提交任务的,那其对应的Thread在哪里?可以在getHandler
方法中看到:
private static Handler getHandler() {synchronized (sLock) {if (sHandler == null) {//创建一个HandlerThread用作工作线程HandlerThread handlerThread = new HandlerThread("queued-work-looper",Process.THREAD_PRIORITY_FOREGROUND);//启动工作线程handlerThread.start();//创建出HandlersHandler = new QueuedWorkHandler(handlerThread.getLooper());}return sHandler;}
}private static class QueuedWorkHandler extends Handler {static final int MSG_RUN = 1;QueuedWorkHandler(Looper looper) {super(looper);}public void handleMessage(Message msg) {if (msg.what == MSG_RUN) {//处理挂起的任务processPendingWork();}}
}
由于写磁盘也是耗时操作,所以说SharedPreferences在执行写任务的时候是会创建一个HandlerThread
线程作为工作线程,并且将其与Handler关联起来,通过Handler处理任务队列。我们之前所说的延时100ms
具体是通过queue
方法体现的:
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);}}
}
这里通过handler将任务发送到MessageQueue中。如果shouldDelay
和sCanDelay
标志位均为true就会通过Handler的sendMessageDelay
方法,第二个参数即为延时的毫秒数,我们可以看到它的具体取值:
可以看到只是一个100ms的延时。
好了,现在言归正传,我们来看apply
方法的源码:
public void apply() {final long startTime = System.currentTimeMillis();final MemoryCommitResult mcr = commitToMemory();final Runnable awaitCommit = new Runnable() {@Overridepublic void run() {try {mcr.writtenToDiskLatch.await();} catch (InterruptedException ignored) {}if (DEBUG && mcr.wasWritten) {Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration+ " applied after " + (System.currentTimeMillis() - startTime)+ " ms");}}};QueuedWork.addFinisher(awaitCommit);Runnable postWriteRunnable = new Runnable() {@Overridepublic void run() {awaitCommit.run();QueuedWork.removeFinisher(awaitCommit);}};SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);// Okay to notify the listeners before it's hit disk// because the listeners should always get the same// SharedPreferences instance back, which has the// changes reflected in memory.notifyListeners(mcr);
}
可以看到这整个apply
方法和commit
是差不多的,区别之一是commit
方法的mcr.writtenToDiskLatch.await()
这一句是直接在commit
方法中执行的,该方法就是用来等待写入完成的;而apply
方法是将该方法封装进入一个Runnable
对象中再塞入工作队列中执行,所以就不会在调用处引起阻塞。除此之外我们还可以在apply
中发现的不同点是调用到了addFinisher
方法,这个就和具体的工作队列类QueuedWork
有关了。
工作队列QueuedWork
该特殊的工作队列和其他的工作队列的不同之处应该就在于其持有的Finisher
队列,具体来说这个队列是保证该队列中的任务一定会被执行,什么叫一定被执行呢?众所周知诸如Activity等组件是存在其生命周期的,如果当其生命周期终结时任务队列中的剩余任务自然也不会被执行了,该队列的存在保证剩余的任务一定会被处理,具体我们可以在waitToFinish
方法的注释中看出来:
Trigger queued work to be processed immediately. The queued work is processed on a separate thread asynchronous. While doing that run and process all finishers on this thread. The finishers can be implemented in a way to check weather the queued work is finished. Is called from the Activity base class’s onPause(), after BroadcastReceiver’s onReceive, after Service command handling, etc. (so async work is never lost)
这个方法将在Activity的onPause
方法中执行来确保Finisher队列中的任务一定会被执行。
读取数据的过程
读取数据的过程我们就以getString
方法为例:
public String getString(String key, @Nullable String defValue) {synchronized (mLock) {awaitLoadedLocked();String v = (String)mMap.get(key);return v != null ? v : defValue;}
}
显然是通过一个map来取数据的,不过在这之前会执行一个awaitLoadedLocked
,顾名思义就是等待Sp读取磁盘文件的过程,这个过程我们就不再深入了,不过在读取的过程中也是加锁的。所以我们可以说SharedPerferences是线程安全的工具。
总结
最后我们来对SharedPreferences的工作流程进行一下总结,首先是它的创建:
接着是它的写入过程:
读取过程很简单就不写了。