参考资料:
SharedPreferences(简称SP)是Android中很常用的数据存储方式,SP采用key-value(键值对)形式, 主要用于轻量级的数据存储。SharedPreferences 对象指向包含键值对的文件,并提供读写这些键值对的简单方法。
SharedPreferences API 用于读写键值对,不要将它们与 Preference API 混淆,后者可帮助您构建用于显示应用设置的界面(虽然它们也使用 SharedPreferences 保存用户设置)
一、概述
SharedPreferences 是 Android 平台为应用开发者提供的一个轻量级的存储辅助类,用来保存应用的一些常用配置,它提供了 putString()、putString(Set)、putInt()、putLong()、putFloat()、putBoolean() 六种数据类型。数据最终是以 XML 形式进行存储。在应用中通常做一些简单数据的持久化存储。SharedPreferences 作为一个轻量级存储,所以就限制了它的使用场景,如果对它使用不当可能会引发"严重后果"。
SP采用xml文件格式来保存数据, 该文件所在目录位于 /data/data/${pkgName}/shared_prefs/
。
二、SharePreferences接口
SharePreferences 是一个接口,其内部包含2个接口,一个是提供编辑能力的Editor接口,另一个是提供SharePreference变更通知的监听器接口。SharePreferences接口提供了一系列从sp xml文件中读取键值的方法以及注册sp变更的监听器,而对sp xml文件的写入(编辑) 能力是通过其内部接口Editor来提供的。
2.1. SharePreference接口方法
arduino
// 读取key对应的值
Map<String, ?> getAll();
String getString(String key, @Nullable String defValue);
Set<String> getStringSet(String key, @Nullable Set<String> defValues);
int getInt(String key, int defValue);
long getLong(String key, long defValue);
float getFloat(String key, float defValue);
boolean getBoolean(String key, boolean defValue);
// 判断key是否存在
boolean contains(String key);
// 获取SP编辑器,来获取写入SP能力
Editor edit();
// 注册和注销,当该SP中的key-value变更时将会回调OnSharedPreferenceChangeListener的方法
void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
2.2. Editor接口
arduino
// 向sp xml文件put 写入key-value键值对
Editor putString(String key, @Nullable String value);
Editor putStringSet(String key, @Nullable Set<String> values);
Editor putInt(String key, int value);
Editor putLong(String key, long value);
Editor putFloat(String key, float value);
Editor putBoolean(String key, boolean value);
// 移除指定key-value
Editor remove(String key);
// 清空sp xml
Editor clear();
// 同步提交,直接写入磁盘文件
boolean commit();
// 异步提交,先写入内存,最后异步写入到磁盘文件;快速频繁调用可能导致OOM
void apply();
三、基本使用
SharePreference的使用非常简单,这也是它为什么受开发者青睐的原因之一:
-
创建SharePreference对象
-
执行读写操作
3.1. 创建SharePreferecne对象
有三种方式:
- 通过PreferenceManager获取:返回的是读写默认sp文件的SharePreference对象。
typescript
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
public static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
- 通过Activity获取:Activity提供getPreferences方法, 以当前Activity的类名作为SP的文件名. 即xxxActivity.xml。
arduino
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
public String getLocalClassName() {
final String pkg = getPackageName();
final String cls = mComponent.getClassName();
int packageLen = pkg.length();
if (!cls.startsWith(pkg) || cls.length() <= packageLen
|| cls.charAt(packageLen) != '.') {
return cls;
}
return cls.substring(packageLen+1);
}
- 通过Context获取:Context提供了getSharedPreferences方法,其实现是在ContextImpl类,具体实现在源码分析。
less
// name是sp xml文件名称
// mode 是模式
public abstract SharedPreferences getSharedPreferences(String name, @PreferencesMode int mode);
// mode 取值
@IntDef(flag = true, prefix = { "MODE_" }, value = {
MODE_PRIVATE,
MODE_WORLD_READABLE,// 弃用
MODE_WORLD_WRITEABLE, // 弃用
MODE_MULTI_PROCESS, // 弃用
})
自 API 级别 17 起,MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
模式已被弃用。 从 Android 7.0(API 级别 24)开始,如果您使用这些模式,Android 会抛出 SecurityException。如果您的应用需要与其他应用共享私有文件,可以通过 FLAG_GRANT_READ_URI_PERMISSION
使用 FileProvider
。
3.2. 读写
ini
private void write() {
SharedPreferences sharedPreferences = getSharedPreferences("jacky", MODE_PRIVATE);
// 多次写入时,不要多次调用edit()方法,因为每次调用edit方法都会新建一个EditorImpl对象
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name", "Jacky");
editor.putInt("age", 18);
editor.apply();
}
private void read() {
SharedPreferences sharedPreferences = getSharedPreferences("jacky", MODE_PRIVATE);
int age = sharedPreferences.getInt("age", 0);
String name = sharedPreferences.getString("name", null);
}
四、源码解析
4.1. 获取SharePreference实例
以ContextImpl的getSharePreference方法为例:
arduino
/**
* 多层级Map,从应用包名映射到SP文件再映射到SharedPreferenceImpl对象。
* static变量,全局缓存
*/
private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
/**
* Map,从SP文件名映射到SP文件File对象
*/
private ArrayMap<String, File> mSharedPrefsPaths;
ContextImpl记录着SharedPreferences的重要数据:
- sSharedPrefsCache:以包名为key, 二级key是以SP文件, 以SharedPreferencesImpl为value的嵌套map结构。sSharedPrefsCache是静态类成员变量,每个应用进程是保存唯一一份, 且由ContextImpl.class锁保护。
- mSharedPrefsPaths:记录所有的SP文件, 以文件名为key, 具体文件为value的map结构;
- mPreferencesDir:是指SP所在目录, 是指/data/data//shared_prefs/
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
// 对于Android 4.4 API 19以下版本,如果name传null值,那么构建的xml文件名就是null.xml
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
// 注意:这是一个类锁,当多处并发调用getSharedPreferences时,必须先获取锁对象才可执行,保证对mSharedPrefsPaths操作的并发安全
synchronized (ContextImpl.class) {
// mSharedPrefsPaths是一个ArrayMap。它记录所有的SP文件,KEY是文件名,Value是文件名对应的文件File对象
// 如果为空,则新创建一个ArrayMap
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
// 如果不为null,就先根据文件名试着找到File
file = mSharedPrefsPaths.get(name);
// 如果找不到该文件,则调用getSharedPreferencesPath方法去创建File对象
if (file == null) {
file = getSharedPreferencesPath(name);
// 创建好新的File之后存入到map中
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
当用Context调用getSharedPreferences方法时,先根据传入的文件名,去mSharedPrefsPaths ArrayMap中查找是否有对应的File对象。如果找不到File对象,说明此SP文件不存在,那么调用getSharedPreferencesPath去 /data/data/应用包名/shared_prefs
路径创建一个新的SP文件;并更新到mSharedPrefsPaths;
File 对象的创建:
arduino
@Override
public File getSharedPreferencesPath(String name) {
// 创建sp文件对象
return makeFilename(getPreferencesDir(), name + ".xml");
}
// 目录创建
@UnsupportedAppUsage
private File getPreferencesDir() {
synchronized (mSync) {
// 如果还没有创建shared_prefs目录,那么就先创建此目录,路径是/data/data/应用包名/shared_prefs
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
// 进一步确保此目录存在,如果不存在会调用OS的方法来创建对应权限的目录
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");
}
SharePreference实现类对象SharePreferenceImpl创建:
scss
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
// SharedPreferences的实现类
SharedPreferencesImpl sp;
// 类锁同步
synchronized (ContextImpl.class) {
// 尝试从缓存中获取SharePreferenceImpl对象
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
// 缓存中没有
if (sp == null) {
// 检查传入的MODE是否合法
checkMode(mode);
// 出现该问题的原因:targetSdk为androidO及以上、访问内部存储空间、当前设备处于锁定状态。
// 8.0以后 https://blog.csdn.net/kongqwesd12/article/details/84673654
// https://www.codeleading.com/article/78852205647/
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实现类对象; 一个File对应一个SharedPreferencesImpl对象
sp = new SharedPreferencesImpl(file, mode);
// 对象放入缓存,返回SharePreference
cache.put(file, sp);
return sp;
}
}
// 如果mode包含MODE_MULTI_PROCESS,且在Android 3.0以前,那么会重新load 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;
}
SharedPreferencesImpl 缓存:
ini
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
// sSharedPrefsCache 是用于缓存,ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> ;
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
// 获取应用包名
final String packageName = getPackageName();
// 一个应用包名,对应一个ArrayMap<File, SharedPreferencesImpl>,它存储的是sp文件对象到SharedPreferencesImpl对象的映射
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
// 如果缓存中没有此应用对应的ArrayMap<File, SharedPreferencesImpl> ,那么创建一个, 并放入内存
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
// 这里的存储是根据包名,保存所有SharedPreferencesImpl集合
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
Mode检查:
arduino
private void checkMode(int mode) {
// Android N及以后,禁止使用MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE,否则直接抛出异常
// MODE_MULTI_PROCESS这种多进程的方式也是Google不推荐的方式,目前已经标记弃用,但是未在代码执行层面抛出异常。后续同样会不再支持, 强烈建议App不用使用该方式来实现多个进程实现 同一个SP文件.
// 当设置MODE_MULTI_PROCESS模式, 则每次getSharedPreferences过程, 会检查SP文件上次修改时间和文件大小, 一旦修改则会重新从磁盘加载文件.
if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.N) {
if ((mode & MODE_WORLD_READABLE) != 0) {
throw new SecurityException("MODE_WORLD_READABLE no longer supported");
}
if ((mode & MODE_WORLD_WRITEABLE) != 0) {
throw new SecurityException("MODE_WORLD_WRITEABLE no longer supported");
}
}
}
-
ContextImpl中包含sSharedPrefsCache对象,它是一个多层次的Map,从应用包名映射到SP文件File对象,再映射到SharedPreferenceImpl对象。它是对SharedPreferenceImpl对象的缓存,避免重复创建与销毁带来的开销。应用包名对应多个SP文件,一个SP文件对应一个SharedPreferenceImpl对象。
-
在获取SharePreference对象时,是先尝试从缓存中获取。如果缓存中没有找到与sp文件对应的SharedPreferenceImpl对象,那么说明此sp文件还未加载到内存中,因此就需要创建一个新的SharedPreferenceImpl对象;否则,直接返回缓存中的SharedPreferenceImpl对象。
-
ContextImpl对SP做了全局缓存,因此即使你反复调用getSharedPreferences(),并不会创建重复的SharedPreferencesImpl对象。坏消息是这个缓存并没有任何的trim机制,如果你使用的SP足够多而且其中堆放的数据足够多,你还是可能会遇上内存问题
4.2. SharePreferenceImpl 创建与初始化
ini
SharedPreferencesImpl(File file, int mode) {
mFile = file;
// 创建用于备份的文件;同名的.bak备份文件用于发生异常时, 可通过备份文件来恢复数据.
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
// 启动子线程,从磁盘中异步加载sp xml文件中的key-value数据到内存map中
startLoadFromDisk();
}
创建备份文件,备份文件名是后缀为.bak的文件:
arduino
static File makeBackupFile(File prefsFile) {
return new File(prefsFile.getPath() + ".bak");
}
异步加载磁盘xml数据:
typescript
private void startLoadFromDisk() {
// 锁同步
synchronized (mLock) {
mLoaded = false; // mLoaded用于标记SP文件是否已加载到内存
}
// 创建子线程从磁盘加载sp xml中的key-value到内存map中
// 注意: 这是一个IO操作,要解析xml,如果key-value很多,那么必然导致数据加载慢的问题
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
private void loadFromDisk() {
synchronized (mLock) {
// 如果已经加载过,那么直接忽略
if (mLoaded) {
return;
}
// 如果存在备份文件,那么用备份文件替代
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
...
Map<String, Object> map = null;
...
try {
stat = Os.stat(mFile.getPath());
// 文件可读
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
// 利用xml工具类从xml中读取key-value的map
map = (Map<String, Object>) XmlUtils.readMapXml(str);
}
...
synchronized (mLock) {
// 到这表明xml文件数据全部加载到内存,存入map中了
mLoaded = true;
...
} finally {
// 注意:这里非常重要,当数据加载完毕后,要唤醒get、put等操作,避免它们阻塞。
mLock.notifyAll();
}
}
}
SharePreferenceImpl对象创建时,会进行初始化操作,将mLoaded是否加载完成的标记初始化为false;创建一个备份文件,与sp文件同名,后缀名为.bak;最后再创建一个工作线程将sp xml文件进行解析,将xml中的键值对加载到内存的map中存储。多个线程同时操作一个SharePreferenceImpl对象时,会在其mLock对象上进行同步,以保证线程安全。
4.3. 查询数据
以getString为例:
typescript
public String getString(String key, @Nullable String defValue) {
// SharePreferencesImpl中get系列方法与注册注销、loadFromDisk等方法都是互斥的,必须要拿到锁后才可进行
synchronized (mLock) {
// 重要:getString方法会阻塞住,直至数据加载完毕后通过notifyAll来唤醒
// 如果loadFromDisk耗时过久,那么getString方法就会一直阻塞,从而导致卡顿
awaitLoadedLocked();
// 从内存map中依据key获取value
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);
}
}
当通过SharePreference查询数据的时候,会先检查此SP文件的xml数据是否已经全部解析完并加载到内存的Map中(mLoaded是否为true)。如果已经加载完成,那么就从内存map中读取值;如果还未加载完成,那么读取线程就会被挂起等待,直到数据加载完成后将此线程恢复,恢复之后再从内存map中读取值。可见,如果sp xml文件比较大,从而导致解析的时间以及加载到内存的时间比较长,那么就会导致查询数据、写数据等操作阻塞住,如果读写数据操作在UI线程,那么就会导致UI线程卡顿甚至ANR。
4.4. 获取Editor
要想插入数据,先要获取Editor对象,通过调用edit方法可以获取Editor对象,Editor的真正实现类型是EditorImpl类型。
typescript
@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();
}
// 每次调用edit方法都会创建一个新的EditorImpl对象
return new EditorImpl();
}
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
// key-value数据暂存
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
// 是否清除全部数据的标记位
@GuardedBy("mEditorLock")
private boolean mClear = false;
...
}
EditorImpl 实现了Editor接口,提供put系列方法、apply方法、commit方法等。EditorImpl对象中也有一个Map,主要是提交修改的记录。每次执行edit的时候实际都会生成一个崭新的EditorImpl对象。因此如果频繁调用edit方法,就会频繁创建EditorImpl对象,相对应就会创建许多HashMap对象,默认HashMap的Size是16,虽然实际并不是很占用内存,但是确实没有必要这样去浪费。
4.5. 插入数据
提交新数据主要有两步:
- 在内存中将EditorImpl中记录的修改和SharedPreferencesImpl中的原数据进行合并;
- 将合并之后的数据写入xml文件。
typescript
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
// key-value数据暂存
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
// 是否清除全部数据的标记位
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
//插入数据, 先暂存到mModified对象
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变量的状态,并未进行真正的清除
mClear = true;
return this;
}
}
...
}
数据修改操作仅仅是修改mModified和mClear。直到数据提交commit或许apply过程, 才会真正的把数据更新到SharedPreferencesImpl(简称SPI)。比如设置mClear=true则会清空SPI的mMap数据。
4.6. commit 同步提交
java
@Override
public boolean commit() {
// commit开始的时间戳
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
// 在内存中将EditorImpl中记录的修改和SharedPreferencesImpl中的原数据进行合并,得到一个新的map,封装在MemoryCommitResult
// 将数据更新到内存,将相关数据包装为一个MemoryCommitResult对象返回
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");
}
}
// 通知监听者, 并在主线程回调onSharedPreferenceChanged()方法
notifyListeners(mcr);
// 返回文件操作的结果数据
return mcr.writeToDiskResult;
}
数据更新到内存:commitToMemory
ini
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
boolean keysCleared = false;
//保存发生变化的key列表
List<String> keysModified = null;
//外部监听器
Set<OnSharedPreferenceChangeListener> listeners = null;
// 最后要写入到磁盘xml文件的key-value map;初始值就是mMap
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
// 把SP中的Map内存拷贝一份作为内存修改的基准(初始状态)
mMap = new HashMap<String, Object>(mMap);
}
//将成员mMap赋值给局部变量,后续for循环中用
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
...
synchronized (mEditorLock) {
// 是否真正发生了改变。该标志主要作用是确保当前是否真正发生变化,避免无谓的I/O操作。
boolean changesMade = false;
// 全部清除的标记被设置为了true
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
// 清除所有数据
mapToWriteToDisk.clear();
}
// keys要全部清除,标记为true
keysCleared = true;
mClear = false;
}
// 通过editor的所有put操作,都会先将数据存入mModified这个临时map中
// 此处对put的所有key-value进行遍历
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
// 当value值为this或null, 并且sp加载的map中存在此key,则移除相应的key;
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
// value不为null并且没有改变,那么继续下一轮循环
if (existingValue != null && existingValue.equals(v)) {
// 如果value相等则跳过本次
// 主要是考虑changesMode标志位,确认当前数据是否真正发生变化
continue;
}
}
// value值发生改变了,那么将此新添加的key-value添加到sp map中;
mapToWriteToDisk.put(k, v);
}
// 在for循环中,如果发生数据变化,该changeMade将会置为true,表示当前数据发生变化
changesMade = true;
...
}
// 清空临时修改数据容器
mModified.clear();
...
// 将5个参数包装成一个MemoryCommitResult对象返回
return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
listeners, mapToWriteToDisk);
}
内存数据更新之后就会开始写入磁盘:enqueueDiskWrite。
java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 根据postWriteRunnable是否为null判断执行此方法是commit方法调用,还是apply方法调用
// 如果是commit,那么postWriteRunnable==null
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 写入磁盘任务
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
// 等待获取写入磁盘的锁,一个SPI对象,无论commit还是apply的磁盘写操作是竞争同一把锁。
synchronized (mWritingToDiskLock) {
// 获取到锁之后,将数据写入磁盘
// 全量写入
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--; // 写入后减一
}
...
}
};
// 当commit提交时,会在当前线程执行run方法
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
// commit操作,直接在当前线程中执行;所以commit是同步操作
writeToDiskRunnable.run();
return;
}
}
......
}
-
先执行commitToMemory方法,在内存中将EditorImpl中记录的修改和SharedPreferencesImpl中的原数据进行合并,得到MemoryCommitResult;
-
执行enqueueDiskWrite方法,在当前线程 执行writeToDiskRunnable任务的run方法,将合并后的map数据全量写入到磁盘xml文件。注意是当前线程执行写入磁盘的操作,也就是说commit可以在主线程执行,也可以在工作线程执行。网上很多说commit是在UI线程执行,这是不正确的。
4.7. apply 异步提交
java
public void apply() {
final long startTime = System.currentTimeMillis();
// 数据更新到内存,返回一个MemoryCommitResult对象
final MemoryCommitResult mcr = commitToMemory();
// 定义一个awaitCommit等待任务,CountDownLatch实现
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");
}
}
};
// 将awaitCommit任务添加到finisher任务队列,finisher任务主要是用于等待异步任务的处理
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
// 进入等待状态,阻塞住,等待writeToFile磁盘写入完成后就移除awitCommit任务
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
// 任务加入队列,异步执行磁盘写入
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 不等待结果
notifyListeners(mcr);
}
java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
// 根据postWriteRunnable是否为null判断执行此方法是commit方法调用,还是apply方法调用
// 如果是apply,那么postWriteRunnable!=null isFromSyncCommit = false
final boolean isFromSyncCommit = (postWriteRunnable == null);
// 写入磁盘任务
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
// 等待获取写入磁盘的锁
synchronized (mWritingToDiskLock) {
// 获取到锁之后,将数据写入磁盘
// 全量写入
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
// 对于apply方法,走这里
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
...
// 如果是apply(),提交则将任务加入线程池排队执行, 延迟100毫秒发送任务到队列
// shouldDelay=!isFromSyncCommit
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
public static void queue(Runnable work, boolean shouldDelay) {
// 此Handler是一个子线程的Handler
Handler handler = getHandler();
synchronized (sLock) {
// sWork是一个LinkedList队列,任务添加到对象
sWork.add(work);
// 延迟100毫秒通知执行任务
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
// Handler是子线程的,优先级是前台线程优先级
private static Handler getHandler() {
synchronized (sLock) {
if (sHandler == null) {
HandlerThread handlerThread = new HandlerThread("queued-work-looper",
Process.THREAD_PRIORITY_FOREGROUND);
handlerThread.start();
sHandler = new QueuedWorkHandler(handlerThread.getLooper());
}
return sHandler;
}
}
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
...
public void handleMessage(Message msg) {
if (msg.what == MSG_RUN) {
processPendingWork();
}
}
}
private static void processPendingWork() {
...
if (work.size() > 0) {
// 依次执行队列中的待处理任务的run方法
for (Runnable w : work) {
w.run();
}
....
}
}
}
-
先执行commitToMemory方法,在内存中将EditorImpl中记录的修改和SharedPreferencesImpl中的原数据进行合并,得到MemoryCommitResult;
-
添加一个awaitCommit任务到finisher队列中;
-
执行enqueueDiskWrite方法,向一个工作线程的sWork队列中添加一个写入磁盘的任务writeToDiskRunnable;
-
当磁盘写入任务执行后,就会执行写入后的任务postWriteRunnable,该任务主要是触发awaitCommit任务的执行,并在其执行后将其从finisher队列中移除,表示某次磁盘写入任务完成了。如果finisher队列不为空,说明还有些写入磁盘的任务没有完成。QueuedWork的finisher队列在这里存在的价值主要是用于在Stop Service, finish BroadcastReceiver过程用于判定是否处理完所有的异步SP操作。
typescript@Override public void handlePauseActivity(ActivityClientRecord r, boolean finished, boolean userLeaving, int configChanges, PendingTransactionActions pendingActions, String reason) { .... // Make sure any pending writes are now committed. 等待未完成的sp写入任务完成,放置数据丢失 if (r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } .... }
- 所以说:通过 SharedPreferences 一系列的 apply 提交的任务,都会被加入到工作线程 QueueWork 中,该任务队列以串行方式执行(只有一个工作线程),如果我们 apply 提交非常多的任务,此时判断任务队列还未执行完成,就会一直等到全部执行完成,这就非常容易发生卡顿,如果超过 5s 还会引发 ANR。
五、SP多线程分析
SharePreference是多线程安全的,主要通过synchronize互斥同步锁来保证线程安全;下面仔细看下几种情况:
5.1. 多线程执行getSharePreference
- 先从缓存中获取SPI对象,多个线程在同一把锁对象上互斥同步
- 缓存中没有获取到SPI对象,也是new一个SPI对象
ini
synchronized (ContextImpl.class) {
// 尝试从缓存中获取SharePreferenceImpl对象
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
// 缓存中没有
if (sp == null) {
....
// 创建SharedPreferencesImpl实现类对象
sp = new SharedPreferencesImpl(file, mode);
// 对象放入缓存,返回SharePreference
cache.put(file, sp);
return sp;
}
}
可见,无论是从缓存获取、还是new一个SPI,多个线程并发调用getSharePreference时,将会进行互斥同步,因此同一个sp文件名,多个线程获取的SPI是同一个对象,并且是多线程安全的。
5.2. 多线程查询
vbnet
@Override
public long getLong(String key, long defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
所有的数据查询方法都会在mLock对象上互斥同步,mLock对象是SPI的一个对象private final Object mLock = new Object();
因此多个线程在操作同一个SPI对象进行查询时,是竞争同一把对象锁mLock,从而保证读的安全性。为啥读也要加锁?多线程读Map也是不安全的?
5.3. 多线程执行edit
每个edit方法执行都会创建一个EditorImpl对象,因此多个线程调用edit方法,将会创建对应个数的EditorImpl对象。因此edit方法无须锁同步。
5.4. 多线程插入数据
less
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
//插入数据, 先暂存到mModified对象
mModified.put(key, value);
return this;
}
}
插入数据时必须先通过edit获取EditorImpl对象,如果多个线程都调用了edit方法获取EditorImpl对象,那么它们是不同的对象,mEditorLock是EditorImpl内部的对象,因此多个线程执行时竞争的不是同一把锁,它们之间无须保证线程安全,因为操作的mModified map都不是同一个,每个线程都操作一个单独的map,不会发生并发安全问题。
但若多个线程是用同一个EditorImpl对象,那么对应的mEditorLock就是同一把锁,因此会互斥同步来保证对mModified的安全操作。
5.5. 多线程执行commit
首先要知道commit是可以在工作线程执行的,而不是非要在UI线程执行。如果在UI线程执行commit,那么commit将会阻塞UI,如果是在工作线程执行,commit不会阻塞UI。既然commit可以在工作线程执行,那么并发执行commit提交数据时会怎样呢?安全吗?假设线程A、线程B同时执行commit方法:
假设线程A执行到commitToMemory方法,先获取到锁SharedPreferencesImpl.this.mLock:
csharp
private MemoryCommitResult commitToMemory() {
...
// 即便多个线程用不同的EditorImpl对象调用commit方法,此处仍然是一把锁,因为SPI是一个
synchronized (SharedPreferencesImpl.this.mLock) {
...
}
那么线程B此时不能获取到锁,因此只能等到A释放锁。当A将新提交的数据更新到内存mapToWriteToDisk后,释放锁;线程B立即获取到锁并执行内存数据更新,而此时线程A已经开始执行enqueueDiskWrite方法进行磁盘写入。
java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
...
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
// 等待获取写入磁盘的锁
synchronized (mWritingToDiskLock) {
// 获取到锁之后,将数据写入磁盘
writeToFile(mcr, isFromSyncCommit);
}
...
}
};
...
}
如果当前没有其他线程拿到mWritingToDiskLock锁,那么线程A将获取到mWritingToDiskLock锁,开始执行writeToFile方法,将内存数据全量写入到sp文件中。此时B线程更新完内存后,释放锁,并且也开始执行enqueueDiskWrite方法,可是线程B还拿不到磁盘写入的锁,因此就在此等待获取锁。当线程A磁盘写入完成后,回到commit方法,执行CountDownLatch的await方法。
当线程A执行完磁盘写入任务后,就会释放锁,并返回结果给commit方法;此时线程B立即获取到磁盘写入的锁,并开始写入磁盘,完成后,也返回结果。至此,线程A和线程B并发执行commit的流程就结束了。
mcr.writtenToDiskLatch.await() 作用
java
try {
// 进入等待状态, 直到写入文件的操作完成
mcr.writtenToDiskLatch.await();
}
...
private static class MemoryCommitResult {
...
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
setDiskWriteResult方法是在writeToFile中调用的。为什么是先调用writeToFile,再调用mcr.writtenToDiskLatch.await(); 对于commit操作而言,这个CountDownLatch其实没有什么用,因为commit的writeToFile是在当前线程执行的,是同步的。磁盘写入完成后返回才会执行mcr.writtenToDiskLatch.await(); 而这个await在commit方法中是不会阻塞的。那么为什么还要写这段code?
总结下并发commit的过程是这样的:内存数据更新是互斥同步,写磁盘文件也是互斥同步。并发commit能提高效率吗?当然能,因为写入内存和写入磁盘是两把锁,一个线程写磁盘时,另外一个线程就可以开始更新内存数据。如果在单个线程多次commit,那么只能等前一个commit操作全部完成(内存数据更新+磁盘写入)后才能开始。
5.6. 多线程执行apply
apply与commit的内存数据更新过程是完全一样的。主要不同的是磁盘写入。apply的磁盘写入任务是放在一个子线程HandlerTrehad的任务队列排队执行的,而不是在当前线程执行。
假设线程A执行到commitToMemory方法,先获取到锁SharedPreferencesImpl.this.mLock:
csharp
private MemoryCommitResult commitToMemory() {
...
// 即便多个线程用不同的EditorImpl对象调用commit方法,此处仍然是一把锁,因为SPI是一个
synchronized (SharedPreferencesImpl.this.mLock) {
...
}
那么线程B此时不能获取到锁,因此只能等到A释放锁。当A将新提交的数据更新到内存mapToWriteToDisk后,释放锁;线程B立即获取到锁并执行内存数据更新,而此时线程A已经开始执行enqueueDiskWrite方法,将writeToDiskRunnable任务添加到队列中,然后就返回了。其他线程如果也执行到enqueueDiskWrite,那么也会将任务添加到任务队列中排队执行。
java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
...
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
// 等待获取写入磁盘的锁;注意对于同一个SPI,该锁是一把锁,部分commit还是apply
synchronized (mWritingToDiskLock) {
// 获取到锁之后,将数据写入磁盘
writeToFile(mcr, isFromSyncCommit);
}
...
}
};
...
// 任务添加到HandlerThread线程的任务队列中
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
方法返回后,就直接通知监听器了,也就是apply方法完全不关注是否写入成功了。
scss
// 任务加入队列,异步执行磁盘写入
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);
那么写入磁盘的任务怎么执行的:是按照任务队列串行执行任务的run方法,这些任务的run方法执行时,也必须先获取mWritingToDiskLock锁,因此它们的执行也是互斥同步,避免了多个线程同时操作磁盘IO带来安全问题。
如果commit、apply本身就是在工作线程调用的情况下,apply与commit到底有何区别?apply新开辟的磁盘写入线程有什么意义呢? 并发commit与并发apply看起来没有什么区别。
-
apply没有返回值, commit有boolean返回值能知道修改是否提交成功;
-
apply和commit都是将修改先更新到内存,再写入到磁盘文件; commit是在当前线程(不一定非要在UI线程)执行磁盘写入;而apply是在另一个工作线程执行磁盘写入;
apply 也会导致OOM
SP写操作提交的过程分为两步:
-
在内存中将EditorImpl中记录的修改和SharedPreferencesImpl中的原数据进行合并;
-
将合并之后的数据写入xml文件。在第一步中如果同时存在多个apply请求,每个都会把SP中的Map内存拷贝一份作为内存修改的基准:
if (mDiskWritesInFlight > 0) {
...
mMap = new HashMap<String, Object>(mMap);
}
同时由于执行apply()时,第二步是在一个单线程线程池中执行的,因此第一步申请的内存实际上都被执行队列同时引用着。
由此我们可以知道,当反复执行SP的edit()和apply()操作时,假设操作的次数为M,而SP中的条目数量为N,HashMap.Entry对象最小占用24bytes,那么占用的总内存约为:MN24*2(HashMap的容积率4/3到8/3之间,取平均数)
,举个例子,若是M=N=1024,则执行开头那个for循环时申请的内存为48M,这个大小已经足够一些app产生OOM了。经过这些分析,我们会有以下结论:
-
如果只创建一个Editor并只进行一次apply(),不会有OOM。
-
如果执行的不是apply()而是commit(),也不会有OOM,当然这样的性能会非常感人。
-
循环执行edit()和apply(),达到1k数量级时,很容易引发OOM。在不同的机型上测试的数据会不同。虽然实际循环执行edit、apply的场景几乎很少,但是倘若真有批量的数据操作时,比如做数据迁移,那么就需要避免此种做法,而是利用一个Editor,批量put后,再执行一次apply。
5.7. 主线程(UI线程)多次commit、多次apply
一般情况下,App内对Sp的操作并不会放在工作线程去做。那么此种情况下commit与apply有什么区别呢?
- 多次commit操作:一个commit操作包括更新内存数据、写磁盘IO,都是在UI线程完成;后面的commit要执行必须等前一个commit操作完成;如果时间比较久,就会导致UI卡顿甚至ANR;
- 多次apply操作:一个commit操作包括更新内存数据、写磁盘IO;其中更新内存数据是在UI线程完成,而写磁盘IO是在另外一个工作线程执行,因此多次apply之后,内存更新很快,而磁盘IO写入的任务则会在单线程的任务队列中排队,依次写入磁盘。从而提高效率,不会阻塞UI。
ini
private void writeApply(int size) {
// 1370ms 执行完返回,磁盘写入任务不知道何时完成的
long sT = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
SharedPreferences sharedPreferences = getSharedPreferences("jacky", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name" + i, "Jacky" + i);
editor.putInt("age" + i, 18 + i);
editor.apply();
}
Log.d(TAG, "write: span:" + (System.currentTimeMillis() - sT));
}
private void writeCommit(int size) {
// 5220ms 才全部执行完返回
long sT = System.currentTimeMillis();
for (int i = 0; i < size; i++) {
SharedPreferences sharedPreferences = getSharedPreferences("jacky", MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("name" + i, "Jacky" + i);
editor.putInt("age" + i, 18 + i);
editor.commit();
}
Log.d(TAG, "write: span:" + (System.currentTimeMillis() - sT));
}
如果上面两个线程在UI线程执行,那么显然commit方法阻塞UI的时间比apply方法长很多,主要是因为commit的磁盘IO操作是在当前线程执行,而apply是在工作线程执行的。如果将上面两个方法都放入子线程执行,那么都不会阻塞UI。
scss
new Thread(() -> writeApply(1500)).start();
new Thread(() -> writeCommit(1500)).start();
六、SharePreference的注意事项
-
最好提前初始化SharedPreferences,避免SharedPreferences第一次创建时读取文件线程未结束而出现等待情况。除非你很确幸第一创建时读取文件到内存的耗时很低。
-
强烈建议不要在sp里面存储特别大的key/value, 比如json string或xml string或HTML,否则会占用大量内存、或导致卡顿/ANR;
-
sp配置不要全部都写在一个文件中,这样不仅第一次加载会很慢,也会占用大量内存。最好是根据一定规则分成多个sp文件。比如频繁和不频繁写入的配置就分别存储在两个不同的文件中。高频写操作的key与高频读操作的key可以适当地拆分文件, 可以减少同步锁竞争;
-
sp文件的写入是全量写入,即使改了一条配置,写入的时候也会对整个文件进行操作,因此最好能批量操作,不要每次都commit。
-
apply方法虽然是在线程中异步将配置写入文件,但是如果任务很多,而且每个任务执行时间很长,也可能会导致Activity或Service在stop的时候出现ANR。
-
每次commit时会把全部的数据更新的文件, 所以整个文件是不应该过大的, 影响整体性能;
-
不要连续多次edit()和apply, 应该获取一次获取edit(),然后多次执行putxxx(), 减少内存波动; 经常看到大家喜欢封装方法, 结果就导致这种情况的出现。大量循环调用edit和apply进行批量操作,可能会导致OOM。
-
不要一上来就执行getSharedPreferences().edit(), 应该分成两大步骤来做, 中间可以执行其他代码;
-
不要指望它能够跨进程通信:
Context.MODE_MULTI_PROCESS
,跨进程可以通过实现ContentProvider接口来访问SP文件。 -
无论是commit同步写入还是apply异步写入,都可能发生文件IO异常而导致文件写入失败。因此写代码时在一些场景下需要考虑该异常情况的处理。
七、封装SP工具类
需求:
-
支持多文件名
-
支持apply、commit两种提交方式
-
支持批量写入
-
不要频繁创建SharePreference实现类SharePreferenceImpl对象
-
不要频繁调用edit方法创建Editor实现类EditorImpl对象
typescript
package com.android.lib.tools.singleton;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import java.util.HashMap;
public class SPSingleton {
private static volatile HashMap<String, SPSingleton> instanceMap = new HashMap<>();
private SharedPreferences sharedPreferences;
private SharedPreferences.Editor editor;
//是否是执行apply的模式,false表示为commit保存数据
private boolean isApplyMode = false;
private static final String DEFAULT = "default";
private SPSingleton(String name) {
if (DEFAULT.equals(name)) {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(APP.get());
} else {
sharedPreferences = APP.get().getSharedPreferences(name, Context.MODE_PRIVATE);
}
editor = sharedPreferences.edit();
}
public static SPSingleton get(String name) {
if (instanceMap.get(name) == null) {
synchronized (SPSingleton.class) {
if (instanceMap.get(name) == null) {
instanceMap.put(name, new SPSingleton(name));
}
}
}
//这里每次get操作时强制将保存模式改为commit的方式
instanceMap.get(name).isApplyMode = false;
return instanceMap.get(name);
}
public static SPSingleton get() {
return get(DEFAULT);
}
// 如果用apply模式的话,得要先调用这个方法,
// 然后链式调用后续的存储方法,最后以commit方法结尾
public SPSingleton applyMode() {
isApplyMode = true;
return this;
}
public void commit() {
editor.commit();
}
public SPSingleton putBoolean(String key, boolean value) {
editor.putBoolean(key, value);
save();
return this;
}
private void save() {
if (isApplyMode) {
editor.apply();
} else {
editor.commit();
}
}
public SPSingleton putFloat(String key, float value) {
editor.putFloat(key, value);
save();
return this;
}
public float getFloat(String key, float defValue){
return sharedPreferences.getFloat(key, defValue);
}
public SPSingleton putLong(String key, long value) {
editor.putLong(key, value);
save();
return this;
}
public long getLong(String key, long defValue){
return sharedPreferences.getLong(key, defValue);
}
public SPSingleton putInt(String key, int value) {
editor.putInt(key, value);
save();
return this;
}
public SPSingleton putString(String key, String value) {
editor.putString(key, value);
save();
return this;
}
public String getString(String key, String defValue) {
return sharedPreferences.getString(key, defValue);
}
public void delete(String key) {
editor.remove(key);
save();
}
public int getInt(String key, int defValue) {
return sharedPreferences.getInt(key, defValue);
}
public boolean getBoolean(String key, boolean defValue) {
return sharedPreferences.getBoolean(key, defValue);
}
}