SharedPreferences源码分析
先给出结论,如果不想跟随源码分析的,可以根据结论,对SharedPreferences有个大概的了解。
结论:
1.SharedPreferences 线程是安全的,内部由大量synchronized
代码块实现同步
2.SharedPreferences 进程是不安全的(虽然官方提供了 ),但官方文档也给出了如下说明:MODE_MULTI_PROCESS
加载模式,该加载模式在API级别11中添加,在API级别23中被废弃。该SharedPreference loading标志:设置后,即使已在此过程中加载了共享首选项实例,也会检查磁盘上的文件是否已修改。在应用程序具有多个进程的情况下,有时需要此行为,所有进程都写入相同的SharedPreferences文件。但是,通常在进程之间存在更好的通信形式。
此常量在API级别23中已弃用
.MODE_MULTI_PROCESS在某些Android版本中无法可靠地工作,并且不提供任何协调跨进程的并发修改的机制。应用程序不应尝试使用它。相反,他们应该使用明确的跨流程数据管理方法,例如ContentProvider。
3.首次getSharedPreferences
会把文件从磁盘加载到内存(用Map进行保存),之后调用getSharedPreferences
获取数据,即从内存中直接获取,也就不会由于大量的synchronized
而影响性能
4.apply
:同步修改内存中的数据,然后异步回写到磁盘且是将其放入单线程任务队列中执行
commit
:同步修改内存中的数据,且同步回写到磁盘,因此会阻塞当前线程
建议:
1.不要使用多进程操作SharedPreferences ,小概率会出现数据丢失(文件被删除)
2.不要存储大量的数据,从磁盘加载SharedPreferences 文件是进行I/O操作的,并且由于文件结构是xml格式的,加载解析都比较耗时,虽然在此过程中是异步的,但如果数据过大,首次调用getSharedPreferences
后,然后马上执行getXX()
或者putXX()
由于需要等待加载文件结束,可能会阻塞线程(如果在UI线程可能会引发ANR)。并且SharedPreferences 中的数据会一次性从磁盘加载到内存,.apply
、commit
都是会将数据写到磁盘的,SharedPreferences 文件不应过大会影响性能
3.尽量使用apply更新数据,因为它是异步写入磁盘的,不会阻塞线程
将您的首选项更改从此编辑器提交回它正在编辑的SharedPreferences对象。这将自动执行请求的修改,替换SharedPreferences中当前的内容。
注意,当两个编辑器同时修改首选项时,最后一个调用apply的编辑器将获胜。
与同步地将其首选项写到持久存储的commit()不同,apply()将其更改立即提交到内存中的SharedPreferences,但是启动到磁盘的异步提交,并且不会通知您任何失败。如果这个SharedPreferences上的另一个编辑器在apply()仍然未完成时执行常规提交(),那么commit()将阻塞,直到所有异步提交和提交本身都完成为止。
由于SharedPreferences实例是流程中的单例实例,如果已经忽略了返回值,那么可以用apply()替换commit()的任何实例。
您不需要担心Android组件的生命周期以及它们与向磁盘写入apply()的交互。该框架确保在切换状态之前完成来自apply()的在途磁盘写操作。
源码分析:
我们通过调用getSharedPreferences
获取SharedPreferences对象时,最终会调用ContextImpl.getSharedPreferences(File file, int mode)
方法:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//获取缓存中所有SharedPreferences对象集合
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
//缓存中是否已存在我们需要的SharedPreferences
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");
}
}
//如果缓存中不存在,则通过创建SharedPreferencesImpl对象,由其加载文件
sp = new SharedPreferencesImpl(file, mode);
//将SharedPreferences对象存入缓存集合中,以便下次直接从内存中获取
cache.put(file, 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;
}
先从缓存中获取,如果未命中则创建SharedPreferencesImpl对象,并保存到缓存中,多次调用getSharedPreferences
并不会多次创建对象,由于整个SharedPreferences获取过程中都放在synchronized
代码块中,因此在多线程下创建对象是安全的
SharedPreferencesImpl
创建过程:
SharedPreferencesImpl(File file, int mode) {
//需要加载的SharedPreferences文件对象
mFile = file;
//备份文件对象,写入失败时会通过其进行数据恢复
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
//数据缓存集合,用于存储SharedPreferences文件中的数据
mMap = null;
mThrowable = null;
//从磁盘加载数据
startLoadFromDisk();
}
下面我们看看从磁盘加载数据过程:
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//从磁盘加载数据
loadFromDisk();
}
}.start();
}
另起一个线程加载SharedPreferences文件
private void loadFromDisk() {
synchronized (mLock) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
···省略代码···
str = new BufferedInputStream(new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
···省略代码···
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
···省略代码···
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
...
mLock.notifyAll();
}
}
从上面方法可知从磁盘加载SharedPreferences文件分别做了一下几步处理:
1.如果由备份文件,把原文件删除,然后修改备份文件名称,使其成为原文件,达到文件恢复效果(备份文件名以.bak结尾)
2.加载文件并解析成Map
3.标记加载完成
4.将Map赋值给mMap(即数据缓存集合,用于存储SharedPreferences文件中的数据)
5.通过notifyAll
唤起所有等待加载的线程
现在SharedPreferences对象已经初始化成功了,下面我们看一下对其数据操作的流程:
getXX()
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//等待SharedPreferences加载完成
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
通过synchronized
代码块,可见获取数据是线程安全的,仅仅是从缓存中获取数据,同步不会影响性能。而awaitLoadedLocked()
即是等待SharedPreferences文件从磁盘加载到内存完毕后再执行下面的获取数据的方法,因此如果在首次调用getSharedPreferences
时,马上调用getXX()
,会阻塞线程
接着看看awaitLoadedLocked()
里面做了如何处理:
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
可见,除了首次加载文件时,mLoaded = false
会卡在此处等待,当文件加载完毕后会重置加载状态即mLoaded = true
,则不会等待直接获取数据
putXX()
修改数据时首先得获取Editor对象(SharedPreferencesImpl.java)
@Override
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
···代码省略···
}
可见我们调用putXX()
仅仅是将数据存到mModified
中,直到我们调用apply
或commit
时才真正将数据同步到缓存并写入磁盘
apply()
@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);
// 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);
}
下面看看同步缓存数据流程:
private MemoryCommitResult commitToMemory() {
···代码省略···
synchronized (SharedPreferencesImpl.this.mLock) {
···代码省略···
//缓存数据集合
mapToWriteToDisk = mMap;
···代码省略···
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;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,mapToWriteToDisk);
}
然后我们瞅瞅写入磁盘SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
//如果是commit则postWriteRunnable == null即isFromSyncCommit = true
//如果是apply则isFromSyncCommit = false
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();
}
}
};
// commit直接在当前线程直接同步写入磁盘
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//apply加入任务队列执行写入磁盘runnable
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
//sCanDelay用于纪录当前线程是否处于写入磁盘任务状态
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
//首次创建一个线程
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
//创建一个handler用于接收queue(Runnable work, boolean shouldDelay)发送的消息,然后执行写入磁盘runnable
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
commit()
@Override
public boolean commit() {
long startTime = 0;
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/apply都回调用SharedPreferencesImpl.this.enqueueDiskWrite
然后同步/异步执行写入磁盘操作