Android 存储优化系列专题
- SharedPreferences 系列
《Android 之不要滥用 SharedPreferences(上)》
《Android 之不要滥用 SharedPreferences(下)》
- ContentProvider 系列(待完善)
《Android 存储选项之 ContentProvider 启动存在的暗坑》
《Android 存储选项之 ContentProvider 深入分析》
- 对象序列化系列
《Android 对象序列化之你不知道的 Serializable》
《Android 对象序列化之 Parcelable 深入分析》
《Android 对象序列化之追求完美的 Serial》
- 数据序列化系列(待更)
《Android 数据序列化之 JSON》
《Android 数据序列化之 Protocol Buffer 使用》
《Android 数据序列化之 Protocol Buffer 源码分析》
- SQLite 存储系列
《Android 存储选项之 SQLiteDatabase 创建过程源码分析》
《Android 存储选项之 SQLiteDatabase 源码分析》
《数据库连接池 SQLiteConnectionPool 源码分析》
《SQLiteDatabase 启用事务源码分析》
《SQLite 数据库 WAL 模式工作原理简介》
《SQLite 数据库锁机制与事务简介》
《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。
本文基于 API Level 28 分析的 ContextImpl 中关于 SharedPreferences 处理机制,这相较于较早版本的管理策略有所不同,具体你可以参考之前基于 API Level 16 源码分析的 SharedPreferences。
小结一下
-
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(); }
当应用指定的 targetSdkVersion 小于 API Level 11 时,则重新从文件中加载一遍数据到内存中,所以指望 SharedPreferences 能够跨进程通信的可以死心了。关于 SharedPreferences 跨进程使用分析你可以参考《Android 之不要滥用SharedPreferences(下)》
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();
}
有关 SharedPreferences 的备份文件 mBackupFile 的作用,由于这部分内容也比较多,主要涉及到 SharedPreferences 的数据丢失,和多进程使用场景,如果想更深入了解该部分内容你可以参考这里。
在 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);
}
系统考虑到本次提交数据是否真正发生变化:changesMade 变量的作用,否则在提交时会直接 return 处理,该部分内容你可以参考下一篇《Android 之不要滥用 SharedPreferences(下)》。这算是一层优化,避免无谓的 I/O 操作。
其实不难分析出 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 性能差的重要原因之一。
分析到这里关于 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 尽量不要放在一起。(如果整个文件本身就较小则可以忽略)
4、不要每次都 edit 操作,每次 edit 都会创建新的 EditorImpl 对象,最好批量处理统一提交。否则每次 edit().commit() 都会创建新的 EditorImpl 对象并进行一次 I/O 操作,严重影响性能。
5、commit 提交发生在 UI 线程,apply 提交发生在工作线程,对于数据的提交最好是批量操作统一提交。虽然 apply 任务发生在工作线程(不会因为 I/O 阻塞 UI 线程),但是如果添加过多任务也有可能带来其它”严重后果“(参照系统源码 ActivityThread - handlePauseActivity 方法实现)。
6、尽量不要存放 JSON 或 HTML 类型数据,这种可以直接文件存储。
7、最好能够提前初始化 SharedPreferences,避免 SharedPreferences 第一次创建时读取文件内容线程未结束而出现的等待情况,参照优化点第 2 条。
8、不要指望它能够跨进程通信:Context.MODE_MULTI_PROCESS。
以上便是对 SharedPreferences 的学习心得和指导建议,文中如果不妥或有更好的分析结果,欢迎你的指正。
有关 SharedPreferences 更深入分析请参考下篇《Android 之不要滥用SharedPreferences(下)》。
文章如果对你有帮助,请留个赞吧!如果喜欢我的分析还可以阅读系列的其他相关文章。
扩展阅读
- Android 存储优化系列专题
- Android 之权限管理只防君子不防小人
- Android 存储选项之 SQLite 优化那些事儿
- Android 对象序列化之 Parcelable 取代 Serializable ?
- Android 对象序列化之你不知道的 Serializable
...
UI 优化系列
- 关于 UI 渲染,你需要了解什么?
- Android 之如何优化 UI 渲染(上)
- Android 之你真的了解 View.post() 原理吗?
- 深入 Activity 三部曲(1)View 绘制流程之 setContentView() 到底做了什么 ?
-
Android 之 Choreographer 详细分析
...
网络优化系列
网友评论