一、SharedPreferences 简略应用
1、创立
第一个参数是贮存的 xml 文件名称,第二个是打开方式,个别就用
Context.MODE_PRIVATE;SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
2、写入
// 能够创立一个新的 SharedPreference 来对贮存的文件进行操作
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
// 像 SharedPreference 中写入数据须要应用 Editor
SharedPreference.Editor editor = sp.edit();
// 相似键值对
editor.putString("name", "string");
editor.putInt("age", 0);
editor.putBoolean("read", true);
//editor.apply();
editor.commit();
- apply 和 commit 都是提交保留,区别在于 apply 是异步执行的,不须要期待。不管删除,批改,减少都必须调用 apply 或者 commit 提交保留;
- 对于更新:如果曾经插入的 key 曾经存在。那么将更新原来的 key;
- 应用程序一旦卸载,SharedPreference 也会被删除;
3、读取
SharedPreference sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
// 第一个参数是键名,第二个是默认值
String name=sp.getString("name", "暂无");
int age=sp.getInt("age", 0);
boolean read=sp.getBoolean("isRead", false);
4、检索
SharedPreferences sp=context.getSharedPreferences("名称", Context.MODE_PRIVATE);
// 查看以后键是否存在
boolean isContains=sp.contains("key");
// 应用 getAll 能够返回所有可用的键值
//Map<String,?> allMaps=sp.getAll();
5、删除
当咱们要革除 SharedPreferences 中的数据的时候肯定要先 clear()、再 commit(),不能间接删除 xml 文件;
SharedPreference sp=getSharedPreferences("名称", Context.MODE_PRIVATE);
SharedPrefence.Editor editor=sp.edit();
editor.clear();
editor.commit();
- getSharedPreference() 不会生成文件,这个大家都晓得;
- 删除掉文件后,再次执行 commit(),删除的文件会新生,新生文件的数据和删除之前的数据雷同;
- 删除掉文件后,程序在没有齐全退出进行运行的状况下,Preferences 对象所存储的内容是不变的,尽管文件没有了,但数据仍然存在;程序齐全退出进行之后,数据才会失落;
-
革除 SharedPreferences 数据肯定要执行 editor.clear(),editor.commit(),不能只是简略的删除文件,这也就是最初的论断,须要留神的中央
二、SharedPreferences 源码剖析
1、创立
SharedPreferences preferences = getSharedPreferences("test", Context.MODE_PRIVATE);
实际上 context 的真正实现类是 ContextImp,所以进入到 ContextImp 的 getSharedPreferences 办法查看:
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
......
File file;
synchronized (ContextImpl.class) {if (mSharedPrefsPaths == null) {
// 定义类型:ArrayMap<String, File> mSharedPrefsPaths;
mSharedPrefsPaths = new ArrayMap<>();}
// 从 mSharedPrefsPaths 中是否可能失去 file 文件
file = mSharedPrefsPaths.get(name);
if (file == null) {// 如果文件为 null
// 就创立 file 文件
file = getSharedPreferencesPath(name);
将 name,file 键值对存入汇合中
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
ArrayMap<String, File> mSharedPrefsPaths; 对象是用来存储 SharedPreference 文件名称和对应的门路,获取门路是在下列办法中,就是获取 data/data/ 包名 /shared_prefs/ 目录下的
@Override
public File getSharedPreferencesPath(String name) {return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {synchronized (mSync) {if (mPreferencesDir == null) {mPreferencesDir = new File(getDataDir(), "shared_prefs");
}
return ensurePrivateDirExists(mPreferencesDir);
}
}
门路之后才开始创建对象
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
// 重点 1
checkMode(mode);
.......
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
// 获取缓存对象(或者创立缓存对象)final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
// 通过键 file 从缓存对象中获取 Sp 对象
sp = cache.get(file);
// 如果是 null,就阐明缓存中还没后该文件的 sp 对象
if (sp == null) {
// 重点 2:从磁盘读取文件
sp = new SharedPreferencesImpl(file, mode);
// 增加到内存中
cache.put(file, sp);
// 返回 sp
return sp;
}
}
// 如果设置为 MODE_MULTI_PROCESS 模式,那么将执行 SP 的 startReloadIfChangedUnexpectedly 办法。if ((mode & Context.MODE_MULTI_PROCESS) != 0 ||
getApplicationInfo().targetSdkVersion < android.os.Build.VERSION_CODES.HONEYCOMB) {sp.startReloadIfChangedUnexpectedly();
}
return sp;
}
就是重载之前的办法,只是入参由文件名改为 File 了,给创立过程加锁了 synchronized,通过办法 getSharedPreferencesCacheLocked() 获取零碎中存储的所有包名以及对应的文件,这就是每个 sp 文件只有一个对应的 SharedPreferencesImpl 实现对象起因
流程:
- 获取缓存区,从缓存区中获取数据,看是否存在 sp 对象,如果存在就间接返回
- 如果不存在,那么就从磁盘获取数据,
- 从磁盘获取的数据之后,增加到内存中,
- 返回 sp;
getSharedPreferencesCacheLocked
private ArrayMap<File, SharedPreferencesImpl> getSharedPreferencesCacheLocked() {if (sSharedPrefsCache == null) {sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap<File, SharedPreferencesImpl> packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
- getSharedPreferences(File file, int mode) 办法中,从下面的零碎缓存中分局 File 获取 - SharedPreferencesImpl 对象,如果之前没有应用过,就须要创立一个对象了,通过办法 checkMode(mode);
- 先查看 mode 是否是三种模式,而后通过 sp = new SharedPreferencesImpl(file, mode);
-
创建对象,并将创立的对象放到零碎的 packagePrefs 中,不便当前间接获取;
SharedPreferencesImpl(File file, int mode) { mFile = file; // 存储文件 // 备份文件(灾备文件)mBackupFile = makeBackupFile(file); // 模式 mMode = mode; // 是否加载过了 mLoaded = false; // 存储文件内的键值对信息 mMap = null; // 从名字能够晓得是:开始加载数据从磁盘 startLoadFromDisk();}
- 次要是设置了几个参数,mFile 是原始文件;mBackupFile 是后缀.bak 的备份文件;
- mLoaded 标识是否正在加载批改文件;
- mMap 用来存储 sp 文件中的数据,存储时候也是键值对模式,获取时候也是通过这个获取,这就是示意每次应用 sp 的时候,都是将数据写入内存,也就是 sp 数据存储数据快的起因,所以 sp 文件不能存储大量数据,否则执行时候很容易会导致 OOM;
- mThrowable 加载文件时候报的谬误;
- 上面就是加载数据的办法 startLoadFromDisk(); 从 sp 文件中加载数据到 mMap 中
2、startLoadFromDisk()
private void startLoadFromDisk() {synchronized (mLock) {mLoaded = false;}
// 开启子线程加载磁盘数据
new Thread("SharedPreferencesImpl-load") {public void run() {loadFromDisk();
}
}.start();}
private void loadFromDisk() {synchronized (mLock) {
// 如果加载过了 间接返回
if (mLoaded) {return;}
// 备份文件是否存在,if (mBackupFile.exists()) {
// 删除 file 原文件
mFile.delete();
// 将备份文件命名为:xml 文件
mBackupFile.renameTo(mFile);
}
}
.......
Map map = null;
StructStat stat = null;
try {
// 上面的就是读取数据
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
} catch (Exception e) {Log.w(TAG, "Cannot read" + mFile.getAbsolutePath(), e);
} finally {IoUtils.closeQuietly(str);
}
}
} catch (ErrnoException e) {/* ignore */}
synchronized (mLock) {
// 曾经加载结束,mLoaded = true;
// 数据不是 null
if (map != null) {
// 将 map 赋值给全局的存储文件键值对的 mMap 对象
mMap = map;
// 更新内存的批改工夫以及文件大小
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {mMap = new HashMap<>();
}
// 重点:唤醒所有以 mLock 锁的期待线程
mLock.notifyAll();}
}
- 首先判断备份文件是否存在,如果存在,就更该备份文件的后缀名;接着就开始读取数据,而后将读取的数据赋值给全局变量存储文件键值对的 mMap 对象,并且更新批改工夫以及文件大小变量;
- 唤醒所有以 mLock 为锁的期待线程;
- 到此为止,初始化 SP 对象就算实现了,其实能够看进去就是一个二级缓存流程:磁盘到内存;
3、get 获取 SP 中的键值对
@Nullable
public String getString(String key, @Nullable String defValue) {synchronized (mLock) { 锁判断
awaitLoadedLocked(); // 期待机制
String v = (String)mMap.get(key); // 从键值对中获取数据
return v != null ? v : defValue;
}
}
private void awaitLoadedLocked() {
.......
while (!mLoaded) { // 在加载数据结束的时候,值为 true
try {
// 线程期待
mLock.wait();} catch (InterruptedException unused) {}}
}
如果数据没有加载结束(也就是说 mLoaded=false),此时将线程期待;
4、putXXX 以及 apply 源码
public Editor edit() {
// 跟 getXXX 原理一样
synchronized (mLock) {awaitLoadedLocked();
}
// 返回 EditorImp 对象
return new EditorImpl();}
public Editor putBoolean(String key, boolean value) {synchronized (mLock) {mModified.put(key, value);
return this;
}
}
public void apply() {final long startTime = System.currentTimeMillis();
// 依据名字能够晓得:提交数据到内存
final MemoryCommitResult mcr = commitToMemory();
........
// 提交数据到磁盘中
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
// 重点:调用 listener
notifyListeners(mcr);
}
- 先执行了 commitToMemory,提交数据到内存;而后提交数据到磁盘中;
- 紧接着调用了 listener;
5、commitToMemory
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
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);
}
// 赋值此时缓存汇合给 mapToWriteToDisk
mapToWriteToDisk = mMap;
.......
synchronized (mLock) {
boolean changesMade = false;
// 重点:是否清空数据
if (mClear) {if (!mMap.isEmpty()) {
changesMade = true;
// 清空缓存中键值对信息
mMap.clear();}
mClear = false;
}
// 循环 mModified,将 mModified 中的数据更新到 mMap 中
for (Map.Entry<String, Object> e : mModified.entrySet()) {String k = e.getKey();
Object v = e.getValue();
// "this" is the magic value for a removal mutation. In addition,
// setting a value to "null" for a given key is specified to be
// equivalent to calling remove on that key.
if (v == this || v == null) {if (!mMap.containsKey(k)) {continue;}
mMap.remove(k);
} else {if (mMap.containsKey(k)) {Object existingValue = mMap.get(k);
if (existingValue != null && existingValue.equals(v)) {continue;}
}
// 留神:此时把键值对信息写入到了缓存汇合中
mMap.put(k, v);
}
.........
}
// 清空长期汇合
mModified.clear();
......
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
- mModified 就是咱们本次要更新增加的键值对汇合;
- mClear 是咱们调用 clear() 办法的时候赋值的;
- 大抵流程就是:首先判断是否须要清空内存数据,而后循环 mModified 汇合,增加更新数据到内存的键值对汇合中;
6、commit 办法
public boolean commit() {
.......
// 更新数据到内存
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");
}
}
// 执行 listener 回调
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
- 首先 apply 没有返回值,commit 有返回值;
- 其实 apply 执行回调是和数据写入磁盘并行执行的,而 commit 办法执行回调是期待磁盘写入数据实现之后;
二、QueuedWork 详解
1、QueuedWork
QueuedWork 这个类,因为 sp 的初始化之后就是应用,后面看到,无论是 apply 还是 commit 办法都是通过 QueuedWork 来实现的;
QueuedWork 是一个治理类,顾名思义,其中有一个队列,对所有入队的 work 进行治理调度;
其中最重要的就是有一个 HandlerThread
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;
}
}
2、入队 queue
// 如果是 commit,则不能 delay,如果是 apply,则能够 delay
public static void queue(Runnable work, boolean shouldDelay) {Handler handler = getHandler();
synchronized (sLock) {sWork.add(work);
if (shouldDelay && sCanDelay) {
// 默认 delay 的工夫是 100ms
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
3、音讯的解决
private static class QueuedWorkHandler extends Handler {
static final int MSG_RUN = 1;
QueuedWorkHandler(Looper looper) {super(looper);
}
public void handleMessage(Message msg) {if (msg.what == MSG_RUN) {processPendingWork();
}
}
}
private static void processPendingWork() {synchronized (sProcessingWork) {
LinkedList<Runnable> work;
synchronized (sLock) {work = (LinkedList<Runnable>) sWork.clone();
sWork.clear();
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {for (Runnable w : work) {w.run();
}
}
}
}
- 能够看到,调度非常简单,外部有一个 sWork,须要执行的时候遍历所有的 runnable 执行;
- 对于 apply 操作,会有肯定的提早再去执行 work,然而对于 commit 操作,则会马上触发调度,而且并不仅仅是调度 commit 传过来的那个工作,而是马上就调度队列中所有的 work;
4、waitToFinish
零碎中很多中央会期待 sp 的写入文件实现,期待形式是通过调用 QueuedWork.waitToFinish();
public static void waitToFinish() {Handler handler = getHandler();
synchronized (sLock) {
// 移除所有音讯,间接开始调度所有 work
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {handler.removeMessages(QueuedWorkHandler.MSG_RUN);
}
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
// 如果是 waitToFinish 调用过去,则马上执行所有的 work
processPendingWork();} finally {StrictMode.setThreadPolicy(oldPolicy);
}
try {
// 在所有的 work 执行结束之后,还须要执行 Finisher
// 后面在 apply 的时候有一步是 QueuedWork.addFinisher(awaitCommit);
// 其中的实现是期待 sp 文件的写入实现
// 如果没有通过 msg 去调度而是通过 waitToFinish,则那个 runnable 就会在这里被执行
while (true) {
Runnable finisher;
synchronized (sLock) {finisher = sFinishers.poll();
}
if (finisher == null) {break;}
finisher.run();}
} finally {sCanDelay = true;}
...
}
零碎中对于四大组件的解决逻辑都在 ActivityThread 中实现,在 service/activity 的生命周期的执行中都会期待 sp 的写入实现,正是通过调用 QueuedWork.waitToFinish(),确保 app 的数据正确的写入到 disk;
5、sp 应用的倡议
- 对数据实时性要求不高,尽量应用 apply
- 如果业务要求必须数据胜利写入,应用 commit
- 缩小 sp 操作频次,尽量一次 commit 把所有的数据都写入结束
- 能够适当思考不要在主线程拜访 sp
-
写入 sp 的数据尽量轻量级
总结:
SharedPreferences 的自身实现就是分为两步,一步是内存,一部是磁盘,而主线程又依赖 SharedPreferences 的写入,所以可能当 io 成为瓶颈的时候,App 会因为 SharedPreferences 变的卡顿,重大状况下会 ANR,总结下来有以下几点:
- 寄存在 xml 文件中的数据会被装在到内存中,所以获取数据很快
- apply 是异步操作,提交数据到内存,并不会马上提交到磁盘
- commit 是同步操作,会期待数据写入到磁盘,并返回后果
- 如果有同一个线程屡次 commit,则前面的要期待后面执行完结
- 如果多个线程对同一个 sp 并发 commit,前面的所有工作会进入到 QueuedWork 中排队执行,且都要等第一个执行结束