SharedPreferences
前言
转载
我们知道 SharedPreferences 会从文件读取 xml 文件, 并将其以 getXxx/putXxx 的形式提供读写服务. 其中涉及到如下几个问题:
- 如何从磁盘读取配置到内存
- getXxx 如何从内存中获取配置
- 最终配置如何从内存回写到磁盘
- 多线程/多进程是否会有问题
- 最佳实践
1. 结论
- SharedPreferences 是线程安全的,内部由大量 synchronized 关键字保障。
- SharedPreferences 不是进程安全的。
- 第一次 getSharedPreferences 会读取磁盘文件,后续的 getSharedPreferences 会从内存缓存中获取。 如果第一次调用 getSharedPreferences 时还没从磁盘加载完毕就调用 getXxx/putXxx , 则 getXxx/putXxx 操作会卡主,直到数据从磁盘加载完毕后返回。
- 所有的 getXxx 都是从内存中取的数据
- apply 是同步回写内存,然后把异步回写磁盘的任务放到一个单线程的队列中等待调度。commit 和前者一样,只不过要等待异步磁盘任务结束后才返回。
- MODE_MULTI_PROCESS 是在每次 getSharedPreferences 时检查磁盘上配置文件上次修改时间和文件大小, 一旦所有修改则会重新从磁盘加载文件。 所以并不能保证多进程数据的实时同步。
- 从 Android N 开始,不再支持 MODE_WORLD_READABLE & MODE_WORLD_WRITEABLE。一旦指定,会抛异常。
2. 最佳实践
- 不要多进程使用,很小几率会造成数据全部丢失(万分之一), 现象是配置文件被删除。
- 不要依赖 MODE_MULTI_PROCESS,这个标记就像 MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE 未来会被废弃。
- 每次 apply / commit 都会把全部的数据一次性写入磁盘,所以单个的配置文件不应该过大, 影响整体性能。
3. 源码分析
3.1 SharedPreferences 对象的获取
一般来说有如下方式:
- PreferenceManager.getDefaultSharedPreferences
- ContextImpl.getSharedPreferences
我们以上述 [1] 为例来看看源码:
// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
可以看到最终也是调用到了 ContextImpl.getSharedPreferences, 源码:
// ContextImpl.java
@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);
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 = new SharedPreferencesImpl(file, mode);
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;
}
可见 sdk 是先取了缓存,如果缓存未命中,才构造对象。也就是说,多次 getSharedPreferences 几乎是没有代价的。 同时, 实例的构造被 synchronized
关键字包裹,因此构造过程是多线程安全的。
3.2 SharedPreferences 的构造
第一次构建对象时:
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
有这么几个关键信息:
-
mFile
代表我们磁盘上的配置文件。 -
mBackupFile
是一个灾备文件,用户写入失败时进行恢复,后面会再说。其路径是 mFile 加后缀 ‘.bak’。 -
mMap
用于在内存中缓存我们的配置数据, 也就是 getXxx 数据的来源。
还涉及到一个 startLoadFromDisk, 我们来看看:
// SharedPreferencesImpl.java
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
开启了一个线程从文件读取, 其源码如下:
// SharedPreferencesImpl.java
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
... 略去无关代码 ...
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
notifyAll();
}
}
loadFromDisk
这个函数很关键。它就是实际从磁盘读取配置文件的函数。 可见, 它做了如下几件事:
- 如果有 ‘灾备’ 文件,则直接使用灾备文件回滚。
- 把配置从磁盘读取到内存的并保存在 mMap 字段中(看代码最后 mMap = map)。
- 标记读取完成, 这个字段后面
awaitLoadedLocked
会用到。 记录读取文件的时间,后面MODE_MULTI_PROCESS
中会用到。 - 发一个
notifyAll
通知已经读取完毕, 激活所有等待加载的其他线程。
总结一下:
从磁盘读取配置
3.3 getXxx 的流程
// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
可见, 所有的 get 操作都是线程安全的。并且 get 仅仅是从内存中(mMap) 获取数据, 所以无性能问题。
考虑到配置文件的加载是在单独的线程中异步进行的(参考 ‘SharedPreferences 的构造’),所以这里的 awaitLoadedLocked
是在等待配置文件加载完毕。 也就是说如果我们第一次构造 SharedPreferences 后就立刻调用 getXxx 方法, 很有可能读取配置文件的线程还未完成, 所以这里要等待该线程做完相应的加载工作。
来看看 awaitLoadedLocked 的源码:
// SharedPreferencesImpl.java
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for this
// thread, since the real read will be in a different
// thread and otherwise ignored by StrictMode.
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
很明显,如果加载还未完成(mLoaded == false),getXxx 会卡在 awaitLoadedLocked,一旦加载配置文件的线程工作完毕, 则这个加载线程会通过 notifyAll
会通知所有在 awaitLoadedLocked
中等待的线程,getXxx 就能够返回了。不过大部分情况下,mLoaded == true
。这样的话 awaitLoadedLocked
会直接返回。
3.4 putXxx 的流程
set 比 get 稍微麻烦一点儿,因为涉及到 Editor
和 MemoryCommitResult
对象。
先来看看 edit() 方法的实现:
// SharedPreferencesImpl.java
public Editor edit() {
// TODO: remove the need to call awaitLoadedLocked() when
// requesting an editor. will require some work on the
// Editor, but then we should be able to do:
//
// context.getSharedPreferences(..).edit().putString(..).apply()
//
// ... all without blocking.
synchronized (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
Editor
Editor 没有构造函数,只有两个属性被初始化:
// SharedPreferencesImpl.java
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;
... 略去方法定义 ...
public Editor putString(String key, @Nullable String value) { ... }
public boolean commit() { ... }
...
}
mModified
是我们每次 putXxx 后所改变的配置项。
mClear
标识要清空配置项, 但是只清了 SharedPreferences.mMap
。
edit() 会保障配置已从磁盘读取完毕,然后仅仅创建了一个对象。接下来看看 putXxx 的真身:
// SharedPreferencesImpl.java
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
很简单,仅仅是把我们设置的配置项放到了 mModified
属性里保存。等到 apply
或者 commit
的时候回写到内存和磁盘。咱们分别来看看。
apply
apply
是各种 ‘最佳实践’ 推荐的方式,那么它到底是怎么异步工作的呢?我们来看个究竟:
// SharedPreferencesImpl.java
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);
// 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);
}
可以看出大致的脉络:
-
commitToMemory
应该是把修改的配置项回写到内存。 -
QueuedWork.add(awaitCommit)
貌似没什么卵用。 -
SharedPreferencesImpl.this.enqueueDiskWrite
把配置项加入到一个异步队列中,等待调度。
我们来看看 commitToMemory
的实现(略去大量无关代码):
// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
... 略去无关 ...
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
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();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
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);
}
}
总结来说就两件事:
- 把
Editor.mModified
中的配置项回写到 SharedPreferences.mMap 中,完成了内存的同步。 - 把 SharedPreferences.mMap 保存在了
mcr.mapToWriteToDisk
中。而后者就是即将要回写到磁盘的数据源。
我们再来回头看看 apply 方法:
// SharedPreferencesImpl.java
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
... 略无关 ...
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
-
commitToMemory
完成了内存的同步回写 -
enqueueDiskWrite
完成了硬盘的异步回写, 我们接下来具体看看 enqueueDiskWrite
// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
...
}
};
...
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
QueuedWork.singleThreadExecutor 实际上就是 ‘一个线程的线程池’, 如下:
// QueuedWork.java
public static ExecutorService singleThreadExecutor() {
synchronized (QueuedWork.class) {
if (sSingleThreadExecutor == null) {
// TODO: can we give this single thread a thread name?
sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
return sSingleThreadExecutor;
}
}
回到 enqueueDiskWrite
中,这里还有一个重要的函数叫做 writeToFile
:
writeToFile
// SharedPreferencesImpl.java
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 (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
return;
}
} else {
mFile.delete();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
return;
}
// Clean up an unsuccessfully written file
mFile.delete();
}
代码大致分为三个过程:
- 先把已存在的老的配置文件重命名(加 ‘.bak’ 后缀), 然后删除老的配置文件。这相当于做了灾备。
- 向
mFile
中一次性写入所有配置项。即mcr.mapToWriteToDisk
(这就是 commitToMemory 所说的保存了所有配置项的字段) 一次性写入到磁盘。如果写入成功则删除灾备文件,同时记录了这次同步的时间。 - 如果上述过程 [2] 失败,则删除这个半成品的配置文件。
好了, 我们来总结一下 apply:
- 通过 commitToMemory 将修改的配置项同步回写到内存 SharedPreferences.mMap 中。此时,任何的 getXxx 都可以获取到最新数据了。
- 通过
enqueueDiskWrite
调用 writeToFile 将所有配置项一次性异步回写到磁盘, 这是一个单线程的线程池。
时序图:
apply 时序图commit
看过了 apply
再看 commit
就非常容易了。
// SharedPreferencesImpl.java
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;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
时序图:
commit 时序图只需关注最后一条 ‘等待异步任务返回’ 的线,对比 apply
的时序图, 一眼就看出差别。
registerOnSharedPreferenceChangeListener
最后需要提一下的就是 listener:
- 对于 apply,listener 回调时内存已经完成同步, 但是异步磁盘任务不保证是否完成。
- 对于 commit, listener 回调时内存和磁盘都已经同步完毕。
申明:开始的图片来源网络,侵删