-
创建
val prefs = getSharedPreferences("myPrefs",Context.MODE_PRIVATE)
通过上下文创建的逻辑在ContextImpl中:
@Override 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); }
这里用缓存保存File,第一次则使用getSharedPreferencesPath方法创建,注意mSharedPrefsPaths是私有变量,因此每一个ContextImpl都有一个,也就是说每个Activity第一次获取SharedPreferences时都会创建一个:
@Override public File getSharedPreferencesPath(String name) { return makeFilename(getPreferencesDir(), name + ".xml"); }
getPreferencesDir方法最终会从以下三个目录中的一个选为存储目录,而SharedPreferences的文件名则会“$name.xml”:
File res = null; if (isCredentialProtectedStorage()) { res = mPackageInfo.getCredentialProtectedDataDirFile(); } else if (isDeviceProtectedStorage()) { res = mPackageInfo.getDeviceProtectedDataDirFile(); } else { res = mPackageInfo.getDataDirFile(); }
private File makeFilename(File base, String name) { if (name.indexOf(File.separatorChar) < 0) { final File res = new File(base, name); ... return res; }... }
文件创建好了之后就会调用getSharedPreferences(File file, int 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; } } ... return sp; }
这里也有一个SharedPreferencesImpl的缓存,getSharedPreferencesCacheLocked方法中按照包名保存了一个集合,对应着文件和SharedPreferencesImpl实例:
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; }
前面我们说到mSharedPrefsPaths保存的存在周期是当前上下文,而这里缓存的sSharedPrefsCache却是一个全局变量,因此只要是获取过SharedPreferences之后这个SharedPreferencesImpl和file会一直存在于内存中,直到进程结束。
-
编辑
我们知道,编辑SharedPreferences需要调用edit方法获取Editor,现在我们知道实际上调用的是SharedPreferencesImpl的edit方法:
@Override public Editor edit() { ... synchronized (mLock) { awaitLoadedLocked(); } return new EditorImpl(); }
private void awaitLoadedLocked() { ... while (!mLoaded) { try { mLock.wait(); } catch (InterruptedException unused) { } } ... }
我们看到这里会产生一个阻塞,等待mLoaded变成true,mLoaded又是什么标志呢?在前面SharedPreferencesImpl的构造方法中调用了一个startLoadFromDisk方法:
private void startLoadFromDisk() { synchronized (mLock) { mLoaded = false; } new Thread("SharedPreferencesImpl-load") { public void run() { loadFromDisk(); } }.start(); }
loadFromDisk方法内可以概括成从SharedPreferences文件中读取数据到mMap中,然后修改mLoaded标志为true,因为loadFromDisk是异步调用的,所以前面要堵塞等待数据读取完成才能返回EditorImpl,那为什么需要先读取数据呢?因为下面的编辑需要用到mMap。
@Override public Editor putString(String key, @Nullable String value) { synchronized (mEditorLock) { mModified.put(key, value); return this; } }
以EditorImpl的一个putString方法为例,可以看到会把要设置的数据放到mModified中,mModified是一个HashMap,没什么好说的,编辑的主要逻辑在于提交操作。
我们知道提交数据有commit和apply两个方法,前者是同步提交,后者是异步提交,它俩的通用伪代码如下:
@Override public void 《apply和commit通用伪代码方法》() { ... final MemoryCommitResult mcr = commitToMemory(); ... //apply方法会传postWriteRunnable,commit会传null SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable); //apply会通过子线程回调这句,commit则直接调用 mcr.writtenToDiskLatch.await(); ... notifyListeners(mcr); }
其实apply就是多了一步把commit的操作放到了子线程队列中:
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(); } } }; // Typical #commit() path with fewer allocations, doing a write on // the current thread. if (isFromSyncCommit) { boolean wasEmpty = false; synchronized (mLock) { wasEmpty = mDiskWritesInFlight == 1; } if (wasEmpty) { writeToDiskRunnable.run(); return; } } QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit); }
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); } } }
getHandler会开启一个子线程来存放异步apply消息:
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; } }
上面的handler就是QueueWorkHandler,可以看到它用的是handlerThread这个子线程的Looper,因此apply是开启了子线程来处理的。这里注意是先调用了start方法开启线程,因为这里的Looper我们需要依附于子线程,因此需要在子线程中调用Looper.prepare方法,也因此HandlerThread中初始化Looper的地方放在了run方法中:
@Override public void run() { mTid = Process.myTid(); Looper.prepare(); synchronized (this) { mLooper = Looper.myLooper(); notifyAll(); } Process.setThreadPriority(mPriority); onLooperPrepared(); Looper.loop(); mTid = -1; }
言归正传,不管是writeToDiskRunnable直接run还是通过子线程队列回调回来run,最终都会执行到writeToDiskRunnable的run方法中,从而执行writeToFile方法:
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) { ... try { FileOutputStream str = createFileOutputStream(mFile); ... XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str); ... str.close(); //设置的mode参数的用处 ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0); ... mcr.setDiskWriteResult(true, true); ... return; } ... mcr.setDiskWriteResult(false, false); }
可以看到,主要是调用XmlUtils的writeMapXml方法往mFile写入,内部使用了缓冲,最后会调用FastXmlSerializer的endDocument方法,进而调用它的flush方法:
public void flush() throws IOException { ... mWriter.write(mText, 0, mPos); mWriter.flush(); ... }
在写入过程中,整个IO操作都没有开启子线程,这意味着如果在主线程中使用EditorImpl提交是会堵塞主线程的。
-
数据逻辑
回过头来看,在调用enqueueDiskWrite方法之前,二者都会先调用一个commitToMemory方法:
private MemoryCommitResult commitToMemory() { ... Map<String, Object> mapToWriteToDisk; synchronized (SharedPreferencesImpl.this.mLock) { ... mapToWriteToDisk = mMap; ... synchronized (mEditorLock) { ... for (Map.Entry<String, Object> e : mModified.entrySet()) { String k = e.getKey(); Object v = e.getValue(); //value不能设置为当前EditorImpl本身和null 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); }
可见这里是处理本次提交的数据的地方,最后MemoryCommitResult会保存本次要提交的最终数据,也就是上面出现的mcr.mapToWriteToDisk。
-
总结
由此,我们可以总结出SharedPreferences的几点不足:
- SharedPreferencesImpl的缓存使用了static全局容器存储,这会增加内存消耗;
- EditorImpl进行数据保存时会有IO操作,而这部分操作并没有在内部开启子线程处理,这就有堵塞主线程的使用风险;
- 每次数据更新都是全量更新,通常每次都只更新一个数据值,却需要整个xml文件数据的整体写入,这就造成了资源的浪费;
- 设置了很多锁来保证同步,但因此造成了多线程的读写效率低下问题。
网友评论