前言
最近在处理一个历史遗留项目的时候饱受其害,主要表现为偶发性的 SharedPreferences 配置文件数据错乱,甚至丢失。经过排查发现是多进程的问题。项目中有两个不同进程,且会频繁的读写 SharedPreferences 文件,所以导致了数据错乱和丢失。趁此机会,精读了一遍 SharedPreferences 源码,下面就来说说 SharedPreferences 都有哪些槽点。
源码解析
SharedPreferences 的使用很简单,这里就不再演示了。下面就按 获取 SharedPreference 、getXXX() 获取数据 和 putXXX()存储数据 这三方面来阅读源码。
1. 获取 SharedPreferences
1.1 getDefaultSharedPreferences()
一般我们会通过 PreferenceManager
的 getDefaultSharedPreferences()
方法来获取默认的 SharedPreferences
对象,其代码如下所示:
> PreferenceManager.java
/**
* 获取默认的 SharedPreferences 对象,文件名为 packageName_preferences , mode 为 MODE_PRIVATE
*/
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode()); // 见 1.2
}
默认的 sp 文件完整路径为 /data/data/shared_prefs/[packageName]_preferences.xml
。mode
默认为 MODE_PRIVATE
,其实现在也只用这种模式了,后面的源码解析中也会提到。最后都会调用到 ContextImpl
的 getSharedPreferences()
方法。
1.2 getSharedPreferences(String name, int mode)
> ContextImpl.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<>();
}
// 先从缓存 mSharedPrefsPaths 中查找 sp 文件是否存在
file = mSharedPrefsPaths.get(name);
if (file == null) { // 如果不存在,新建 sp 文件,文件名为 "name.xml"
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode); // 见 1.3
}
首先这里出现了一个变量 mSharedPrefsPaths
,找一下它的定义:
/**
* 文件名为 key,具体文件为 value。存储所有 sp 文件
* 由 ContextImpl.class 锁保护
*/
@GuardedBy("ContextImpl.class")
private ArrayMap<String, File> mSharedPrefsPaths;
mSharedPrefsPaths
是一个 ArrayMap ,缓存了文件名和 sp 文件的对应关系。首先会根据参数中的文件名 name
查找缓存中是否存在对应的 sp 文件。如果不存在的话,会新建名称为 [name].xml
的文件,并存入缓存 mSharedPrefsPaths
中。最后会调用另一个重载的 getSharedPreferences()
方法,参数是 File 。
1.3 getSharedPreferences(File file, int mode)
> ContextImpl.java
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked(); // 见 1.3.1
sp = cache.get(file); // 先从缓存中尝试获取 sp
if (sp == null) { // 如果获取缓存失败
checkMode(mode); // 检查 mode,见 1.3.2
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); // 创建 SharedPreferencesImpl,见 1.4
cache.put(file, sp);
return sp;
}
}
// mode 为 MODE_MULTI_PROCESS 时,文件可能被其他进程修改,则重新加载
// 显然这并不足以保证跨进程安全
if ((mode & Context.MODE_MULTI_PROC