我们经常用SharedPreferences用来存储一些比较小的键值对集合,适合保存应用的配置参数, 我们将会带着以下几个问题来分析SharedPreferences的源码实现:
- 数据是如何保存到磁盘的
- commit() 和apply()的区别
- 为什么会造成ANR
- SharedPreferences有哪些缺点
源码分析
本文参照Android-26的源码,并不介绍SharedPreferences的基础使用,而是从源码角度来分析它的原理
获取SharedPreferences
我们通过以下方法来获取SharedPreferences实例
- context.getSharedPreferences
- 在Activity中getSharedPreferences
- PreferenceManager.getDefaultSharedPreferences
这三种方法最终都会调用到 ContextImpl.getSharedPreferences
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
//SharedPreferences对应的xml文件,数据保存在其中
File file;
synchronized (ContextImpl.class) {
...//省略
file = mSharedPrefsPaths.get(name);
if (file == null) {
//如果没有该name命名的文件,则新建一个并放入缓存
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, 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) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
//mode设置为多进程模式时会检测SP文件最后修改的时间和大小,如果文件被其他进程改变时,则会重新加载
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
可以看到最终返回的是一个SharedPreferencesImpl对象,首先getSharedPreferencesCacheLocked()从一个静态的ArrayMap中获取SharedPreferences 缓存,如果有缓存中有SharedPreferencesImpl对象则返回,没有的话则创建一个并存入缓存中,同时synchronized 包裹可以保证多线程同步,由此可见无论getSharedPreferences调用多少次,返回的都是一个SharedPreferencesImpl对象
SharedPreferencesImpl
SharedPreferencesImpl 实现了SharedPreferences这个接口,是我们通过getSharedPreferences得到的实体对象,所有存取操作都由该类来实现
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file); //备份文件
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
mBackupFile 代表发生异常时, 可通过备份文件来恢复数据.
mLoaded 表示是否已经将mFile中的数据都读取到mMap 中
mMap 用于在内存中缓存我们的配置数据, 也就是 getXxx 数据的来源
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 map = null;
BufferedInputStream str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
//...省略
synchronized (mLock) {
mLoaded = true;
if (map != null) {
mMap = map;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
开启一个子线程来从硬盘读取数据,如果备份文件存在则直接使用灾备文件回滚,使用XmlUtils把文件所有的数据读取到内存中的mMap中,mLoaded = true 标志SharedPreferencesImpl已经将数据读取完成,notifyAll()唤醒getXXX系列方法等待状态的线程,由于已经将数据中磁盘读取到内存中,此时调用getXXX系列方法就可以获取值了
getString分析
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
synchronized 关键字保证了线程安全,然后直接从mMap中获取对应的键值对就可以了,当我们调用getSharedPreferences 之后马上调用getString方法有可能SharedPreferencesImpl在子线程中还没有将文件中的数据读取完,此时mMap 还没有被赋值,所以awaitLoadedLocked()将会阻塞当前线程,直到读取完毕
private void awaitLoadedLocked() {
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
}
mLoaded为false表示尚未读取完成,其他的getXXX系列方法和getString如出一辙,都是先等待文件读取完毕,然后从mMap中获取相应的value
数据保存
我们通过getSharedPreferences().edit()来put各种值,看一下.edit()获取的是一个什么对象
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
保证磁盘读取完毕后,返回了一个新的EditorImpl对象
public final class EditorImpl implements Editor {
private final Object mLock = new Object();
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
public Editor putString(String key, @Nullable String value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putInt(String key, int value) {
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor remove(String key) {
synchronized (mLock) {
mModified.put(key, this);
return this;
}
}
...//省略
}
EditorImpl 中有两个重要属性,mModified 用来暂时保存put方法提供的值,当调用commit()或者apply()才会将mModified中的数据存储到mMap,进而保存到磁盘中,mClear标志是否要清空文件中所有数据。接下来需要注意看remove()方法,调用getSharedPreferences().edit().remove()时是将当前key的value置为this,删除数据时检测到value为this即可删除
总结:调用put()后,数据只是暂存到了EditorImpl 的mModified** 对象中,并没有回写到磁盘,调用commit()或apply才会将数据写到磁盘中**
commit()
public boolean commit() {
...//省略
MemoryCommitResult mcr = commitToMemory();
SharedPreferencesImpl.this.enqueueDiskWrite(
mcr, null /* sync write on this thread okay */);
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException e) {
return false;
}
return mcr.writeToDiskResult;
}
主要有三步
- commitToMemory将mModified 中的数据写到内存mMap中
- SharedPreferencesImpl.this.enqueueDiskWrite 将内存中mMap的数据回写到磁盘中
- mcr.writtenToDiskLatch.await() 线程等待,直到回写磁盘完毕
-
commitToMemory()
我们逐个分析,首先分析commitToMemory()返回一个MemoryCommitResult对象,代表了提交到内存的返回结果
private static class MemoryCommitResult {
//...省略代码
final Map<String, Object> mapToWriteToDisk;
//此处初始换CountDownLatch 的计数器为1
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
volatile boolean writeToDiskResult = false;
boolean wasWritten = false;
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
其中关键有 writtenToDiskLatch 是一个 CountDownLatch 对象,它允许一个或多个线程一直等待,直到回写磁盘线程的操作执行完后再执行,mapToWriteToDisk引用内存中的mMap,writeToDiskResult代表回写磁盘是否成功,接下来继续分析commitToMemory()
private MemoryCommitResult commitToMemory() {
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
mapToWriteToDisk = mMap;
//需要写入磁盘次数+1
mDiskWritesInFlight++;
synchronized (mLock) {
if (mClear) {
//...省略代码,
//如果调用了edit().clear()则清空内存中的数据
mMap.clear();
mClear = false;
}
//将putXXX()的数据提交到内存中
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
//value为this则删除,与之前的getSharePreferences().edit().remove()对应
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);
}
}
mModified.clear();
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
mDiskWritesInFlight代表写入磁盘这个操作的次数,也是由synchronized 保证线程安全,首先判断是否需要clear,如果需要这把mMap中的数据清空,需要注意此时mModified中数据还没有复制到mMap中,所以以下代码并不能将"foo" clear掉
sharedPreferences.edit()
.putBoolean("foo";, true) // foo 无法被 clear 掉
.clear()
.putBoolean("bar", true)
.commit()
然后通过for循环将put到mModified中的数据添加到mMap中,mModified.clear()之后返回MemoryCommitResult
总结commitToMemory()只是将数据都写入到内存中
- SharedPreferencesImpl.this.enqueueDiskWrite
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
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);
}
commit() 时postWriteRunnable参数为null,所以isFromSyncCommit == true,进入到if (isFromSyncCommit) 语句中,如果此时只有一个commit()操作,则直接在当前线程执行writeToFile()将内存中的数据回写到磁盘中,如果此时有多个commit()则,排队进入QueuedWork中等待执行,看一下writeToFile()的实现
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
//...省略
boolean fileExists = mFile.exists();
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;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
str.close();
mBackupFile.delete();
mcr.setDiskWriteResult(true, true);
return;
} catch (Exception e) {
}
//如果写入操作出现异常,则将半成品删掉
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
- 将之前的配置文件mFile备份为buckup文件,然后删除
- mcr.mapToWriteToDisk即内存中数据,全部写入到新的mFile中
- 写入成功,删掉备份文件,如果写入失败则把半成品mFile删掉
-
mcr.writtenToDiskLatch.await()
CountDownLatch.await()会阻塞当前线程,直到CountDownLatch.countDown()使计数器值到达0时,它表示磁盘写入线程已经完成了任务,然后在锁上等待的线程就可以恢复执行任务。在writeToFile()中,写入完成之后会调用mcr.setDiskWriteResult()中的writtenToDiskLatch.countDown()
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
writtenToDiskLatch初始时计数器为1,countDown()之后为0,此时磁盘已经回写完毕,commit()方法继续执行,返回结果
commit()总结:
- 流程是先写入内存在写入磁盘
- 写入磁盘完成之前调用线程会一直等待,直到内存和磁盘都已经同步完毕
- 每次写入磁盘时都会从内存中将所有数据都全量写入,效率并不高
apply()
public void apply() {
//第一步:提交到内存
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public 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() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 第三步:写入磁盘
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
- 第一步提交到内存和commit()是一样的
- 第二步中将任务mcr.writtenToDiskLatch.await()提交到QueuedWork之中,该任务的作用是让线程等待,而释放的时机跟commit()一样(详细代码看上述commit()),但是QueuedWork.addFinisher()将线程等待的任务提交之后并没有立即运行,而是保存在了一个队列之中,当应用收到系统广播,或者被调用 onPause 等一些时机才会运行(详情查看QueuedWork源码,在ActivityThread中可以找到调用任务的方法waitToFinish())
- 第三步同commit,不同点在于enqueueDiskWrite(mcr, postWriteRunnable)传递了Runnable,在异步线程中写入磁盘
apply总结 - 异步写入磁盘,没有等待结果,直接返回
- 应用收到系统广播,或者被调用 onPause等时机,如果磁盘写入未完成则主线程会等待其完成
- 和commit()写入过程一样,都是全量写入
SharedPreferences总结
通过上文对SharedPreferences分析,我们已经可以对开头的几个问题进行回答并总结了
- 数据是如何保存到磁盘的
答:通过putXXX系列方法将数据先保存到内存中,调用commit()或者apply(之后将所有数据全量写入磁盘文件中 - commit() 和apply()的区别
答:commit()线程同步写入,写入完成时才会返回,如果在主线程调用,写入过程比较费时可能会阻塞主线程,
apply异步线程写入,但是应用收到系统广播,或者被调用 onPause等时机,未完成写入任务时主线程会等待其完成 - commit()和apply()相同点
答:都是全量写入,如果SharedPreferences中数据量很多,则每次写入都会很慢 - 为什么会造成ANR
答:commit()和apply()都可能在成ANR,分析如上 - SharedPreferences有哪些缺点
答:1. 全量写入:commit() 还是 apply(),即使我们只改动其中一条数据,都会把整个数据写入到文件中
2. 卡顿:commit() 还是 apply()都有可能造成ANR
3. 跨进程不安全:MODE_MULTI_PROCESS已被谷歌标为Deprecated
总之:系统提供的 SharedPreferences 的应用场景是用来存储一些简单、轻量的数据,例如配置文件等,不适合json、html等,并且每个SharedPreference不宜过大,考虑将频繁修改的配置项单独隔离