SharedPreferences想必大家都很熟悉了,它是Android给我们提供的一种轻量级的文件存储的方式。
写这篇文章的契机源自于一次项目中大家对SharedPreferences性能的讨论,你一句我一句的,因为平时对SharedPreferences的了解只是停留在使用的阶段,不好说什么,因此下了决定要对SharedPreferences源码通读一遍。
SharedPreferences的概念就不多说了,直接上一段代码看看SharedPreferences是如何使用的
//获取一个SharedPreferences实例
SharedPreferences preferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
//读
String value = preferences.getString(key, defValue);
//写
preferences.edit().putString(key, value).commit();
preferences.edit().putString(key, value).apply();
在开始分析源码之前,先提几个疑问,带着疑问去看源码,更有目的性。虽然平时都说看系统源码不要追求细枝末节,很容易陷入代码无法自拔,但这次SharedPreferences的源码不多,决定细读一番。
问题:
1.我们知道SharedPreferences中的数据是保存在文件中的,那么这个文件是在什么时候创建的?获取SharedPreferences实例的时候?第一次读的时候?第一次写的时候?
2.SharedPreferences在使用时和map有点类似,但它又是以文件的形式保存数据的,那它是如何实现类似map的使用的
3.SharedPreferences每次读写都要访问文件吗?这样效率岂不是很低下?
4.数据写到磁盘文件的时候是增量写入还是全量?
5.使用commit一定是同步提交吗?
好,带着疑问开始分析源码,本文的源码是基于Android 6.0
首先从SharedPreferences的获取说起
SharedPreferences preferences = context.getSharedPreferences(fileName, Context.MODE_PRIVATE);
Context是一个抽象类,它的实现类是ContextImpl
private static ArrayMap> sSharedPrefs;
public SharedPreferences getSharedPreferences(String name, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
if (sSharedPrefs == null) {
sSharedPrefs = new ArrayMap>();
}
final String packageName = getPackageName();
ArrayMap packagePrefs = sSharedPrefs.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap();
sSharedPrefs.put(packageName, packagePrefs);
}
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
sp = packagePrefs.get(name);
if (sp == null) {
File prefsFile = getSharedPrefsFile(name);
sp = new SharedPreferencesImpl(prefsFile, mode);
packagePrefs.put(name, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
}
return mPreferencesDir;
}
}
代码不多,getSharedPreferences相关代码全上了。getSharedPreferences需要传入两个参数,一个是文件的名字,一个是创建的文件模式,这个文件模式是什么意思呢?其实就是在创建文件时指定一些行为:是否可以被其它应用读或者写,是否可以跨进程使用等等,这里就不细讲了,一般指定为Context.MODE_PRIVATE,表示文件只能在本应用中访问。
代码还是挺简单清晰的,我们总结一下从上面这几段代码可以获得的信息就好
1.我们获得的SharedPreferences实例的具体类型是SharedPreferencesImpl,因此后面对SharedPreferences的使用的具体实现都是在SharedPreferencesImpl里;
2.SharedPreferences实例会在内存中缓存起来,多次调用getSharedPreferences获得的是同一个对象;
3.文件是以传入的name + “.xml”为文件名,存储在data/data/包名/shared_prefs目录下;
接着看看SharedPreferencesImpl的构造函数
private final File mFile;
private final File mBackupFile;
private final int mMode;
private Map mMap; // guarded by 'this'
private boolean mLoaded = false;
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
这里我们先来总结一下各个变量的含义,接下来的分析需要用到这些变量,因此要先了解了变量的作用才能更好的分析,其实这些变量也是挺好理解的。
mFile:代表我们要创建的保存数据的文件,这只是一个File类型的对象,真正的文件还没创建出来的哦
mBackupFile:代表一个备份文件,后面出现的时候再分析
mMap:内存中保存数据的数据结构,前面提出的疑问SharedPreferences和map的使用很类似就是因为有它
mLoaded:就是一个状态变量,代表是否已经全部从磁盘文件mFile加载数据到内存mMap中
在SharedPreferencesImpl的构造函数中还有这么一句代码startLoadFromDisk(),它的作用就是从磁盘文件中加载数据到内存中。
startLoadFromDisk
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
synchronized (SharedPreferencesImpl.this) {
loadFromDiskLocked();
}
}
}.start();
}
private void loadFromDiskLocked() {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (XmlPullParserException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (FileNotFoundException e) {
Log.w(TAG, "getSharedPreferences", e);
} catch (IOException e) {
Log.w(TAG, "getSharedPreferences", e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
}
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap();
}
notifyAll();
}
startLoadFromDisk主要是起了一个线程去读取文件数据到一个mMap中。这里会先判断一下mBackupFile是否存在,如果存在就删掉mFile,mBackupFile重命名为mFile。这几句代码是什么意思呢?到后面分析数据在写入磁盘文件时就会知道,如果mBackupFile文件存在,其实代表了在写的过程中发生了异常,如果在发生异常的时候用户刚好杀死了应用进程,下次再使用SharedPreferences时这个mBackupFile就起作用了,它会直接重命名为mFile,然后接着从mFile中读取数据到mMap中。当然,如果在写文件发生异常的时候刚好用户杀死了进程,那么这次要写入文件的新数据就会丢失,因为mBackupFile是一个写数据前的备份文件,新数据还没写入磁盘文件中。但这也是没办法的事,况且这种情况概率还是比较小的。
将数据写到mMap中的过程就不看了,不在本文要分析的范围之内。最后还调用了一下notifyAll,我们知道startLoadFromDisk中是起了一个新的线程去读去文件数据的,假如还没读取完就有人通过get的操作去获取数据了,这时候肯定是获取不到的,怎么办呢?那就是在get的时候判断一下mLoaded是否为true,即是否已经读取完,如果还没有,那就要让get的线程等待,等到读取完的时候再唤醒就OK了,因此这里的notifyAll就是起了唤醒的作用。
好,那就先看看获取数据的过程来验证一下
以getString为例
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
获取过程很简单,主要就是从mMap中读取,分析到这里可以知道,在读取的时候没有访问文件的操作,只是从内存中读取。
从mMap中读取之前还有调用了awaitLoadedLocked方法,这个方法是干嘛用的呢,进去看看
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
wait();
} catch (InterruptedException unused) {
}
}
}
这里就验证了前面说的当还没读取完(mLoaded为false)的时候,线程需要等待
接着分析写数据过程。从前面的用例代码可以知道,我们并不是直接调用SharedPreferences的相关put方法,而是先调用了SharedPreferences的edit方法返回一个Editor对象,然后再调用Editor对象的相关put方法,那Editor是干嘛用的呢?
首先看一下SharedPreferencesImpl对edit方法的实现
public Editor edit() {
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
写的过程也是要等待前面loadFromDiskLocked读取完的,好,知道了Editor具体的实现类为EditorImpl,那就来看看这个EditorImpl吧
public final class EditorImpl implements Editor {
private final Map mModified = Maps.newHashMap();
private boolean mClear = false;
EditorImpl只有两个成员变量,mClear就不分析了,mModified是一个map,存储的是这次要写入的数据,因为一次写的过程是可以写入多条数据的,因此需要一个临时的数据结构存储这次要写入的数据,EditorImpl就是起了这个作用的
下面以putString为例分析
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
很简单,就是把键值对保存在mModified中
最后来看一下EditorImpl的commit方法和apply方法,两个方法代码很类似,代码就一起上了
public boolean commit() {
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
commit和apply整体代码很类似
相同点:都是先commitToMemory把数据写到内存,然后enqueueDiskWrite把数据写到磁盘文件,最后notifyListeners通知监听者;
不同点:commit在调用enqueueDiskWrite时传入的postWriteRunnable为空,而apply是会传入一个不为空的postWriteRunnable,enqueueDiskWrite内部就是根据postWriteRunnable是否为空来判断是同步提交还是异步提交;commit在notify前有一个mcr.writtenToDiskLatch.await的等待而apply没有,apply有一个awaitCommit的Runnable,并且是在postWriteRunnable的run中执行的;最后commit还有个返回值表示commit写入的结果。
下面开始分析代码
首先调用了一个commitToMemory方法,接着调用enqueueDiskWrite方法,即先把内容提交到内存,然后再写入磁盘文件中。最后调用notifyListeners(mcr)将结果mcr通知监听者。
不看不知道,一看源码才发现原来使用SharedPreferences还可以监听提交结果的,原来我们可以使用registerOnSharedPreferenceChangeListener对提交结果进行监听,那对什么结果监听呢,自然是值发生变化的key了,notifyListeners会保证在主线程回调
public interface OnSharedPreferenceChangeListener {
void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
}
看到commit的代码不知道大家有没有这样一个疑问,既然commit是同步提交的,那这里的mcr.writtenToDiskLatch.await()岂不是多余的了?
接下来通过阅读代码解开这个谜团,先来看看cr.writtenToDiskLatch是个什么东西先,
private static class MemoryCommitResult {
...
public final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
...
}
原来mcr.writtenToDiskLatch是一个CountDownLatch类型的对象,那CountDownLatch的作用是什么呢?CountDownLatch是一个计数同步器,协调多个线程之间的工作,作用类似于Object的wait和notify,即一个线程的执行依赖于其它线程,并且还可以依赖多个其它线程,线程数量可以由CountDownLatch的构造函数参数指定。当一个线程调用CountDownLatch的await方法时会阻塞等待,只有其它线程执行CountDownLatch的countDown方法并且countDown的次数为构造函数传入的参数时,会唤醒等待的线程继续执行。
那么这里commit中await的意思是通知监听者和返回mcr.writeToDiskResult这两个动作要等到执行完写磁盘文件的操作后,那么写文件操作里肯定会有countDown方法的调用。但是如果是同步的话,为什么还要await呢?带着这个疑问继续分析
好,先来看看commitToMemory
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
...
if (mDiskWritesInFlight > 0) {
...
mMap = new HashMap(mMap);
}
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
mcr.keysModified = new ArrayList();
mcr.listeners =
new HashSet(mListeners.keySet());
}
synchronized (this) {
if (mClear) {
if (!mMap.isEmpty()) {
mcr.changesMade = true;
mMap.clear();
}
mClear = false;
}
for (Map.Entry e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mMap.containsKey(k)) {
continue;
}
mMap.remove(k);
} else {
if (mMap.containsKey(k)) {
Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mMap.put(k, v);
}
mcr.changesMade = true;
if (hasListeners) {
mcr.keysModified.add(k);
}
}
mModified.clear();
}
}
return mcr;
}
代码有点长,挑重点看,首先会实例化一个MemoryCommitResult类型的mcr,并且mcr.mapToWriteToDisk指向了mMap,mapToWriteToDisk代表此次要写入磁盘文件的数据;紧接着有一个for循环遍历mModified,mModified就是前面我们put进入的内容,然后将mModified的内容保存到mMap中,这样内存中就有了我们要提交的内容,因为在执行前mcr的mapToWriteToDisk指向了mMap,因此执行完commitToMemory后,mcr中mapToWriteToDisk就有了我们这次要写入磁盘文件的数据了。这里就要注意了!!!这个mapToWriteToDisk包含的数据不单单是这次要写入的数据(也就是此次put的数据),而是包含了此次要写入的数据和写入前的数据,也就是所有的数据!!!也就是说每次调用commit的时候都是将所有的数据重新写入磁盘文件中的。
这里不得不说,阅读了源码之后对SharedPreferences有了更深的认识,我们都知道SharedPreferences是一种很轻量级的文件存储方式,因此我还以为写文件的时候写增量写入,没想到是全量写入,真是直接粗暴。
接下来看看写入磁盘文件中的代码
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
enqueueDiskWrite方法有两个参数,一个是提交到内存结果的MemoryCommitResult,一个是Runnable对象,其中commit方法传进来的Runnable为null,因此会进入if判断里执行writeToDiskRunnable的run方法,我们知道直接调用Runnable对象run方法里面的代码是运行在当前线程中的。这也和我们前面的认知是一样的,也就是commit是同步提交的。
但是,不知道大家注意到没有,执行writeToDiskRunnable.run前还有个wasEmpty的判断,只有wasEmpty为true的时候才会直接调用writeToDiskRunnable的run方法,如果为false就会调用到下面这句代码了,QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable),其中QueuedWork.singleThreadExecutor()返回的是1个核心线程的线程池,也就是会将writeToDiskRunnable扔到线程池中异步执行,那什么时候wasEmpty会为false呢?当然如果前面isFromSyncCommit为false(postWriteRunnable不为空),就直接异步执行了,这种情况就是当调用apply而不是commit的时候传进的postWriteRunnable不为空,后面再分析;
我们来看看wasEmpty赋值的地方,wasEmpty = mDiskWritesInFlight == 1;就是mDiskWritesInFlight等于1的时候wasEmpty为true,否则就为false,看来得分析一下mDiskWritesInFlight了,
private int mDiskWritesInFlight = 0;
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
...
mDiskWritesInFlight++;
...
return mcr;
}
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
...
}
};
首先mDiskWritesInFlight是一个全局变量,初始值为0,当调用完一次commitToMemory写内存操作后mDiskWritesInFlight变为1,当执行完一次writeToDiskRunnable写文件操作后mDiskWritesInFlight重新变为0,而每一次的commit或者apply包含了上面两步操作。
正常的一次commit或者apply自然没问题,那如果短时间内多次commit或者apply呢?mDiskWritesInFlight++是内存的操作,同步执行的,不会有问题,而mDiskWritesInFlight--是在写磁盘文件的操作里的,是一个耗时操作,并且还可能是异步执行的,那么有可能出问题的就是在这里了。如果我们执行一次apply,mDiskWritesInFlight先变为1,然后异步执行writeToDiskRunnable,因为writeToDiskRunnable是耗时操作并且是异步执行的,如果writeToDiskRunnable执行完之前又进行了一次commit呢?大家细看一下mDiskWritesInFlight--的同步代码是在writeToFile之后的,也就是说执行完writeToFile前进行的这次commit是又会执行了一次mDiskWritesInFlight++操作的,而这时mDiskWritesInFlight变为了2,那当执行到wasEmpty = mDiskWritesInFlight == 1的时候,wasEmpty是为false的,也就是说,此次的commit,writeToDiskRunnable是放到线程池中异步执行的!!!这就有点出乎意料了!
分析到这里其实就可以解释上面的谜团了,就是commit中为什么还要有mcr.writtenToDiskLatch.await()阻塞的代码,因为有可能commit是异步执行的,而commit却还要有个写文件的返回结果,因此当前线程必须阻塞,等待写文件的完成。当然,如果commit最终是同步执行的,await并不会阻塞,因为写文件在先,写文件后是会调用countDown的,因此await不会阻塞。
但是为什么commit也要考虑异步的情况呢?下面只是一点自己的猜测,不一定正确,希望大家有不同的意见可以分享讨论一下。
一开始我认为这主要是为了主线程设计的。因为commit有可能是同步提交,那么它就有可能是在主线程执行的,而writeToDiskRunnable写文件又是一个耗时的操作,如果上一次的apply的writeToDiskRunnable耗时过长,具体是writeToFile耗时过长,而此次的commit又是在主线程执行,那么就会在执行到writeToFile的时候和上一次apply(commit)的writeToFile竞争同一把锁mWritingToDiskLock而挂起,这样主线程就会因为上一次的apply而陷入阻塞,所以只好把它扔到线程池中执行了。
但是很快我否认了这一想法,因为commit是有返回值的,commit中在写文件后还有一句代码mcr.writtenToDiskLatch.await();这句话的作用前面已经说过,主要是要等待写文件操作的完成,然后将返回值返回给调用者,如果前面那里是为了主线程设计而把写操作异步执行,那这里的await等待就和这个设计思想矛盾了。
后来我从下面这段话看到了些端倪
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
重点关注postWriteRunnable为null的那句话
if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
英语不好,试着翻译一下。如果postWriteRunnable为null(一个常规的commit调用),允许我们在主线程进行写磁盘操作,这除了减少资源分配和创建后台线程外,还有一个好处就是。。。
这完全没必要啊。。。上面我辛辛苦苦的为了你寻找了一个为了优化而把写磁盘文件操作放到线程中去执行的理由,而你却无情的写着为了优化而把写磁盘文件放到主线程执行!
嗯,好像倒过来也说的通哦,如果前一次的apply顺利的写入了文件,那么就允许这次的commit在主线程执行,还能减少资源分配和不必要的后台线程的创建呢。
最后来分析一些真正写文件操作
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mcr.changesMade) {
...
mcr.setDiskWriteResult(true);
return;
}
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false);
return;
}
} else {
mFile.delete();
}
}
...
try {
FileOutputStream str = createFileOutputStream(mFile);
if (str == null) {
mcr.setDiskWriteResult(false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
FileUtils.sync(str);
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (this) {
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
mcr.setDiskWriteResult(true);
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false);
}
代码有点长,但整体逻辑并没有很复杂。写文件前先把文件mFile重命名为mBackupFile,这就是mBackupFile备份文件的由来;然后根据mFile创建一个FileOutputStream,将mcr的mapToWriteToDisk里面的数据写入磁盘文件,成功后最后会把备份文件删掉;写文件过程如果发生异常,会将mFile文件删掉。这里要注意哦,这个mFile文件不是原来的数据文件了,这只是一个不完整的文件,原来的数据文件已经重命名为mBackupFile了。这时候可能有人就有疑问了,这样的话岂不是数据文件都不存在了?那不会影响读写吗?
回答这个问题之前先来解决文章开头的第一个疑问,数据文件是什么时候创建的。代码分析到这里,SharedPreferences的创建,读的过程,写的过程其实都已经分析到了,但好像没有看到数据文件mFile的创建的代码啊?哈哈,其实在根据mFile创建FileOutputStream的时候也是会创建文件的,也就是在写的时候创建的文件,并且是每次写都会创建一个新的文件,因为前面分析过写之前是会将mFile的文件重命名为mBackupFile的,因此每次写之前mFile代表的文件都是不存在的。
那如果写的过程中发生异常,mFile代表的数据文件是不存在的,是否会影响读写?读的过程前面分析过了,只是读的内存,在写文件前已经将数据写入内存,因此读不会有问题;至于写的过程,刚刚上面一段话已经说的很清楚了,每次写文件前mFile代表的数据文件都是不存在的,每次写都会通过构造一个FileOutputStream创建一个新的mFile代表的数据文件,因此写的过程也不会有问题。
前面说过写文件完成后应该是有一次countDown操作的,就是通过mcr.setDiskWriteResult进行countDown的
public void setDiskWriteResult(boolean result) {
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
到这里代码就分析完了,现在来回答开头提出的疑问
1.这个文件是在什么时候创建的?获取SharedPreferences实例的时候?第一次读的时候?第一次写?
写的时候创建的,并且是每次写都会创建一个新的文件。
2.SharedPreferences在使用时和map有点类似,它是以文件的形式保存内容的,那它是如何实现类似map的使用的
其实在使用SharedPreferences的过程中就是对HashMap的操作,读的是HashMap的内容,写也是将HashMap的数据写入文件。
3.SharedPreferences每次读写都要访问文件吗?这样效率岂不是很低下
读的时候访问的是内存,因此读的时候不会有效率问题;而写的时候是全量写入的,因此平时在使用的时候还是要注意一下的,SharedPreferences的数据不适宜过多
4.数据写到磁盘文件的时候是增量写入还是全量?
全量写入。
5.使用commit一定是同步提交吗?
不一定,同步异步都有可能。
总结:虽然SharedPreferences源码不多,但文章写下来还是挺长的,对SharedPreferences的整体架构基本分析了一遍,遗憾的是由于个人能力有限,还是有些点没分析到的,比如说SharedPreferences的跨进程使用、apply中的设计思想(QueuedWork、awaitCommit、postWriteRunnable),但总体上说还是收益颇多的,对SharedPreferences有了更深的认识。
都说书中自有黄金屋,Android源码就是我们Android开发者的黄金屋,那里有很多的宝藏等待我们去挖掘!