一直想深入Android系统的组件 一直没有抽出太多时间来总结 先集百家之所长 做一个简单的知识梳理 后继再研读补充 致敬原创
简介
是 Android 平台为应用开发者提供的一个轻量级的存储辅助类,用来保存应用的一些常用配置,它提供了 putString()、putString(Set<String>)、putInt()、putLong()、putFloat()、putBoolean() 六种数据类型。在应用中通常做一些简单数据的持久化存储,通常用来保存各种配置信息,其本质是一个以“键-值”对的方式保存数据的xml文件,其文件保存在/data/data//shared_prefs目录下
使用详解
获取SharedPreferences的两种方式
- 调用Context对象的getSharedPreferences()方法
- 调用Activity对象的getPreferences()方法
两种方式的区别:
调用Context对象的getSharedPreferences()方法获得的SharedPreferences对象可以被同一应用程序下的其他组件共享.
调用Activity对象的getPreferences()方法获得的SharedPreferences对象只能在该Activity中使用
SharedPreferences的四种操作模式
- Context.MODE_PRIVATE:为默认操作模式,代表该文件是私有数据,只能被应用本身访问,在该模式下,写入的内容会覆盖原文件的内容
- Context.MODE_APPEND:模式会检查文件是否存在,存在就往文件追加内容,否则就创建新文件。
- MODE_WORLD_READABLE:表示当前文件可以被其他应用读取。
- MODE_WORLD_WRITEABLE:表示当前文件可以被其他应用写入。
SharedPreferences的保存、读取 、清除操作
//保存数据
SharedPreferences preferences=getSharedPreferences("user", Context.MODE_PRIVATE);
SharedPreferences.Editor editor=preferences.edit();
String name="xixi";
String age="22";
editor.putString("name", name);
editor.putString("age", age);
editor.commit();
//读取数据
SharedPreferences preferences=getSharedPreferences("user", Context.MODE_PRIVATE);
String name=preferences.getString("name", "defaultname");
String age=preferences.getString("age", "0");
//清除数据
SharedPreferences preferences = getSharedPreferences("user", MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.clear();
- 上面用到的是Context类的getSharedPreferences()方法,需要传入文件名和操作模式,默认为0也就是MODE_PRIVATE。
获取SharedPreferences还有两种方法:Activity类的getPreferences()方法,和PreferenceManager类的静态方法getDefaultSharedPreferences()。前者会自动将当前类名作为文件名,只需要传入操作模式。后者已被系统弃用 不推荐使用。 - 提交SharedPreferences数据时,可以用SharedPreferences.Editor的commit()方法,也 可以用它的apply()方法。两者有什么区别呢,下面的解释来自《阿里巴巴Android开发手册》:
SharedPreference 提 交 数 据 时 , 尽 量 使 用 Editor#apply()
,而非Editor#commit()。一般来讲,仅当需要确定提交结果,并据此有后续操作时,才使用 Editor#commit()。SharedPreference 相关修改使用 apply 方法进行提交会先写入内存,然后异步写入磁盘,commit
方法是直接写入磁盘。如果频繁操作的话 apply 的性能会优于 commit,apply会将最后修改内容写入磁盘。
但是如果希望立刻获取存储操作的结果,并据此做相应的其他操作,应当使用 commit。
源码解读
一 SharedPreferences 文件保存位置
@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 创建对应文件 File 对象。并且会将其缓存在 ContextImpl 的 Map(mSharedPerfsPaths)容器中
二 SharedPreferences 创建过程
如果是已经创建则根据File从返回保存 SharedPreferences 的集合中获取,如果是第一次创建,直接创建 SharedPreferencesImpl 对象,并将其缓存在Map(sSharedPrefsCache) 容器中
三SharedPreferences 数据加载过程
BufferedInputStream 加载对应 SharedPreferences 文件内容,系统封装了 XmlUtils 进行 XML 文件数据读写,并且将数据封装在 Map 容器并返回,如果整个过程未发生任何异常,则直接将其赋值给 SharedPreferencesImpl 的成员 mMap,每个 SharedPreferences 存储都会对应一个 name.xml 文件,在使用时,系统通过异步线程一次性将该文件内容加载到内存中,保存在 Map 容器中。实际后续我们对 SharedPreferences 的一些列 getXxx() 操作都是直接操作的该 Map 容器
优化点:
SharedPreferencesImpl 在初始化时,会开启异步线程加载对应 name 的 XML 文件内容到 Map 容器中,如果文件内容较大,这一过程耗时还是不能忽视的,主要体现在如果此时我们操作 SharedPreferences 会导致线程等待问题,这里主要根据前面分析到的加载状态标志 mLoaded 变量有关,接下来我们就对其进行分析
四 一系列 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;
}
}
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);
}
}
在开始加载文件数据之前先将该标志位置为 false,从文件加载完成之后,重新将其置为 true,表示此次文件内容加载完成。如果加载过程较为耗时,此时我们在 UI 线程中对 SharedPreferences 做相关数据操作,该线程就会进入 wait 状态。这就导致出现主线程等待低优先级线程锁的问题,比如一个 100KB 的 SP 文件读取等待时间大约需要 50 ~ 100ms。此时非常容易造成卡顿,如果再严重甚至会引发 ANR
1.mLoaded 标志起到 SharedPreferences 文件内容是否加载完成(加载到 Map 容器中),如果未加载完成,此时对其做相关数据操作就会导致 awaitLoadedLocked 方法的等待。
2.通过 SharedPreferences 存储的数据都会在内存中保留一份(Map 变量中),后续的一系列 getXxx() 操作直接在该容器中获取数据。
五 一系列 putXxx() 操作
Editor 只是一个接口,与 SharedPreferences 功能类似,定义基础操作 API,我们一系列的 putXxx()、remove()、clear()、apply()、commit() 实际都是在 EditorImpl 中完成。
从源码中我们可以看出,操作数据都保存在 EditorImpl 中的 mModified 容器中,最后我们必须通过 commit 或 apply 进行提交,这里也是我们重点要分析的。
这里也需要注意每次通过 SharedPreferences.edit() 都会创建一个新的 EditorImpl 对象,应该尽量批量操作统一提交
任务提交 commIt 或 apply 方法调用几乎一致,都会经过 commitToMemory 方法后调用 enqueueDiskWrite 方法。不同之处在于 enqueueDiskWrite 方法,如果当前是 commit 提交,则将数据写入文件任务在当前线程执行;否则 apply 提交则将写入文件任务在工作线程中完成
如果是 commit 操作,会直接在当前线程中执行 writeToDiskRunnable.run();除了 commit 提交之外,还可以 apply 进行提交,此时 writeToDiskRunnable 任务将被添加到线程池,该线程池只有一个线程,故所有提交的任务都需要经过串行等待执行
无论是使用 commit 还是 apply 数据提交 ,即使我们只改动其中一个条目,都会把整个内容(mMap)全部写入到文件。而且即使我们多次写入同一个文件,SP 也没有将多次修改合并为一次,这也是 SharedPreferences 性能差的重要原因之一
六 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 中最后一次数据提交后写入文件。
优化建议:
- 不要存放大的 key 或 value 在 SharedPreferences 中,否则会一直存储在内存中(Map 容器中)得不到释放,内存使用过高会频繁引发 GC,导致界面丢帧甚至 ANR。
- 不相关的配置选项最好不要放在一起,单个文件越大加载时间越长。(参照 SharedPreferences 初始化时会开启异步线程读取对应文件,如果此时耗时较长,当对其进行相关数据操作时会导致线程等待)
- 读取频繁的 key 和 不频繁的 key 尽量不要放在一起。(如果整个文件本身就较小则可以忽略)
- 不要每次都 edit 操作,每次 edit 都会创建新的 EditorImpl 对象,最好批量处理统一提交。否则每次 edit().commit() 都会创建新的 EditorImpl 对象并进行一次 I/O 操作,严重影响性能。
- commit 提交发生在 UI 线程,apply 提交发生在工作线程,对于数据的提交最好是批量操作统一提交。虽然 apply 任务发生在工作线程(不会因为 I/O 阻塞 UI 线程),但是如果添加过多任务也有可能带来其它”严重后果“(参照系统源码 ActivityThread - handlePauseActivity 方法实现)。
- 尽量不要存放 JSON 或 HTML 类型数据,这种可以直接文件存储。
- 最好能够提前初始化 SharedPreferences,避免 SharedPreferences 第一次创建时读取文件内容线程未结束而出现的等待情况,参照优化点第 2 条。
- 不要指望它能够跨进程通信:Context.MODE_MULTI_PROCESS