本文是独立解析源码的第二篇,SharedPreference 是一个 Android 开发自带的适合保存轻量级数据的 K-V 存储库,它使用了 XML 的方式来存储数据,比如我就经常用它保存一些如用户登录信息等轻量级数据。那么今天就让我们来分析一下它的源码,研究一下其内部实现。
获取SharedPreferences
我们在使用 SharedPreferences 时首先是需要获取到这个 SharedPreferences 的,因此我们首先从 SharedPreferences 的获取入手,来分析其源码。
根据名称获取 SP
不论是在 Activity 中调用 getPreferences() 方法还是调用 Context 的 getSharedPreferences 方法,最终都是调用到了 ContextImpl 的 getSharedPreferences(String name, int mode) 方法。我们先看看它的代码:
@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);
}
可以看到,它首先对 Android 4.4 以下的设备做了特殊处理,之后将对 mSharedPrefsPaths 的操作加了锁。mSharedPrefsPaths 的声明如下:
private ArrayMap
可以看到它是一个以 name 为 key,name 对应的 File 为 value 的 HashMap。首先调用了 getSharedPreferencesPath 方法构建出了 name 对应的 File,将其放入 map 后再调用了 getSharedPreferences(File file, int mode) 方法。
获取 SP 名称对应的 File 对象
我们先看看是如何构建出 name 对应的 File 的。
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
可以看到,调用了 makeFilename 方法来创建一个名为 name.xml 的 File。makeFilename 中仅仅是做了一些判断,之后 new 出了这个 File 对象并返回。
可以看到,SharedPreference 确实是使用 xml 来保存其中的 K-V 数据的,而具体存储的路径我们这里就不再关心了,有兴趣的可以点进去看看。
根据创建的 File 对象获取 SP
我们接着看到获取到 File 并放入 Map 后调用的 getSharedPreferences(file, mode) 方法:
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap cache = getSharedPreferencesCacheLocked(); // 1
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); // 2
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;
}
首先可以看到注释 1 处,这里调用了 getSharedPreferencesCacheLocked 来获取到了一个 ArrayMap
之后看到注释 2 处,当获取不到对应 SPI 时,再创建一个对应的 SPI,并将其加入这个 ArrayMap 中。
这里很明显是一个缓存机制的实现,以加快之后获取 SP 的速度,同时可以发现,SP 其实只是一个接口,而 SPI 才是其具体的实现类。
缓存机制
那么我们先来看看其缓存机制,进入 getSharedPreferencesCacheLocked 方法:
private ArrayMap getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
显然,这里有个全局的 ArrayMap:sSharedPrefsCache。
它是一个 ArrayMap
SharedPreferencesImpl
那么终于到了我们 SPI 的创建了,在 cache 中找不到对应的 SPI 时,就会 new 出一个 SPI,看看它的构造函数:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file); // 1
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk(); // 2
}
可以看到注释 1 处它调用了 makeBackupFile 来进行备份文件的创建。
之后在注释 2 处则调用了 startLoadFromDisk 来开始从 Disk 载入信息。
首先我们看看 makeBackupFile 方法:
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
很简单,返回了一个同目录下的后缀名为 .bak 的同名文件对象。
从 Disk 加载数据
接着,我们看看 startLoadFromDisk 方法:
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
可以看到,首先在加锁的情况下对 mLoaded 进行了修改,之后则开了个名为「SharedPreferencesImpl-load」的线程来加载数据。
我们看到 loadFromDisk 方法:
private void loadFromDisk() {
synchronized (mLock) { // 1
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 { // 2
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 (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
/* ignore */
}
synchronized (mLock) { // 3
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
mLock.notifyAll();
}
}
代码比较长,我们慢慢分析
首先在注释 1 处,如果已经加载过,则不再进行加载,之后又开始判断是否存在备份文件,若存在则直接将备份文件直接修改为数据文件 ${name}.xml。
之后在注释 2 处,通过 XmlUtils 将 xml 中的数据读取为一个 Map。由于本文主要是对 SP 的大致流程的解读,因此关于 XML 文件的具体读取部分,有兴趣的读者可以自己进入源码研究。
之后在注释 3 处,进行了一些收尾处理,将 mLoaded 置为 true,并对 mMap 进行了判空处理,以保证在 xml 没有数据的情况下其仍不为 null,最后释放了这个读取的锁,表示读取成功。
编辑 SharedPreferences
我们都知道,真正对 SP 的操作其实都是在 Editor 中的,它其实是一个接口,具体的实现类为 EditorImpl。让我们先看看 Editor 的获取:
获取 Editor
看到 SharedPreferencesImpl 的 edit 方法:
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();
}
可以看到,这里首先先调用了 awaitLoadedLocked() 方法来等待读取的完成,当读取完成后才会真正创建并返回 EditorImpl 对象。
等待读取机制
由于读取过程是一个异步的过程,很有可能导致读取还没结束,我们就开始编辑,因此这里用到了一个 awaitLoadedLocked 方法来阻塞线程,直到读取过程完成,下面我们可以先看看 awaitLoadedLocked 方法:
private void awaitLoadedLocked() {
if (!mLoaded) {
// Raise an explicit StrictMode onReadFromDisk for th
// 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) {
}
}
}
可以看到,这里会阻塞直到 mLoaded 为 true,这样就保证了该方法后的方法都会在读取操作进行后执行。
EditorImpl
前面我们提到了 Edit 的真正实现类是 EditorImpl,它其实是 SPI 的一个内部类。它内部维护了一个Map
下面列出一些 EditorImpl 对外提供的修改接口,其实都是在对 mModified 这个 Map 进行修改,具体代码就不再讲解,比较简单:
public Editor putString(String key, @Nullable String val
synchronized (mLock) {
mModified.put(key, value);
return this;
}
}
public Editor putStringSet(String key, @Nullable Set
提交 SharedPreferences
提交本来可以放到编辑中的,但因为它才是重头戏,因此我们单独拎出来讲一下。
我们都知道 SP 的提交有两种方式——apply 和 commit。下面我们来分别分析:
apply
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory(); // 1
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); // 2
// 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);
}
首先看到注释 1 处,调用了 commitToMemory 方法,它内部就是将原先读取进来的 mMap 与刚刚修改过的 mModified 进行合并,并存储于返回的 MemoryCommitResult mcr中。
而在注释 2 处,调用了 enqueueDiskWrite 方法,传入了之前构造的 Runnable 对象,这里的目的是进行一个异步的写操作。
前面提到的两个方法,我们放到后面分析。
也就是说,apply 方法会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘。
commit
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
MemoryCommitResult mcr = commitToMemory(); // 1
SharedPreferencesImpl.this.enqueueDiskWrite( // 2
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;
}
看到注释 1 处,可以发现,同样调用了 commitToMemory 方法进行了合并。
之后看到 2 处,同样调用了 enqueueDiskWrite 方法,不过传入的第二个不再是 Runnable 方法。这里提一下,如果 enqueueDiskWrite 方法传入的第二个参数为 null,则会在当前线程执行写入操作。
也就是说,commit 方法会将数据先提交到内存,但之后则是一个同步的过程写入硬盘。
同步数据至内存
下面我们来看看两个方法中都调用了的 commitToMemory 的具体实现:
private MemoryCommitResult commitToMemory() {
...
synchronized (mLock) {
boolean changesMade = false;
if (mClear) {
if (!mMap.isEmpty()) {
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);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
可以看到,具体的代码就如同我们之前所说的一样,将 mMap 的数据与 mModified 的数据进行了整合,之后将 mModified 重新清空。最后将合并的数据放入了 MemoryCommitResult 中。
写入数据至硬盘
我们同样看到 apply 和 commit 都调用了的方法 enqueueDiskWrite:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null); // 1
final Runnable writeToDiskRunnable = new Runnable() {
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(); // 2
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
可以看到 1 处,若第二个 Runnable 为 null 的话,则会将 isFromSyncCommit 置为 true,也就是写入会是一个同步的过程。之后在注释 2 处便进行了同步写入。否则会构造一个 Runnable 来提供给 QueueWork 进行异步写入。
QueueWork 类内部维护了一个 single 线程池,这样可以达到我们异步写入的目的。
而 writeToFile 方法中其实就是又调用了之前的 XmlUtils 来进行 XML 的写入。
总结
SharedPreferences 其实就是一个用使用 XML 进行保存的 K-V 存储库。
在获取 SP 时会进行数据的加载,将 name 对应的 xml 文件以 Map 的形式读入到内存。
而 SP 的编辑操作其实都是在 Editor 内实现,它内部维护了一个新的 Map,所有的编辑操作其实都是在操作这个 Map,只有提交时才会与之前读取的数据进行合并。
其提交分为两种,apply 和 commit,它们的特性如下
-
apply
- 会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘。
- 返回值时可能写入操作还没有结束
- 写入失败时不会有任何提示
-
commit
会将数据先提交到内存,但之后则是一个同步的过程写入硬盘。
写入操作结束后才会返回值
写入失败会有提示
因此,当我们对写入的结果不那么关心的情况下,可以使用 apply 进行异步写入,而当我们对写入结果十分关心且提交后有后续操作的话最好使用 commit 来进行同步写入。