[Framework] 深入理解 Android SharedPreferences
SharedPreferences
是 Android
中用来本地化存储简单 key-value
的工具,在开发过程中经常用来存放一些用户的一些简单设置。 Android
给我们写了一个非常优秀的样板代码来存储本地数据,还是非常有价值来学习下。
源码基于 Android 31
读取数据
我们使用 SP
都是 Context#getSharedPreferences()
来获取 SP
实例,最终都会调用到 ContextImpl#getSharedPreperences()
,然后我们也以这个函数为我们分析的入口函数。
Java
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
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);
}
通过 name
来获取 SP
对应的文件,如果没有获取到就通过 getSharedPreferencesPath()
方法来创建一个文件,最后调用 getSharedPreferences()
方法来获取 SP
对象。
来看看 getSharedPreferencesPath()
创建文件的实现:
Java
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
@UnsupportedAppUsage
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
final File res = new File(base, name);
// We report as filesystem access here to give us the best shot at
// detecting apps that will pass the path down to native code.
BlockGuard.getVmPolicy().onPathAccess(res.getPath());
return res;
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
上面的方法很简单,就是在 /data/data/<pakcage_name>/shared_prefs
目录下面创建一个 SP
的本地文件,文件名称是 name
+ .xml
。
getSharedPreferences()
方法获取 SP
对象:
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);
// ...
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return sp;
}
}
// ...
return sp;
}
以文件为 key
去查询有没有 SharedPreferencesImpl
对象,如果没有就创建一个。
SharedPreferencesImpl
构造函数:
Java
@UnsupportedAppUsage
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
startLoadFromDisk();
}
构造函数非常简单,简单说一下其中重要的成员变量,mFile
就是对应的 SP
本地文件;mBackupFile
备份文件,在写之前都会把之前的文件存储为备份,如果写入失败,后续就继续读这个备份文件,写入成功就删除备份文件,他的名字是源文件加上 .bak
后缀; mLoaded
表示是否已经把本地文件加载到内存中了;mMap
内存中存放的数据;最后执行 startLoadFromDisk()
方法从文件中加载数据。
继续看看关键的 startLoadFromDisk()
方法。
Java
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
直接新建一个线程调用 loadFromDisk()
方法。
Java
private void loadFromDisk() {
synchronized (mLock) {
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<String, Object> map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
// An errno exception means the stat failed. Treat as empty/non-existing by
// ignoring.
} catch (Throwable t) {
thrown = t;
}
synchronized (mLock) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
try {
if (thrown == null) {
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
// In case of a thrown exception, we retain the old map. That allows
// any open editors to commit and store updates.
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
}
}
刚开始会判断备份文件是否存在,如果存在就表示上次写入的时候失败了,就直接使用备份文件的数据当成新的文件,然后加载。
通过 XmlUtils#readMapXml()
方法读取文件流中的 xml
,以 Map
的格式返回,最后存储在 mMap
成员变量中。
最后会通过 mLock.notifyAll()
通知其他还在等待加载完成的方法,比如读取的时候就需要等待加载完成,后续会讲到。
再看看读取数据的方法,我以 getString()
方法作为例子,其他的方法都是大同小异:
Java
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@GuardedBy("mLock")
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);
}
}
获取前会通过自旋的方式去检查加载的状态,加载成功后直接,从内存中的数据去读取。
写入数据
写入数据的时候需要通过 SharedPreferences#edit()
方法返回一个 Editor
对象,然后通过 Editor
对象进行写,写完了以后调用 commit()
或者 apply()
方法提交,最后写入到本地文件中。
先看看 SharedPreferencesImpl#edit()
方法:
Java
@Override
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 (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
和读取一样也需要等待文件加载完成,然后直接新建一个 EditorImpl
实例返回,EditorImpl
是 SharedPreferencesImpl
中的内部类。
我们以 EditorImpl#putString()
、 EditorImpl#remove()
和 EditorImpl#clear()
方法为例子看看写入,删除和清空,其他写入的方法也都是大同小异。
Java
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
@Override
public Editor remove(String key) {
synchronized (mEditorLock) {
mModified.put(key, this);
return this;
}
}
@Override
public Editor clear() {
synchronized (mEditorLock) {
mClear = true;
return this;
}
}
插入或者修改数据会直接把值放在 mModified
中,删除的时候直接把对应 key
的 value
设置成 EditorImpl
自己,清空数据只是加一个 mClear
变量标识。
我们一同看看提交修改的 commit()
和 apply()
方法:
Java
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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;
}
@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) {
}
if (DEBUG && mcr.wasWritten) {
Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
+ " applied after " + (System.currentTimeMillis() - startTime)
+ " ms");
}
}
};
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);
}
commit()
和 apply()
方法都是大同小异,不同的是 commit()
是同步写入,apply()
是异步写入。他们都会调用 commitToMemory()
方法把 EditorImpl
中修改的数据提交到内存中,然后调用 SharedPreferencesImpl#enqueueDiskWrite()
方法同步到本地文件。只是 commit()
没有传递 Runnable
对象,而 apply()
有传递,在这个 Runnable
对象中会等待修改完成,没有完成的可以在 QueuedWork
中查询对应的 Runnable
。
看看如何把修改同步到内存中去:
Kotlin
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List<String> keysModified = null;
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap<String, Object>(mMap);
}
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();
}
keysCleared = true;
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, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
这里有一个非常重要的参数 mDiskWritesInFlight
变量,当它大于 0 的时候就表示有其他的写入还没有完成,后续会再讲到这个参数。
首先判断是否要清空数据,如果要清空数据就把以前的数据清空。
然后遍历 EditorImpl
中修改的数据,如果 value
值为空或者为 EditorImpl
对象时就表示要删除这条数据,反之就是更新数据或者插入数据。最后就得到了更新后的数据 mapToWriteToDisk
,也就是 mMap
内存中的数据,最后会把更新后等待写入到本地文件中的数据添加到 MemoryCommitResult
中供后续写入到文件中使用。
然后再来看看 SharedPreferences#enqueueDiskWrite()
是如何写入修改后的文件到磁盘的:
Java
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);
}
最终执行写文件的任务是 writeToDiskRunnable
,如果是异步调用或者如果有其他提交还没有修改完(异步通过 postWriteRunnable
判断,是否有其他的修改没有完成通过 mDiskWritesInFlight
判断),就会用其他线程异步执行。 反之就在当前线程直接执行。执行写文件的方法是 writeToFile()
。
Java
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
// ...
try {
FileOutputStream str = createFileOutputStream(mFile);
// ...
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
// ...
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
// ...
mcr.setDiskWriteResult(true, 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, false);
}
我省略了部分代码,留下关键代码,通过 XolUtils.writeMapXml()
方法把更新后的数据写入到文件中,更新成功后会把结果写入到结果中,然后删除备份文件,如果更新失败会删除更新失败的文件。
总结
每个不同 SP
的 name
都对应一个不同的本地文件,每次使用时都会去检查是否有加载到内存中,如果没有加载就会从本地去加载然后保存到内存中(没有回收机制),以后再使用就不用再加载。 如果要执行读写操作的话,也必须阻塞等待加载完成。读取操作性能比较好,是直接读取内存中的数据。写操作需要先更新内存中的数据,然后把更新的数据全部重新再写人本地文件。
根据 SP
的各种特点我们在开发中需要注意以下事项:
-
同一个
SP
文件存放的数据不宜过大,过大会导致初次加载慢,写入的数据也慢,占用的内存也多,一个SP
文件存放的数据过大可以考虑根据业务逻辑拆分成多个SP
文件。如果是存放的单条key-value
过大,可以考虑自己写一个本地文件缓存,如果是有非常多条的key-value
也可以考虑使用数据库。 -
在获取
SP
对象时是不会阻塞线程的,但是在使用它时会有可能会阻塞,因为需要等待本地文件加载完成,所以使用时最好能够在后台线程操作。 -
写入有通过
commit
和apply
两种方式提交数据,commit
是同步等待整个SP
文件写入完成,apply
是异步,他们都有可能造成卡顿,甚至ANR
,虽然apply
是异步的,但是在某些组件的生命周期中也会检查没有完成的SP
的apply
任务,长时间没有完成也会ANR
。commit
方法强烈建议在后台线程中执行,这样就不会导致ANR
。