SharedPreference
是一个 Android 开发自带的适合保存轻量级数据的key-value
存储库,它使用了xml
的方式来存储数据,比如可以使用它保存一些如用户 登录信息等轻量级数据。
官方文档:
https://developer.android.google.cn/training/data-storage/shared-preferences
注:本文以 API 30 的 SharedPreferences 源码进行解析。
一、获取 SharedPreferences
在使用SharedPreferences
时首先是需要获取到这个SharedPreferences
,因此我们首先从SharedPreferences
的获取入手,来分析其源码。
根据名称获取 SharedPreferences
不论是在Activity
中调用getPreferences
方法还是调用Context
的 getSharedPreferences
方法,最终都是调用到了ContextImpl
的 getSharedPreferences(String name, int mode)
方法。
注:ContextImpl 是 Context 的实现类。
源码位置:
/sdk/sources/android-30/android/app/ContextImpl.java
我们先看看它的代码:
public SharedPreferences getSharedPreferences(String name, int mode) {
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) {
// 调用 getSharedPreferencesPath 方法构建出 name 对应的 File
file = getSharedPreferencesPath(name);
// 将 file 放入 map
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
可以看到,它首先对 Android 4.4 以下的设备做了特殊处理,之后将对 mSharedPrefsPaths
的操作加了锁。
mSharedPrefsPaths
的声明如下:
private ArrayMap mSharedPrefsPaths;
可以看到它是一个以name
为key
,name
对应的File
为value
的
HashMap
。
获取 SharedPreferences 名称对应的 File 对象
先看看是如何构建出name
对应的File
的:
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
可以看到,调用makeFilename
方法来创建一个名为name.xml
的File
。
makeFilename
中仅仅是做了一些判断,之后new
出了这个File
对象并返回。
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
final File res = new File(base, name);
BlockGuard.getVmPolicy().onPathAccess(res.getPath());
return res;
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
可以看到,SharedPreference
确实是使用xml
来保存其中的key-value
数据的。
根据创建的 File 对象获取 SharedPreference
接着看到获取到File
并放入Map
后调用的getSharedPreferences(file, mode)
方法:
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// 调用 getSharedPreferencesCacheLocked 来获取到一个 ArrayMap
final ArrayMap cache = getSharedPreferencesCacheLocked();
// 从这个 Map 中尝试获取到对应的 SharedPreferencesImpl 实现类
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");
}
}
// 当获取不到对应 SharedPreferencesImpl 时,再创建一个对应的 SharedPreferencesImpl,并将其加入这个 ArrayMap 中
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) {
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
很明显是一个缓存机制的实现,以加快之后获取SharedPreference
的速度,同时可以发现SharedPreference
其实只是一个接口,而 SharedPreferencesImpl
才是其具体的实现类。
缓存机制
进入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;
}
显然,这里有个全局的sSharedPrefsCache
,它是一个ArrayMap
类型的Map
。根据PackageName
来保存不同的SharedPreference
缓存Map
的,通过这样的方式,就保证了不同PackageName
中相同name
的SharedPreference
从缓存中拿到的数据是不同的。
SharedPreferencesImpl 的创建
在cache
中找不到对应的SharedPreferencesImpl
时,就会new
出一个 SharedPreferencesImpl
,看看它的构造函数:
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 调用 makeBackupFile 来进行备份文件的创建
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 调用 startLoadFromDisk 来开始从 Disk 载入信息
startLoadFromDisk();
}
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) {
// 如果已经加载过,则不再进行加载
if (mLoaded) {
return;
}
// 判断是否存在备份文件,若存在则将备份文件直接修改为数据文件 name.xml
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map 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);
// 通过 XmlUtils 将 xml 中的数据读取为一个 Map
map = (Map) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
} finally {
IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {
} catch (Throwable t) {
thrown = t;
}
// 进行一些收尾处理
synchronized (mLock) {
// 将 mLoaded 置为 true
mLoaded = true;
mThrowable = thrown;
try {
if (thrown == null) {
// 对 mMap 进行判空处理,以保证在 xml 没有数据的情况下其仍不为 null
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
}
} catch (Throwable t) {
mThrowable = t;
} finally {
// 释放这个读取的锁,表示读取成功
mLock.notifyAll();
}
}
}
二、编辑 SharedPreferences
真正对SharedPreferences
的操作其实都是在Editor
中的,它其实是一个接口, 具体的实现类为EditorImpl
。
获取 Editor
看到SharedPreferencesImpl
的edit
方法:
public Editor edit() {
synchronized (mLock) {
awaitLoadedLocked();
}
return new EditorImpl();
}
首先先调用了awaitLoadedLocked()
方法来等待读取的完成,当读取完成后才会真正创建并返回EditorImpl
对象。
等待读取机制
由于读取过程是一个异步的过程,很有可能导致读取还没结束,我们就开始编辑,因此这里用到了一个awaitLoadedLocked
方法来阻塞线程,直到读取过程完成。
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
为true
,这样就保证了该方法后的方法都会在读取操作进行后执行。
EditorImpl
Editor
的真正实现类是EditorImpl
,它其实是SharedPreferencesImpl
的一个内部类。它内部维护了一个mModified
,mModified
是一个HashMap
类型的Map
。通过mModified
来存放对SharedPreferences
进行的操作,此时还不会提交到SharedPreferencesImpl
中的mMap
,我们做的操作都是在改变mModified
。
下面列出一些EditorImpl
对外提供的修改接口,其实都是在对mModified
这个Map
进行修改:
public Editor putString(String key, @Nullable String value) {
...
}
public Editor putStringSet(String key, @Nullable Set values) {
...
}
public Editor putInt(String key, int value) {
...
}
public Editor putLong(String key, long value) {
...
}
public Editor putFloat(String key, float value) {
...
}
public Editor putBoolean(String key, boolean value) {
...
}
public Editor remove(String key) {
...
}
public Editor clear() {
...
}
提交 SharedPreferences
SharedPreferences
的提交有两种方式:apply
和commit
。
apply
public void apply() {
final long startTime = System.currentTimeMillis();
// 调用 commitToMemory 方法,它内部就是将原先读取进来的 mMap 与刚刚修改过的 mModified 进行合并,并存储于返回的 MemoryCommitResult mcr 中
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);
}
};
// 调用 enqueueDiskWrite 方法,传入了之前构造的 Runnable 对象,这里的目的是进行一个异步的写操作。
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
也就是说,apply
方法会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘。
commit
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 和 apply 方法一样,调用 commitToMemory 方法进行了合并
MemoryCommitResult mcr = commitToMemory();
// 和 apply 方法一样,调用 enqueueDiskWrite 方法,不过传入的第二个不再是 Runnable 方法,
// 通过源码注释 "if null, then we're allowed to do this disk write on the main thread",如果 enqueueDiskWrite 方法传入的第二个参数为 null,则会在当前线程执行写入操作
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
方法会将数据先提交到内存,但之后则是一个同步的过程写入硬盘。
同步数据至内存
commitToMemory
方法:
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
List keysModified = null;
Set listeners = null;
Map mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
mMap = new HashMap(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList();
listeners = new HashSet(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
keysCleared = true;
mClear = false;
}
for (Map.Entry 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, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
可以看到,将mMap
的数据与mModified
的数据进行了整合,之后将mModified
重新清空。最后将合并的数据放入了MemoryCommitResult
中。
写入数据至硬盘
enqueueDiskWrite
方法:
private void enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable) {
// 若第二个 Runnable 为 null 的话,则会将 isFromSyncCommit 置为 true,也就是写入会是一个同步的过程
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();
}
}
};
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// 进行同步写入
writeToDiskRunnable.run();
return;
}
}
// 构造一个 Runnable 来提供给 QueueWork 进行异步写入
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
QueueWork
类内部维护了一个single
线程池,这样可以达到我们异步写入的目的。而writeToFile
方法中其实就是又调用了之前的XmlUtils
来进行xml
的写入。
总结
SharedPreferences
其实就是一个用使用xml
进行保存的key-value
存储库。在获取SharedPreferences
时会进行数据的加载,将name
对应的xml
文件以Map
的形式读入到内存。
而SharedPreferences
的编辑操作其实都是在Editor
内实现,它内部维护了一个新的Map
,所有的编辑操作其实都是在操作这个Map
,只有提交时才会与之前读取的数据进行合并。
SharedPreferences
的提交分为两种,apply
和commit
,它们的特性如下:
- apply
- 会将数据先提交到内存,再开启一个异步过程来将数据写入硬盘
- 返回值时可能写入操作还没有结束
- 写入失败时不会有任何提示
- commit
- 会将数据先提交到内存,但之后则是一个同步的过程写入硬盘
- 写入操作结束后才会返回值
- 写入失败会有提示
因此,当我们对写入的结果不那么关心的情况下,可以使用apply
进行异步写入,而当我们对写入结果十分关心且提交后有后续操作的话最好使用commit
来进行同步写入。
© 2020 Tyhoo Wu, All rights reserved.