- SharedPreferences 系列
- ContentProvider 系列(待更)
《Android 存储选项之 ContentProvider 启动过程源码分析》
《Android 存储选项之 ContentProvider 深入分析》
- 对象序列化系列
- 数据序列化系列(待更)
《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》
- SQLite 存储系列
前言
本文不是与大家一起探讨关于 SharedPreferences 的基本使用,而是结合源码的角度分析对 SharedPreferences 使用不当可能引发的“严重后果”以及该如何正确的使用 SharedPreferences。
SharedPreferences 是 Android 平台为应用开发者提供的一个轻量级的存储辅助类,用来保存应用的一些常用配置,它提供了 putString()、putString(Set<String>)、putInt()、putLong()、putFloat()、putBoolean() 六种数据类型。数据最终是以 XML 形式进行存储。在应用中通常做一些简单数据的持久化存储。SharedPreferences 作为一个轻量级存储,所以就限制了它的使用场景,如果对它使用不当可能会引发“严重后果”。
从源码角度出发(基于 API Level 28)
1、 SharedPreferences 文件保存位置
SharedPreferences config = context.getSharedPreferences("config", Context.MODE_PRIVATE);
String value = config.getString("key", "default");
通过 Context 的 getSharedPreferences() 方法得到 SharedPreferences 对象,这里实际调用的是 ContextImpl.getSharedPreferences() 方法。
@Override
public SharedPreferences getSharedPreferences(String name, int mode){
//mBase实际类型是 ContextImpl
return mBase.getSharedPrefenences(name, mode);
}
mBase 的实际类型是 ContextImpl(不熟悉的朋友,可以去看下 Activity 的创建过程,在 ActivityThread 中)。
ContextImpl 中 getSharedPreferences(String name, int mode) 调用过程如下:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
//如果 targetSdkVersion 小于 19 版本,name 传递 null,
//则直接将文件名设置为null,既文件名为:null.xml
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
//mSharedPrefsPaths维护文件名name和文件File 的映射关系
//这个在较早版本中不存在
mSharedPrefsPaths = new ArrayMap<>();
}
//通过文件名name获取对应的文件File
file = mSharedPrefsPaths.get(name);
if (file == null) {
//SharedPreferences文件目录创建过程
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
//根据File创建SharedPreferences
return getSharedPreferences(file, mode);
}
代码中标注了详细的注释,这里主要维护了 SharedPreferences 文件名 name 和文件 File 的映射关系,既根据文件名 name 得到文件 File,每个 Activity 都会包含一个 ContextImpl 对象,mSharedPrefsPaths 是它的成员变量,既仅在当前对象有效。这里重点跟踪下 SharedPreferences 文件的保存目录,SharedPreferences 文件路径创建过程:
/**
* 根据文件名创建File对象
*/
@Override
public File getSharedPreferencesPath(String name){
return makeFilename(getPreferencesDir(), name+".xml");
}
getPreferencesDir 方法如下:
@Override
private File getPreferencesDir(){
synchronized(mSync){
if(mPreferencesDir == null){
//创建SharedPreferences文件保存目录
//getDataDir返回:/data/data/packageName/
mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
//确保应用私有文件目录已经存在
return ensurePrivateDirExists(mPreferencesDir);
}
}
从这里可以看出 SharedPreferences 文件的存储位置是在应用程序包名下 shared_prefs 目录内。
这里需要注意的是文件名 name 不能是路径形式如:“/config”,如下将会抛出异常:
@override
private File makeFilename(File base, String name){
if(name.indexOf(File.separatorChar) < 0){
//SharedPreferences文件名中如果包含“/”字符将会抛出异常
return new File(base, name);
}
throw new IllegalArgumentException("File " + name + " contains a path separator" );
}
跟踪到这里,SharedPreferences 的文件保存路径我们就算是找到了。这一步中主要通过文件名 name 创建对应文件 File 对象。并且会将其缓存在 ContextImpl 的 Map(mSharedPerfsPaths)容器中。 接着我们看 SharedPreferences 的创建过程。
2、SharedPreferences 创建过程
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//得到用于缓存SharedPreferences的Map容器
//该Map容器在ContextImpl单例方式声明
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
//Android N之后不在支持MODE_WORLD_READABLE 和 MODE_WORLD_WRITEABLE
checkMode(mode);
//Android 7.0之后的文件级加密相关
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");
}
}
//SharedPreferences首次创建,实际类型是SharedPreferencesImpl
//SharedPreferences只是一个接口,定义了操作的基本API。
//真正实现是在SharedPreferencesImpl中
sp = new SharedPreferencesImpl(file, mode);
//保存在Map容器中,该Map容器为单例
cache.put(file, sp);
return sp;
}
}
if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {
//MODE_MULTI_PROCESS的加载策略
sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
在该方法中首先看下 getSharedPreferencesCacheLocked 方法如下:
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
//sSharedPrefsCache是static ArrayMap容器
//早期是HashMap,ArrayMap相比HashMap在内存占用上略有一定优势
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
//根据应用包名,获取ArrayMap对象
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
//这里的存储是根据包名,保存所有SharedPreferencesImpl集合
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
sSharedPrefsCache 声明为 static ArrayMap 对象,根据当前应用包名 packageName 得到保存 SharedPreferences 的集合并返回。
回到上面的方法中,根据 File 从返回保存 SharedPreferences 的集合中获取,如果是第一次创建,直接创建 SharedPreferencesImpl 对象,并将其缓存在 Map(sSharedPrefsCache) 容器中。跟踪到这里我们可以确定 SharedPreferences 的实际返回类型是 SharedPreferencesImpl。
小结一下
-
SharedPreferences 只是一个接口,定义了标准操作 API,而真正实现的是 SharedPreferencesImpl,我们后续的一系列对 SharedPreferences 的操作实际都是通过 SharedPreferencesImpl 完成的。
-
系统会将每个 SharedPreferences 文件对应的操作对象(实际为 SharedPreferencesImpl)进行缓存,后续相关 Context.getSharedPreferences("name", mode) 都是从该缓存中直接获取。
-
SharedPreferences 为我们提供了 Context.MODE_MULTI_PROCESS 的加载模式,不知道在上面 getSharedPreferences(File file, int mode) 方法中,你有没有注意到:
if ((mode & Context.MODE_MULTI_PROCESS) != 0 || getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) { //MODE_MULTI_PROCESS的加载策略 sp.startReloadIfChangedUnexpectedly(); }
3、SharedPreferences 的数据加载过程
终于说到 SharedPreferences 数据操作的相关内容了,这部分也是我们要重点讨论的内容,因为这里面或多或少存在一些暗坑,如果对它不足够了解,很容易引发相关性能问题。
上面有分析到 SharedPreferences 的实际操作类型是 SharedPreferencesImpl,它的构造方法如下:
SharedPreferencesImpl(File file, int mode) {
//SharedPreferences保存文件,前面有分析到
mFile = file;
//SharedPreferences备份文件
mBackupFile = makeBackupFile(file);
//加载模式
mMode = mode;
//标志位,表示是否正在加载
mLoaded = false;
mMap = null;
mThrowable = null;
//开启线程,加载对应文件数据到Map容器中
startLoadFromDisk();
}
在 SharedPreferencesImpl 的构造方法中,我们需要重点跟踪方法的最后 startLoadFromDisk 方法如下:
private void startLoadFromDisk() {
synchronized (mLock) {
//加载状态标志位,每当需要加载时,先将其置为false
//加载完成之后再置为true
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
//开启独立线程进行数据加载
loadFromDisk();
}
}.start();
}
mLoaded 起到加载状态标志的作用,该标志状态非常重要(主要是多线程访问等待),如果此时在 UI 线程操作 SharedPreferences 数据,可能导致 UI 线程等待。后面会详细分析到该部分。
SharedPreferences 文件内容加载使用了异步线程,真正开始加载 loadFromDisk 方法如下:
//代码中省略了部分
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;
//主要在MODE_MULTI_PROCESS起到作用
StructStat stat = null;
//确定加载过程中是否发生过异常
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//通过BufferedInputStream从文件中读取内容
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
//SharedPreferences的文件操作都封装在XmlUtils中
//返回Map实例
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) {
//表示SharedPreferences文件中数据已经加载到内存Map中
mLoaded = true;
mThrowable = thrown;
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();
}
}
}
代码篇幅虽然较长,但是不难理解,源码可以看出通过 BufferedInputStream 加载对应 SharedPreferences 文件内容,系统封装了 XmlUtils 进行 XML 文件数据读写,并且将数据封装在 Map 容器并返回,如果整个过程未发生任何异常,则直接将其赋值给 SharedPreferencesImpl 的成员 mMap,声明如下:
private Map<String, Object> mMap;
跟踪到这里 SharedPreferences 的首次加载机制就已经明确了,每个 SharedPreferences 存储都会对应一个 name.xml 文件,在使用时,系统通过异步线程一次性将该文件内容加载到内存中,保存在 Map 容器中。实际后续我们对 SharedPreferences 的一些列 getXxx() 操作都是直接操作的该 Map 容器。后面我们将验证到该部分内容。
小结一下
SharedPreferencesImpl 在初始化时,会开启异步线程加载对应 name 的 XML 文件内容到 Map 容器中,如果文件内容较大,这一过程耗时还是不能忽视的,主要体现在如果此时我们操作 SharedPreferences 会导致线程等待问题,这里主要根据前面分析到的加载状态标志 mLoaded 变量有关,接下来我们就对其分析。
4、一系列 getXxx() 操作
通过前面的分析,你肯定也能想到:SharedPreferences 的数据都保存在 Map 容器中,此时就是根据 Key 到该 Map 容器中查找对应的数据即可,以 getString() 为例:
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
//这里就是根据前面分析到的mLoaded加载状态标志
//判断当前SharedPreferences文件内容是否加载完成
//否则调用方线程进入等待wait
awaitLoadedLocked();
//这里直接就是从Map容器中获取
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
可以看到直接根据 key 到 Map 中查找对应的数据并返回。
这里我们还需要重点跟踪 mLoaded 标志起到的作用,awaitLoadedLocked 方法如下:
private void awaitLoadedLocked() {
if (!mLoaded) {
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) {
//加载状态标志位,如果未加载完成,该变量为false,会将调用线程wait住
try {
mLock.wait();
} catch (InterruptedException unused) {
}
}
if (mThrowable != null) {
throw new IllegalStateException(mThrowable);
}
}
还记得前面分析 SharedPreferences 数据加载过程 mLoaded 标志位,在开始加载文件数据之前先将该标志位置为 false,从文件加载完成之后,重新将其置为 true,表示此次文件内容加载完成。如果加载过程较为耗时,此时我们在 UI 线程中对 SharedPreferences 做相关数据操作,该线程就会进入 wait 状态。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50 ~ 100ms。此时非常容易造成卡顿,如果再严重甚至会引发 ANR。这里涉及到一个优化点,最后会给大家总结出。
mLock 锁的唤醒操作,在 loadFromDisk 方法最后,唤醒所有等待线程(如果存在)
try {
// ... 省略
} catch (Throwable t) {
mThrowable = t;
} finally {
mLock.notifyAll();
}
小结一下
-
mLoaded 标志起到 SharedPreferences 文件内容是否加载完成(加载到 Map 容器中),如果未加载完成,此时对其做相关数据操作就会导致 awaitLoadedLocked 方法的等待。
-
通过 SharedPreferences 存储的数据都会在内存中保留一份(Map 变量中),后续的一系列 getXxx() 操作直接在该容器中获取数据。
5、一系列 putXxx() 操作
前面分析到对 SharedPreferences 的一系列 getXxx() 操作,大家此时是否会认为 putXxx() 操作也是直接对该 Map 容器操作呢?显然不是的,修改数据操作相比 getXxx() 操作要麻烦很多,继续结合源码进行分析:
SharedPreferences config = context.getSharedPreferences("config", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = config.edit().putString("key", "value");
//提交
editor.apply();
put 操作要首先经过 edit 方法返回 Editor 对象:
@Override
public Editor edit() {
synchronized (mLock) {
//这里与一系列getXxx()作用一致
//同样受到mLoaded标志状态的作用
awaitLoadedLocked();
}
//实际返回的是EditorImpl
return new EditorImpl();
}
SharedPreferences 的 edit 方法实际返回的是 EditorImpl 对象:
public final class EditorImpl implements Editor {
private final Object mEditorLock = new Object();
/**
* 保存修改数据的容器
* 一系列添加、修改、删除数据都保存在该临时容器Map中
*/
@GuardedBy("mEditorLock")
private final Map<String, Object> mModified = new HashMap<>();
//标志当前是否是清除操作
@GuardedBy("mEditorLock")
private boolean mClear = false;
//添加String类型数据
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
//添加到一个临时Map容器
mModified.put(key, value);
return this;
}
}
//其它数据类型添加省略
}
Editor 只是一个接口,与 SharedPreferences 功能类似,定义基础操作 API,我们一系列的 putXxx()、remove()、clear()、apply()、commit() 实际都是在 EditorImpl 中完成。
从源码中我们可以看出,操作数据都保存在 EditorImpl 中的 mModified 容器中,最后我们必须通过 commit 或 apply 进行提交,这里也是我们重点要分析的。
这里也需要注意每次通过 SharedPreferences.edit() 都会创建一个新的 EditorImpl 对象,应该尽量批量操作统一提交。最后会一起总结出。
任务提交 commIt 或 apply 方法调用几乎一致,都会经过 commitToMemory 方法后调用 enqueueDiskWrite 方法。不同之处在于 enqueueDiskWrite 方法,如果当前是 commit 提交,则将数据写入文件任务在当前线程执行;否则 apply 提交则将写入文件任务在工作线程中完成,看下详细过程:
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//将mModified容器中数据提交到SharedPreferencesImpl成员Map容器中
//后者数据要写入文件时使用
MemoryCommitResult mcr = commitToMemory();
//将MemoryCommitResult作为参数
//根据策略 commit/apply决定任务在工作线程还是在当前线程
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;
}
我们先来跟踪下 commitToMemory 方法过程:
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
//保存发生变化的key
List<String> keysModified = null;
//外部监听器
Set<OnSharedPreferenceChangeListener> listeners = null;
Map<String, Object> mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
if (mDiskWritesInFlight > 0) {
//数据拷贝
mMap = new HashMap<String, Object>(mMap);
}
//将成员mMap赋值给局部变量,后续for循环中
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
//我们可以监听SharedPreferences数据提交完成
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList<String>();
//这里收集回调通知
listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
}
synchronized (mEditorLock) {
//该标志主要作用是确保当前是否真正发生变化,避免无谓的I/O操作。
boolean changesMade = false;
if (mClear) {
//如果是clear操作,可以看出直接清空数据
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
//这里开始遍历一系列修改后的数据容器mModified
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
if (v == this || v == null) {
if (!mapToWriteToDisk.containsKey(k)) {
//value等于null,然后mMap由不包含该key
//可以直接跳过
continue;
}
//如果value==null,可以直接将其移除
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
//如果mMap容器中包含该key,则直接修正为最新提交数据value
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
//如果value相等则跳过本次
//主要是考虑changesMode标志位,确认当前数据是否真正发生变化
continue;
}
}
//否则直接添加新的key:value
mapToWriteToDisk.put(k, v);
}
//这里在for循环中,如果发生数据变化,该changeMade将会置为true
//表示当前数据发生变化
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
//清空临时修改数据容器
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
其实不难分析出 commitToMemory 方法主要工作是:前面我们一系列的 putXxx() 或 remove() 操作都会添加到 mModified 临时容器中,mModified 保留着我们当前的改变,通过遍历该容器与 mMap(SharedPreferencesImpl 成员)容器做比较,比如相同 key 不同 value 此时将修改提交到 mMap 容器中,然后 mMap 中就保存了修正后,我们最后一次提交的数据。最后清空 mModified 容器。
重新回到前面 commit 方法,调用 enqueueDiskWrite 方法如下:
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
//执行写入文件的Runnalbe任务
//这里也主要区分commit或apply提交的区别
//apply提交会将该任务丢入线程池,异步执行
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//这里执行写入文件操作
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//当commit提交时,会在当前线程执行run方法
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
//commit操作,直接在当前线程中执行
writeToDiskRunnable.run();
return;
}
}
//如果是apply(),提交则将任务加入线程池排队执行
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
writeToDiskRunnable 是执行写入文件操作的任务(就是将最后一次 commitToMemory 之后的 mMap 数据写回到文件)。
如果是 commit 操作,会直接在当前线程中执行 writeToDiskRunnable.run();除了 commit 提交之外,还可以 apply 进行提交,此时 writeToDiskRunnable 任务将被添加到线程池,该线程池只有一个线程,故所有提交的任务都需要经过串行等待执行。(注意:QueuedWork 早期版本实现是只有一个线程的线程池,本文依据 API Level 28 分析,系统已经改成 HandlerThread ,熟悉它的朋友都知道,这仍然是串行执行)
无论是使用 commit 还是 apply 数据提交 ,即使我们只改动其中一个条目,都会把整个内容(mMap)全部写入到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,这也是性能差的重要原因之一。
分析到这里关于 SharedPreferences 数据提交过程:commit 发生在当前线程,apply 发生在工作线程,如果要保证 I/O 操作不阻塞 UI 线程我们可以优先考虑使用 apply 来提交修改,这样是否就绝对安全了呢?这里先告诉大家绝对不是的!!!
6、apply() 异步提交一定安全吗?
前面说到 apply 使写入文件任务发生在工作线程中,这样防止 I/O 操作阻塞 UI 线程;但它同样可能会引发卡顿性能问题,我们需要跟踪另外一部分系统源码:
首先 Android 四大组件的创建以及生命周期管理调用,都是通过进程间通信完成的,到我们自己应用进程,通过调度完成过渡任务的是 ActivityThread,ActivityThread 是我们应用进程的入口类(main 方法所在),来看下 Activity 的 onPause 的回调过程:
@Override
public void handlePauseActivity(IBinder token, boolean show, int configChanges, PendingTransactionActions pendingactions, boolean finalStateRequest, String reason){
//... 省略
if(!r.isPreHoneycomb()){
//这里检查,异步提交的SharedPreferences任务是否已经完成
//否则一直等到执行完成
QueuedWork.waitToFinish();
}
//... 省略
}
你没有看错又要等待,等待什么呢?
我们通过 SharedPreferences 一系列的 apply 提交的任务,都会被加入到工作线程 QueueWork 中,该任务队列以串行方式执行(只有一个工作线程),如果我们 apply 提交非常多的任务,此时判断任务队列还未执行完成,就会一直等到全部执行完成,这就非常容易发生卡顿,如果超过 5s 还会引发 ANR。
由此可见 apply 提交也不是”绝对安全“的,试想当你 apply 提交大量任务,并且还都是大型 key 或 value 时!!!
总结
SharedPreferences 的实际操作者是 SharedPreferencesImpl,当首次创建 SharedPreferences 对象,会根据文件名将对应文件内容使用异步线程一次性加载到 Map 容器中,试想如果此时存储了一些大型 key 或 value 它们一直在内存中得不到释放。如果加载过程中,对其做相关数据操作,会导致线程等待 awaitLoadedLocked。系统会缓存每个使用过的 SharedPreferencesImpl 对象。每当我们 edit 都会创建一个新的 EditorImpl 对象,当修改或者添加数据时会将其添加到 EditorImpl 的 mModifiled 容器中,通过 commit 或 apply 提交后会比较 mModifiled 与 mMap 容器数据,修正(commitToMemory 方法作用) mMap 中最后一次数据提交后写入文件。
使用建议:
1、不要存放大的 key 或 value 在 SharedPreferences 中,否则会一直存储在内存中(Map 容器中)得不到释放,内存使用过高会频繁引发 GC,导致界面丢帧甚至 ANR。
2、不相关的配置选项最好不要放在一起,单个文件越大加载时间越长。(参照 SharedPreferences 初始化时会开启异步线程读取对应文件,如果此时耗时较长,当对其进行相关数据操作时会导致线程等待)
3、读取频繁的 key 和 不频繁的 key 尽量不要放在一起。(如果整个文件本身就较小则可以忽略)
5、commit 提交发生在 UI 线程,apply 提交发生在工作线程,对于数据的提交最好是批量操作统一提交。虽然 apply 任务发生在工作线程(不会因为 I/O 阻塞 UI 线程),但是如果添加过多任务也有可能带来其它”严重后果“(参照系统源码 ActivityThread - handlePauseActivity 方法实现)。
6、尽量不要存放 JSON 或 HTML 类型数据,这种可以直接文件存储。
7、最好能够提前初始化 SharedPreferences,避免 SharedPreferences 第一次创建时读取文件内容线程未结束而出现的等待情况,参照优化点第 2 条。
8、不要指望它能够跨进程通信:Context.MODE_MULTI_PROCESS。
以上便是个人在学习 SharedPreferences 过程中一些心得和体会,文中如有不妥或更好的分析方法,欢迎您的指教!
如果你喜欢我的文章,就请留个赞吧!