先来一波灵魂追问:
( 年末福利: 知道你很忙,参考答案可直接看文末... )
一切从getSharedPreference(String name,int Mode)这个方法说起;通过这个方法获取到一个SharedPreference实例。SharedPreferences是一个接口(interface),他的具体实现类为SharedPreferencesImpl。 SharedPreference的加载的主要过程:
private static ArrayMap> sSharedPrefsCache;
private ArrayMap mSharedPrefsPaths;
复制代码
sSharedPrefsCache存储的是File和SharedPreferencesImpl键值对,当对应File的SharedPreferencesImpl加载之后就会一支存储于sSharedPrefsCache中。类似的mSharedPrefsPaths存储的是name和File的对应关系。使用的ArrayMap,关于ArrayMap这种Android特有的数据结构,详细了解可以看这juejin.im/post/684490…
当通过name最终找到对应的File之后,就会实例化一个SharedPreferencesImpl对象。在SharedPreferences构造方法中开启一个子线程加载磁盘中的xml文件。
大家都应该很明确的一点是,SP持久化的本质是在本地磁盘记录了一个xml文件,这个文件所在的文件夹shared_prefs
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
复制代码
怎么保证使用sp.get(String name)的时候SP的初始化或者说从磁盘中加载到内存中这一过程已经完成了呢?
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() {
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);
}
}
复制代码
使用awaitLoadedLocked()方法检测,是否已经加载完成,如果没有加载完成,就等待堵塞。等加载完成之后,继续执行;
在loadFromDisk()方法中,如果加载成功会把mLoaded标志位置为true,然后 mLock.notifyAll();
最终,就把位于磁盘中的文件,加载到了内存中对应一个SharedPreferces对象,SharedPreferences中mMap。
当想SP中存入数据的时候,实例代码如下。
sharedPreferences.edit().putInt("number", 100).puString("age","18").apply();
sharedPreferences.edit().putInt("number", 100).commit();
复制代码
调用sharedPreferences.edit()返回一个EditorImpl对象,操作数据之后调用apply()或者commit()。
2.1、 commit()流程
@Override
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 {}
notifyListeners(mcr);//通知监听
return mcr.writeToDiskResult;
}
//
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
//如果postWriteRunnable为空表示来自commit()方法调用
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);//写入磁盘
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
//当commit提交,且mDiskWritesInFlight为1的时候,直接在当前所在线程执行写入磁盘操作
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//交个QueuedWork,QueuedWork内部维护了一个HandlerThread,一直执行写入磁盘操作。
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
复制代码
如注释:当调用commit()方法之后
首先将编辑的结果同步到内存中。
enqueueDiskWrite()将这个结果同步到磁盘中,enqueueDiskWrite()的第二个参数postWriteRunnable传入空。通常情况下也就是mDiskWritesInFlight(正在执行的写入磁盘操作的数量)为1的时候,直接在当前所在线程执行写入磁盘操作。否则还是异步到QueuedWork中去执行。commit()时,写入磁盘操作会发生在当前线程的说法是不准确的。
执行mcr.writtenToDiskLatch.await(); MemoryCommitResult 中有个一个CountDownLatch 成员变量,他的具体作用可以查阅其他资料。总的来说,当前线程执行会堵塞在这,直到mcr.writtenToDiskLatch满足了条件。也就是当写入磁盘成功之后,会继续执行下面的操作。
所以,commit提交之后会有返回结果,同步堵塞直到有返回结果。
2.2、 apply()流程
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {mcr.writtenToDiskLatch.await();}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
notifyListeners(mcr);
}
复制代码
2.3 、QueuedWork
2.3.1、 关于延迟磁盘写入。
/** Delay for delayed runnables, as big as possible but low enough to be barely perceivable */
private static final long DELAY = 100;
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
复制代码
2.3.2、主线程堵塞ANR
You don't need to worry about Android component lifecycles and their interaction with apply() writing to disk. The framework makes sure in-flight disk writes from apply() complete before switching states.
官方文档中有这样段化话,意思是您不需要担心Android组件生命周期及其对apply()写入磁盘的影响。框层架确保在切换状态之前完成使用apply()方法正在执行磁盘写入的动作。
然而还真是不让人那么省心。
罪魁祸首在这:
//QueuedWork.java
public static void waitToFinish() {
...
processPendingWork();//执行文件写入磁盘操作
....
}
private static void processPendingWork() {
long startTime = 0;
....
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
...
}
复制代码
waitToFinish()会将,储存在QueuedWork的操作一并处理掉。什么时候呢?在Activiy的 onPause()、BroadcastReceiver的onReceive()以及Service的onStartCommand()方法之前都会调用waitToFinish()。大家知道这些方法都是执行在主线程中,一旦waitToFinish()执行超时,就会跑出ANR。
至于waitToFinish调用具体时机,查看ActivityThread.java类文件。这里只是说本质原理。
\\ContextImpl
private void checkMode(int mode) {
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");
}
}
}
复制代码
Andorid 7.0及以上会抛出异常,Sharepreferences不再支持多进程模式。多进程共享文件会出现问题的本质在于,因为不同进程,所以线程同步会失效。要解决这个问题,可尝试跨进程解决方案,如ContentProvider、AIDL、AIDL、Service。
既然SharedPreferences有这么多问题?就没人管管吗? 温和的治理方法或者说小建议
通过本文我们了解了SharedPreferences的基本原理。再回头看看文章开头的那几个问题,是不是有答案了。
彻底搞懂 SharedPreferences
我们知道 SharedPreferences
会从文件读取 xml 文件, 并将其以 getXxx/putXxx
的形式提供读写服务. 其中涉及到如下几个问题:
1. 如何从磁盘读取配置到内存
2. getXxx 如何从内存中获取配置
3. 最终配置如何从内存回写到磁盘
4. 多线程/多进程是否会有问题
5. 最佳实践
SharedPreferences
是线程安全的. 内部由大量 synchronized
关键字保障SharedPreferences
不是进程安全的getSharedPreferences
会读取磁盘文件, 后续的 getSharedPreferences
会从内存缓存中获取. 如果第一次调用 getSharedPreferences
时还没从磁盘加载完毕就调用 getXxx/putXxx , 则 getXxx/putXxx 操作会卡主, 直到数据从磁盘加载完毕后返回apply
是同步回写内存, 然后把异步回写磁盘的任务放到一个单线程的队列中等待调度. commit
和前者一样, 只不过要等待异步磁盘任务结束后才返回MODE_MULTI_PROCESS
是在每次 getSharedPreferences
时检查磁盘上配置文件上次修改时间和文件大小, 一旦所有修改则会重新从磁盘加载文件. 所以并不能保证多进程数据的实时同步MODE_MULTI_PROCESS
. 这个标记就像 MODE_WORLD_READABLE/MODE_WORLD_WRITEABLE
未来会被废弃apply
/ commit
都会把全部的数据一次性写入磁盘, 所以单个的配置文件不应该过大, 影响整体性能现在我们从源码入手, 彻底搞懂 SharedPreferences
有关的所有问题. 本文并不是入门教程, 主要面向有 SharedPreferences
基础的同学
一般来说有如下方式:
1. PreferenceManager.getDefaultSharedPreferences
2. ContextImpl.getSharedPreferences
我们以上述 [1] 为例来看看源码:
// PreferenceManager.java
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
上述无论那种方法, 最终都是调用到了 ContextImpl.getSharedPreferences
. 源码:
// ContextImpl.java
public SharedPreferences getSharedPreferences(File file, int mode) {
checkMode(mode);
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap<File, SharedPreferencesImpl> cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return 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;
}
可见 sdk 是先取了缓存, 如果缓存未命中, 才构造对象. 也就是说, 多次 getSharedPreferences
几乎是没有代价的. 同时, 实例的构造被 synchronized
关键字包裹, 因此构造过程是多线程安全的
我们再来看看第一次构建对象时发生了什么:
// SharedPreferencesImpl.java
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
有这么几个关键信息:
1. mFile
代表我们磁盘上的配置文件
2. mBackupFile
是一个灾备文件, 用户写入失败时进行恢复, 后面会再说. 其路径是 mFile
加后缀 ‘.bak’
3. mMap
用于在内存中缓存我们的配置数据, 也就是 getXxx
数据的来源
还涉及到一个 startLoadFromDisk
, 我们来看看:
// SharedPreferencesImpl.java
private void startLoadFromDisk() {
synchronized (this) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
开启了一个线程从文件读取, 其源码如下:
// SharedPreferencesImpl.java
private void loadFromDisk() {
synchronized (SharedPreferencesImpl.this) {
if (mLoaded) {
return;
}
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
... 略去无关代码 ...
str = new BufferedInputStream(
new FileInputStream(mFile), 16*1024);
map = XmlUtils.readMapXml(str);
synchronized (SharedPreferencesImpl.this) {
mLoaded = true;
if (map != null) {
mMap = map;
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
notifyAll();
}
}
loadFromDisk
这个函数很关键. 它就是实际从磁盘读取配置文件的函数. 可见, 它做了如下几件事:
1. 如果有 ‘灾备’ 文件, 则直接使用灾备文件回滚.
2. 把配置从磁盘读取到内存的并保存在 mMap
字段中(看代码最后 mMap = map
)
3. 标记读取完成, 这个字段后面 awaitLoadedLocked
会用到. 记录读取文件的时间, 后面 MODE_MULTI_PROCESS
中会用到
4. 发一个 notifyAll
通知已经读取完毕, 激活所有等待加载的其他线程
总结一下(点击大图):
// SharedPreferencesImpl.java
public String getString(String key, @Nullable String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
可见, 所有的 get 操作都是线程安全的. 并且 get 仅仅是从内存中(mMap
) 获取数据, 所以无性能问题.
考虑到 配置文件的加载 是在单独的线程中异步进行的(参考 ‘SharedPreferences 的构造’), 所以这里的 awaitLoadedLocked
是在等待配置文件加载完毕. 也就是说如果我们第一次构造 SharedPreferences 后就立刻调用 getXxx 方法, 很有可能读取配置文件的线程还未完成, 所以这里要等待该线程做完相应的加载工作. 来看看 awaitLoadedLocked
的源码:
// SharedPreferencesImpl.java 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 { wait(); } catch (InterruptedException unused) { } } }
很明显, 如果加载还未完成(mLoaded == false
), getXxx 会卡在 awaitLoadedLocked
, 一旦加载配置文件的线程工作完毕, 则这个加载线程会通过 notifyAll
会通知所有在 awaitLoadedLocked
中等待的线程, getXxx 就能够返回了. 不过大部分情况下, mLoaded == true
. 这样的话 awaitLoadedLocked
会直接返回
set 比 get 稍微麻烦一点儿, 因为涉及到 Editor
和 MemoryCommitResult
对象
先来看看 edit()
方法的实现:
// SharedPreferencesImpl.java
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 (this) {
awaitLoadedLocked();
}
return new EditorImpl();
}
Editor
Editor
没有构造函数, 只有两个属性被初始化:
// SharedPreferencesImpl.java
public final class EditorImpl implements Editor {
private final Map<String, Object> mModified = Maps.newHashMap();
private boolean mClear = false;
... 略去方法定义 ...
public Editor putString(String key, @Nullable String value) { ... }
public boolean commit() { ... }
...
}
mModified
是我们每次 putXxx 后所改变的配置项mClear
标识要清空配置项, 但是只清了 SharedPreferences.mMap
. 所以不要写这样的愚蠢代码:sharedPreferences.edit()
.putBoolean("foo", true) // foo 无法被 clear 掉
.clear()
.putBoolean("bar", true)
.commit()
edit()
会保障配置已从磁盘读取完毕, 然后仅仅创建了一个对象. 接下来看看 putXxx
的真身:
// SharedPreferencesImpl.java
public Editor putString(String key, @Nullable String value) {
synchronized (this) {
mModified.put(key, value);
return this;
}
}
很简单, 仅仅是把我们设置的配置项放到了 mModified
属性里保存. 等到 apply
或者 commit
的时候回写到内存和磁盘. 咱们分别来看看
apply
apply 是各种 ‘最佳实践’ 推荐的方式, 那么它到底是怎么异步工作的呢? 我们来看个究竟:
// SharedPreferencesImpl.java
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
public void run() {
try {
mcr.writtenToDiskLatch.await();
} catch (InterruptedException ignored) {
}
}
};
QueuedWork.add(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.remove(awaitCommit);
}
};
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);
}
可以看出大致的脉络:
1. commitToMemory
应该是把修改的配置项回写到内存
2. QueuedWork.add(awaitCommit)
貌似没什么卵用
3. SharedPreferencesImpl.this.enqueueDiskWrite
把配置项加入到一个异步队列中, 等待调度
我们来看看 commitToMemory
的实现(略去大量无关代码):
// SharedPreferencesImpl.java
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
... 略去无关 ...
mcr.mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
synchronized (this) {
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) {
mMap.remove(k);
} else {
mMap.put(k, v);
}
}
mModified.clear();
}
}
return mcr;
}
总结来说就两件事:
1. 把 Editor.mModified
中的配置项回写到 SharedPreferences.mMap
中, 完成了内存的同步
2. 把 SharedPreferences.mMap
保存在了 mcr.mapToWriteToDisk
中. 而后者就是即将要回写到磁盘的数据源
我们再来回头看看 apply
方法:
// SharedPreferencesImpl.java
public void apply() {
final MemoryCommitResult mcr = commitToMemory();
... 略无关 ...
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}
commitToMemory
完成了内存的同步回写enqueueDiskWrite
完成了硬盘的异步回写, 我们接下来具体看看enqueueDiskWrite
// SharedPreferencesImpl.java
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
...
}
};
...
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
QueuedWork.singleThreadExecutor
实际上就是 ‘一个线程的线程池’, 如下:
// QueuedWork.java
public static ExecutorService singleThreadExecutor() {
synchronized (QueuedWork.class) {
if (sSingleThreadExecutor == null) {
// TODO: can we give this single thread a thread name?
sSingleThreadExecutor = Executors.newSingleThreadExecutor();
}
return sSingleThreadExecutor;
}
}
可以看出 google 工程师这里还留了一个 todo, 想要给这个专门负责向磁盘同步配置项的线程起一个名字. 这里坐等大神你给 google 提个 pr
回到 enqueueDiskWrite
中, 这里还有一个重要的函数叫做 writeToFile
:
writeToFile
// SharedPreferencesImpl.java
private void writeToFile(MemoryCommitResult mcr) {
// Rename the current file so it may be used as a backup during the next read
if (mFile.exists()) {
if (!mBackupFile.exists()) {
if (!mFile.renameTo(mBackupFile)) {
return;
}
} else {
mFile.delete();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
try {
final StructStat stat = Os.stat(mFile.getPath());
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
return;
}
// Clean up an unsuccessfully written file
mFile.delete();
}
代码大致分为三个过程:
1. 先把已存在的老的配置文件重命名(加 ‘.bak’ 后缀), 然后删除老的配置文件. 这相当于做了灾备
2. 向 mFile
中一次性写入所有配置项. 即 mcr.mapToWriteToDisk
(这就是 commitToMemory
所说的保存了所有配置项的字段) 一次性写入到磁盘. 如果写入成功则删除灾备文件, 同时记录了这次同步的时间
3. 如果上述过程 [2] 失败, 则删除这个半成品的配置文件
好了, 我们来总结一下 apply
:
1. 通过 commitToMemory
将修改的配置项同步回写到内存 SharedPreferences.mMap
中. 此时, 任何的 getXxx 都可以获取到最新数据了
2. 通过 enqueueDiskWrite
调用 writeToFile
将所有配置项一次性异步回写到磁盘. 这是一个单线程的线程池
来看个时序图压压惊(点击大图):
commit
看过了 apply
再看 commit
就非常容易了.
// SharedPreferencesImpl.java
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;
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
直接看时序图吧(点击大图):
只需关注最后一条 ‘等待异步任务返回’ 的线, 对比 apply
的时序图, 一眼就看出差别
registerOnSharedPreferenceChangeListener
最后需要提一下的就是 listener:
* 对于 apply
, listener 回调时内存已经完成同步, 但是异步磁盘任务不保证是否完成
* 对于 commit
, listener 回调时内存和磁盘都已经同步完毕
指的是, 在保存文件的时候设置的文件属性. PRIVATE 就只有自己和所属组的读写权限, READABLE/WRITEABLE 是对 other 用户和组的读写权限. 主要源码位于: FileUtils.setPermissions
阅读过本文的话你会知道, 一个 prefs 实例通常有两种获得途径, 一个是第一次被 new 创建出来的, 这种方式会实际的读取磁盘文件. 还一种是后续从缓存(sSharedPrefsCache) 中取出来了.
而这个标记的意思就是: 使用 getSharedPrefercences 获取实例时, 无论是从磁盘读文件构造对象还是从缓存获取, 都会检查实例的 ‘内存中保存的时间’ 和 ‘磁盘上文件的最后修改时间’, 如果内存中保存的时间和磁盘上文件的最后修改时间, 则重新加载文件. 可以认为如果实例是从磁盘读取构造出来的, 那么他的 ‘内存中保存的时间’ 和 ‘文件的最后修改时间’ 一定是一样的, 而从缓存中来的实例就不一样了, 因为它可能很早就被创建(那个时候就已经读取了磁盘的文件并记录了当时文件的最后修改时间), 在随后的期间里其他进程很可能修改过磁盘上的配置文件导致最后修改时间变化, 这时候当我们从缓存中再次获取这个实例的时候, 系统会帮你检查这个文件在这段时间是否被修改过(‘内存中保存的时间’ 和 ‘磁盘上文件的最后修改时间’ 是否一致), 如果被修改过, 则重新从磁盘读取配置文件, 保证获取实例的内容是最新的.
SharedPreferences
应该是任何一名 Android 初学者都知道的存储类了,它轻量,适合用于保存软件配置等参数。以键值对的 XML 文件形式存储在本地,程序卸载后也会一并清除,不会残留信息。
使用起来也非常简单。
// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
// 写入
val editor = sharedPreferences.edit()
editor.putString("123","123")
editor.commit()
当我们写下这样的代码的时候,IDE 极易出现一个警告,提示我们用 apply()
来替换 commit()
。原因也很简单,因为 commit()
是同步的,而 apply()
采用异步的方式通常来说效率会更高一些。但是,当我们把 editor.commit()
的返回值赋给一个变量的时候,这时候就会发现 IDE 没有了警告。这是因为 IDE 认为我们想要使用 editor.commit()
的返回值了,所以,通常来说,在我们不关心操作结果的时候,我们更倾向于使用 apply()
进行写入的操作。
我们可以通过 3 种方式来获取 SharedPreferences
的实例。
首先当然是我们最常见的写法。
getSharedPreferences("123", Context.MODE_PRIVATE)
Context
的任意子类都可以直接通过 getSharedPreferences()
方法获取到 SharedPreferences
的实例,接受两个参数,分别对应 XML 文件的名字和操作模式。其中 MODE_WORLD_READABLE
和 MODE_WORLD_WRITEABLE
这两种模式已在 Android 4.2 版本中被废弃。
SharedPreferences
数据只能被本应用程序读、写;SharedPreferences
数据能被其他应用程序读,但不能写;SharedPreferences
数据能被其他应用程序读写;另外在 Activity
的实现中,还可以直接通过 getPreferences()
获取,实际上也就把当前 Activity 的类名作为文件名参数。
public SharedPreferences getPreferences(@Context.PreferencesMode int mode) {
return getSharedPreferences(getLocalClassName(), mode);
}
此外,我们也可以通过 PreferenceManager
的 getDefaultSharedPreferences()
获取到。
public static SharedPreferences getDefaultSharedPreferences(Context context) {
return context.getSharedPreferences(getDefaultSharedPreferencesName(context),
getDefaultSharedPreferencesMode());
}
public static String getDefaultSharedPreferencesName(Context context) {
return context.getPackageName() + "_preferences";
}
private static int getDefaultSharedPreferencesMode() {
return Context.MODE_PRIVATE;
}
可以很明显的看到,这个方式就是在直接把当前应用的包名作为前缀来进行命名的。
注意:如果在 Fragment 中使用
SharedPreferences
时,SharedPreferences
的初始化尽量放在onAttach(Activity activity)
里面进行 ,否则可能会报空指针,即getActivity()
会可能返回为空。
有较多 SharedPreferences
使用经验的人,就会发现 SharedPreferences
其实具备挺多的坑,但这些坑主要都是因为不熟悉其中真正的原理所导致的,所以,笔者在这里,带大家一起揭开 SharedPreferences
的神秘面纱。
SharedPreferences 实例获取
前面讲了 SharedPreferences
有三种获取实例的方法,但归根结底都是调用的 Context
的 getSharedPreferences()
方法。由于 Android 的 Context
类采用的是装饰者模式,而装饰者对象其实就是 ContextImpl
,所以我们来看看源码是怎么实现的。
// 存放的是名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name
private ArrayMap mSharedPrefsPaths;
public SharedPreferences getSharedPreferences(String name, int mode) {
// At least one application in the world actually passes in a null
// name. This happened to work because when we generated the file name
// we would stringify it to "null.xml". Nice.
if (mPackageInfo.getApplicationInfo().targetSdkVersion <
Build.VERSION_CODES.KITKAT) {
if (name == null) {
name = "null";
}
}
File file;
synchronized (ContextImpl.class) {
if (mSharedPrefsPaths == null) {
mSharedPrefsPaths = new ArrayMap<>();
}
file = mSharedPrefsPaths.get(name);
if (file == null) {
file = getSharedPreferencesPath(name);
mSharedPrefsPaths.put(name, file);
}
}
return getSharedPreferences(file, mode);
}
@Override
public File getSharedPreferencesPath(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File makeFilename(File base, String name) {
if (name.indexOf(File.separatorChar) < 0) {
return new File(base, name);
}
throw new IllegalArgumentException(
"File " + name + " contains a path separator");
}
可以很明显的看到,内部是采用 ArrayMap
来做的处理,而这个 mSharedPrefsPaths
主要是用于存放名称和文件夹的映射,实际上这个名称就是我们外面传进来的 name,这时候我们通过 name 拿到我们的 File,如果当前池子中没有的话,则直接新建一个 File,并放入到 mSharedPrefsPaths
中。最后还是调用的重载方法 getSharedPreferences(File,mode)
// 存放包名与ArrayMap键值对,初始化时会默认以包名作为键值对中的 Key,注意这是个 static 变量
private static ArrayMap> sSharedPrefsCache;
@Override
public SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
final ArrayMap cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
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");
}
}
sp = new SharedPreferencesImpl(file, mode);
cache.put(file, sp);
return 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;
}
private ArrayMap getSharedPreferencesCacheLocked() {
if (sSharedPrefsCache == null) {
sSharedPrefsCache = new ArrayMap<>();
}
final String packageName = getPackageName();
ArrayMap packagePrefs = sSharedPrefsCache.get(packageName);
if (packagePrefs == null) {
packagePrefs = new ArrayMap<>();
sSharedPrefsCache.put(packageName, packagePrefs);
}
return packagePrefs;
}
可以看到,又采用了一个 ArrayMap
来存放文件和 SharedPreferencesImpl
组成的键值对,然后通过通过单例的方式返回一个 SharedPreferences
对象,实际上是 SharedPreferences
的实现类 SharedPreferencesImpl
,而且在其中还建立了一个内部缓存机制。
所以,从上面的分析中,我们能知道 对于一个相同的 name,我们获取到的都是同一个 SharedPreferencesImpl 对象。
SharedPreferencesImpl
在上面的操作中,我们可以看到在第一次调用 getSharedPreferences
的时候,我们会去构造一个 SharedPreferencesImpl
对象,我们来看看都做了什么。
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
mThrowable = null;
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()) {
mFile.delete();
mBackupFile.renameTo(mFile);
}
}
// Debugging
if (mFile.exists() && !mFile.canRead()) {
Log.w(TAG, "Attempt to read preferences file " + mFile + " without permission");
}
Map map = null;
StructStat stat = null;
Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
new FileInputStream(mFile), 16 * 1024);
map = (Map) 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) {
mLoaded = true;
mThrowable = thrown;
// It's important that we always signal waiters, even if we'll make
// them fail with an exception. The try-finally is pretty wide, but
// better safe than sorry.
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();
}
}
}
注意看我们的 startLoadFromDisk
方法,我们会去新开一个子线程,然后去通过 XmlUtils.readMapXml()
方法把指定的 SharedPreferences
文件的所有的键值对都读出来,然后存放到一个 map 中。
而众所周知,文件的读写操作都是耗时的,可想而知,在我们第一次去读取一个 SharedPreferences
文件的时候花上了太多的时间会怎样。
SharedPreferences 的读取操作
上面讲了初次获取一个文件的 SharedPreferences
实例的时候,会先去把所有键值对读取到缓存中,这明显是一个耗时操作,而我们正常的去读取数据的时候,都是类似这样的代码。
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
val string = sharedPreferences.getString("123","")
SharedPreferences
的getXXX()
方法可能会报ClassCastException
异常,所以我们在同一个 name 的时候,对不一样的类型,必须使用不同的 key。但是putXXX
是可以用不同的类型值覆盖相同的 key 的。
那势必可能会导致这个操作需要等待一定的时间,我们姑且可以这么猜想,在 getXXX()
方法执行的时候应该是会等待前面的操作完成才能执行的。
因为 SharedPreferences
是一个接口,所以我们主要来看看它的实现类 SharedPreferencesImpl
,这里以 getString()
为例。
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
awaitLoadedLocked()
方法应该就是我们所想的等待执行操作了,我们看看里面做了什么。
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);
}
}
可以看到,在 awaitLoadedLocked
方法里面我们使用了 mLock.wait()
来等待初始化的读取操作,而我们前面看到的 loadFromDiskLocked()
方法的最后也可以看到它调用了 mLock.notifyAll()
方法来唤醒后面这个阻塞的 getXXX()
。那么这里就会明显出现一个问题,我们的 getXXX()
方法是写在 UI 线程的,如果这个方法被阻塞的太久,势必会出现 ANR 的情况。所以我们一定在平时需要根据具体情况考虑是否需要把 SharedPreferences
的读写操作放在子线程中。
SharedPreferences 的内部类 Editor
我们在写入数据之前,总是要先通过类似这样的代码获取 SharedPreferences
的内部类 Editor
。
val editor = sharedPreferences.edit()
我们当然要看看这个到底是什么东西。
@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();
}
return new EditorImpl();
}
我们在
可以看到,我们在读取解析完 XML 文件的时候,直接返回了一个 Editor
的实现类 EditorImpl
。我们随便查看一个 putXXX 的方法一看。
private final Object mEditorLock = new Object();
@GuardedBy("mEditorLock")
private final Map mModified = new HashMap<>();
@GuardedBy("mEditorLock")
private boolean mClear = false;
@Override
public Editor putString(String key, @Nullable String value) {
synchronized (mEditorLock) {
mModified.put(key, value);
return this;
}
}
可以看到,我们在 EditorImpl
里面使用了一个 HashMap
来存放我们的键值对数据,每次 put 的时候都会直接往这个键值对变量 mModified
中进行数据的 put 操作。
commit() 和 apply()
我们总是在更新数据后需要加上 commit()
或者 apply()
来进行输入的写入操作,我们不妨来看看他们的实现到底有什么区别。
先看 commit() 和 apply() 的源码。
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
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");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
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);
}
可以看到,apply()
和 commit()
的区别是在 commit()
把内容同步提交到了硬盘,而 apply()
是先立即把修改提交给了内存,然后开启了一个异步的线程提交到硬盘。commit()
会接收 MemoryCommitResult
里面的一个 boolean
参数作为结果,而 apply()
没有对结果做任何关心。
我们可以看到,文件写入更新的操作都是交给 commitToMemory()
做的,这个方法返回了一个 MemoryCommitResult
对象,我们来看看到底做了什么。
// Returns true if any changes were made
private MemoryCommitResult commitToMemory() {
long memoryStateGeneration;
List keysModified = null;
Set listeners = null;
Map mapToWriteToDisk;
synchronized (SharedPreferencesImpl.this.mLock) {
// We optimistically don't make a deep copy until
// a memory commit comes in when we're already
// writing to disk.
if (mDiskWritesInFlight > 0) {
// We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
mMap = new HashMap(mMap);
}
mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList();
listeners = new HashSet(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
if (mClear) {
if (!mapToWriteToDisk.isEmpty()) {
changesMade = true;
mapToWriteToDisk.clear();
}
mClear = false;
}
for (Map.Entry 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 (!mapToWriteToDisk.containsKey(k)) {
continue;
}
mapToWriteToDisk.remove(k);
} else {
if (mapToWriteToDisk.containsKey(k)) {
Object existingValue = mapToWriteToDisk.get(k);
if (existingValue != null && existingValue.equals(v)) {
continue;
}
}
mapToWriteToDisk.put(k, v);
}
changesMade = true;
if (hasListeners) {
keysModified.add(k);
}
}
mModified.clear();
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
可以看到,我们这里的 mMap
即存放当前 SharedPreferences
文件中的键值对,而 mModified
则存放的是当时 edit()
时 put 进去的键值对,这个我们前面有所介绍。这里有个 mDiskWritesInFlight
看起来应该是表示正在等待写的操作数量。
接下来我们首先处理了 edit().clear()
操作的 mClear
标志,当我们在外面调用 clear()
方法的时候,我们会把 mClear
设置为 true,这时候我们会直接通过 mMap.clear()
清空此时文件中的键值对,然后再遍历 mModified
中新 put 进来的键值对数据放到 mMap
中。也就是说:在一次提交中,如果我们又有 put 又有 clear()
操作的话,我们只能 clear()
掉之前的键值对,这次 put()
进去的键值对还是会被写入到 XML 文件中。
// 读取
val sharedPreferences = getSharedPreferences("123", Context.MODE_PRIVATE)
// 写入
val editor = sharedPreferences.edit()
editor.putInt("1", 123)
editor.clear()
editor.apply()
Log.e("nanchen2251", "${sharedPreferences.getInt("1", 0)}")
也就是说,当我们编写下面的代码的时候,得到的打印还是 123。
然后我们接着往下看,又发现了另外一个 commit()
和 apply()
都做了调用的方法是 enqueueDiskWrite()
。
/**
* Enqueue an already-committed-to-memory result to be written
* to disk.
*
* They will be written to disk one-at-a-time in the order
* that they're enqueued.
*
* @param postWriteRunnable if non-null, we're being called
* from apply() and this is the runnable to run after
* the write proceeds. if null (from a regular commit()),
* then we're allowed to do this disk write on the main
* thread (which in addition to reducing allocations and
* creating a background thread, this has the advantage that
* we catch them in userdebug StrictMode reports to convert
* them where possible to apply() ...)
*/
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
在这个方法中,首先通过判断 postWriteRunnable
是否为 null 来判断是 apply()
还是 commit()
。然后定义了一个 Runnable
任务,在 Runnable
中先调用了 writeToFile()
进行了写入和计数器更新的操作。
然后我们再来看看这个 writeToFile()
方法做了些什么。
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
long startTime = 0;
long existsTime = 0;
long backupExistsTime = 0;
long outputStreamCreateTime = 0;
long writeTime = 0;
long fsyncTime = 0;
long setPermTime = 0;
long fstatTime = 0;
long deleteTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
boolean fileExists = mFile.exists();
if (DEBUG) {
existsTime = System.currentTimeMillis();
// Might not be set, hence init them to a default value
backupExistsTime = existsTime;
}
// Rename the current file so it may be used as a backup during the next read
if (fileExists) {
boolean needsWrite = false;
// Only need to write if the disk state is older than this commit
if (mDiskStateGeneration < mcr.memoryStateGeneration) {
if (isFromSyncCommit) {
needsWrite = true;
} else {
synchronized (mLock) {
// No need to persist intermediate states. Just wait for the latest state to
// be persisted.
if (mCurrentMemoryStateGeneration == mcr.memoryStateGeneration) {
needsWrite = true;
}
}
}
}
if (!needsWrite) {
mcr.setDiskWriteResult(false, true);
return;
}
boolean backupFileExists = mBackupFile.exists();
if (DEBUG) {
backupExistsTime = System.currentTimeMillis();
}
// 此处需要注意一下
if (!backupFileExists) {
if (!mFile.renameTo(mBackupFile)) {
Log.e(TAG, "Couldn't rename file " + mFile
+ " to backup file " + mBackupFile);
mcr.setDiskWriteResult(false, false);
return;
}
} else {
mFile.delete();
}
}
// Attempt to write the file, delete the backup and return true as atomically as
// possible. If any exception occurs, delete the new file; next time we will restore
// from the backup.
try {
FileOutputStream str = createFileOutputStream(mFile);
if (DEBUG) {
outputStreamCreateTime = System.currentTimeMillis();
}
if (str == null) {
mcr.setDiskWriteResult(false, false);
return;
}
XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
writeTime = System.currentTimeMillis();
FileUtils.sync(str);
fsyncTime = System.currentTimeMillis();
str.close();
ContextImpl.setFilePermissionsFromMode(mFile.getPath(), mMode, 0);
if (DEBUG) {
setPermTime = System.currentTimeMillis();
}
try {
final StructStat stat = Os.stat(mFile.getPath());
synchronized (mLock) {
mStatTimestamp = stat.st_mtim;
mStatSize = stat.st_size;
}
} catch (ErrnoException e) {
// Do nothing
}
if (DEBUG) {
fstatTime = System.currentTimeMillis();
}
// Writing was successful, delete the backup file if there is one.
mBackupFile.delete();
if (DEBUG) {
deleteTime = System.currentTimeMillis();
}
mDiskStateGeneration = mcr.memoryStateGeneration;
mcr.setDiskWriteResult(true, true);
if (DEBUG) {
Log.d(TAG, "write: " + (existsTime - startTime) + "/"
+ (backupExistsTime - startTime) + "/"
+ (outputStreamCreateTime - startTime) + "/"
+ (writeTime - startTime) + "/"
+ (fsyncTime - startTime) + "/"
+ (setPermTime - startTime) + "/"
+ (fstatTime - startTime) + "/"
+ (deleteTime - startTime));
}
long fsyncDuration = fsyncTime - writeTime;
mSyncTimes.add((int) fsyncDuration);
mNumSync++;
if (DEBUG || mNumSync % 1024 == 0 || fsyncDuration > MAX_FSYNC_DURATION_MILLIS) {
mSyncTimes.log(TAG, "Time required to fsync " + mFile + ": ");
}
return;
} catch (XmlPullParserException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
} catch (IOException e) {
Log.w(TAG, "writeToFile: Got exception:", e);
}
// Clean up an unsuccessfully written file
if (mFile.exists()) {
if (!mFile.delete()) {
Log.e(TAG, "Couldn't clean up partially-written file " + mFile);
}
}
mcr.setDiskWriteResult(false, false);
}
代码比较长,做了一些时间的记录和 XML 的相关处理,但最值得我们关注的还是其中打了标注的对于 mBackupFile
的处理。我们可以明显地看到,在我们写入文件的时候,我们会把此前的 XML 文件改名为一个备份文件,然后再将要写入的数据写入到一个新的文件中。如果这个过程执行成功的话,就会把备份文件删除。由此可见:即使我们每次只是添加一个键值对,也会重新写入整个文件的数据,这也说明了 SharedPreferences 只适合保存少量数据,文件太大会有性能问题。
看完了这个 writeToFile()
,我们再来看看下面做了啥。
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
可以看到,当且仅当是 commit()
并且只有一个待写入操作的时候才能直接执行到 writeToDiskRunnable.run()
,否则都会执行到 QueuedWork
的 queue()
方法,这个 QueuedWork
又是什么东西?
/** Finishers {@link #addFinisher added} and not yet {@link #removeFinisher removed} */
@GuardedBy("sLock")
private static final LinkedList sFinishers = new LinkedList<>();
/** Work queued via {@link #queue} */
@GuardedBy("sLock")
private static final LinkedList sWork = new LinkedList<>();
/**
* Internal utility class to keep track of process-global work that's outstanding and hasn't been
* finished yet.
*
* New work will be {@link #queue queued}.
*
* It is possible to add 'finisher'-runnables that are {@link #waitToFinish guaranteed to be run}.
* This is used to make sure the work has been finished.
*
* This was created for writing SharedPreference edits out asynchronously so we'd have a mechanism
* to wait for the writes in Activity.onPause and similar places, but we may use this mechanism for
* other things in the future.
*
* The queued asynchronous work is performed on a separate, dedicated thread.
*
* @hide
*/
public class QueuedWork {
/**
* Add a finisher-runnable to wait for {@link #queue asynchronously processed work}.
*
* Used by SharedPreferences$Editor#startCommit().
*
* Note that this doesn't actually start it running. This is just a scratch set for callers
* doing async work to keep updated with what's in-flight. In the common case, caller code
* (e.g. SharedPreferences) will pretty quickly call remove() after an add(). The only time
* these Runnables are run is from {@link #waitToFinish}.
*
* @param finisher The runnable to add as finisher
*/
public static void addFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.add(finisher);
}
}
/**
* Remove a previously {@link #addFinisher added} finisher-runnable.
*
* @param finisher The runnable to remove.
*/
public static void removeFinisher(Runnable finisher) {
synchronized (sLock) {
sFinishers.remove(finisher);
}
}
/**
* Trigger queued work to be processed immediately. The queued work is processed on a separate
* thread asynchronous. While doing that run and process all finishers on this thread. The
* finishers can be implemented in a way to check weather the queued work is finished.
*
* Is called from the Activity base class's onPause(), after BroadcastReceiver's onReceive,
* after Service command handling, etc. (so async work is never lost)
*/
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
// We should not delay any work as this might delay the finishers
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
/**
* Queue a work-runnable for processing asynchronously.
*
* @param work The new runnable to process
* @param shouldDelay If the message should be delayed
*/
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
}
简单地说,这个 QueuedWork
类里面有一个专门存放 Runnable
的两个 LinkedList
对象,他们分别对应未完成的操作 sFinishers
和正在工作的 sWork
。
我们在 waitToFinish()
方法中,会不断地去遍历执行未完成的 Runnable
。我们根据注释也知道了这个方法会在 Activity
的 onPause()
和 BroadcastReceiver
的 onReceive()
方法后调用。假设我们频繁的调用了 apply()
方法,并紧接着调用了 onPause()
,那么就可能会发生 onPause()
一直等待 QueuedWork.waitToFinish
执行完成而产生 ANR。也就是说,即使是调用了 apply()
方法去异步提交,也不是完全安全的。如果 apply()
方法使用不当,也是可能出现 ANR 的。
说了这么多,我们当然还是需要做一个总结。
apply()
没有返回值而 commit()
返回 boolean
表明修改是否提交成功 ;commit()
是把内容同步提交到硬盘的,而 apply()
先立即把修改提交到内存,然后开启一个异步的线程提交到硬盘,并且如果提交失败,你不会收到任何通知。commit()
提交是同步过程,效率会比 apply()
异步提交的速度慢,在不关心提交结果是否成功的情况下,优先考虑 apply()
方法。apply()
是使用异步线程写入磁盘,commit()
是同步写入磁盘。所以我们在主线程使用的 commit()
的时候,需要考虑是否会出现 ANR 问题。SharedPreferencesImpl
里面的对象,互斥其他操作,而当 put
、commit()
和 apply()
操作的时候都会锁住 Editor
的对象,在这样的情况下,效率会降低。SharedPreferences
,也不要把较多数据存储到同一个 name 对应的 SharedPreferences
中,最好根据规则拆分为多个 SharedPreferences
文件。SharedPreferences
。SharedPreferences
对象的时候会读取 SharedPreferences
文件,如果文件没有读取完,就执行了 get 和 put 操作,可能会出现需要等待的情况,因此最好提前获取 SharedPreferences
对象。edit()
方法都会创建一个新的 EditorImpl
对象,不要频繁调用 edit()
方法。
最近在排查app卡顿问题,在公司内部的bug管理平台上发现这个类卡顿问题,知道卡顿了多长时间吗,足足4s多,这让线上用户怎么想?让我怎么想?
java.lang.Object.wait(Native Method)
java.lang.Thread.parkFor(Thread.java:1220)
sun.misc.Unsafe.park(Unsafe.java:299)
java.util.concurrent.locks.LockSupport.park(LockSupport.java:157)
java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:813)
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(AbstractQueuedSynchronizer.java:973)
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(AbstractQueuedSynchronizer.java:1281)
java.util.concurrent.CountDownLatch.await(CountDownLatch.java:202)
android.app.SharedPreferencesImpl$EditorImpl$1.run(SharedPreferencesImpl.java:363)
android.app.QueuedWork.waitToFinish(QueuedWork.java:88)
android.app.ActivityThread.handleServiceArgs(ActivityThread.java:3336)
android.app.ActivityThread.access$2300(ActivityThread.java:197)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1709)
android.os.Handler.dispatchMessage(Handler.java:111)
android.os.Looper.loop(Looper.java:224)
android.app.ActivityThread.main(ActivityThread.java:5958)
java.lang.reflect.Method.invoke(Native Method)
java.lang.reflect.Method.invoke(Method.java:372)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1113)
刚开始以为是系统Unsafe的卡顿,就没怎么细看,后来发现不对中间居然看见了SharedPreferences的代码,之前就知道SharedPreferences这个玩意坑很多,我又回忆起之前面试一个面试者,他提到过如何采用objectbox替换SharedPreferences解决卡顿问题,我怀疑就是这玩意导致的,这激起了我的好奇心
项目中用了SharedPreferences 这个玩意,谁知道这额玩意有大坑呀,给我们app卡的不行不行的,代码在apply的时候,SharedPreferences 内部发送了一个异步任务取执行文件的写操作,按道理说写操作都是在异步线程中执行的,不应该会卡顿主线程呀,是的,读写操作时在异步线程,QueuedWork.waitToFinish 这个方法是在主线程中执行,具体的调用到代码在ActiviytThread 类的handleStopActivity 方法和handleServiceArgs 方法中等多处方法中有调用,我们出问题的地方就是调用了handleServiceArgs方法,QueuedWork.waitToFinish 这个方法中执行了线程操作,所以导致了主线程卡住了
我们先看一下SharedPreferencesImpl 这个类,这个类是具体的实现类,我们看一下commit方法
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
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");
}
}
notifyListeners(mcr);
return mcr.writeToDiskResult;
}
在commit方法中,首先执行写入任务也就是enqueueDiskWrite这个方法,我们稍后分析,然后让调用线程处于等待状态,当写入任务执行成功后唤起调用commit的线程,假设调用commit的线程就是主线线程,并且写入任务耗时还比较多的,这不就阻塞住主线程了吗?
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
final boolean isFromSyncCommit = (postWriteRunnable == null);
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//往系统的队列中发送任务,然后在工作线程中执行任务
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
enqueueDiskWrite 方法中首先判断的postWriteRunnable 是否等于null,如果等于空了,就在当前调用的地方执行写入操作,如果不是就往QueuedWork 队列中发送任务
总结一下:如果是使用commit方式提交,会阻塞调用commit方法的线程,如果写入任务很多比较耗时,就卡住了,所以不要在主线程执行写入文件的操作,但是我们上线卡顿日志是另外一种情况,是使用了apply提交的时候才会出现的
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
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");
}
}
};
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
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);
}
enqueueDiskWrite是执行异步任务的方法,我们之前已经见过这个方法,在apply方法中调用enqueueDiskWrite方法的时候最后一个参数是不等于空的,也就是说我们要执行一个异步任务,最终这异步任务的执行是在QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)方法中
QueuedWork就是android系统提供的一个执行异步任务的工具类,内部的实现逻辑的就是创建一个HandlerThread作为工作线程,然后QueuedWorkHandler和这个HandlerThread进行管理,每当有任务添加进来就在这个异步线程中执行,这个异步线程的名字queued-work-looper
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else {
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
首先往sWork 添加一个任务,sWork是一个LinkedList,这个队列中数据最终在queued-work-looper 线程中依次得到执行
创建handle的过程
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;
}
}
在QueuedWorkHandler 是如何处理消息的
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() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
synchronized (sProcessingWork) {
LinkedList work;
synchronized (sLock) {
work = (LinkedList) sWork.clone();
sWork.clear();
// Remove all msg-s as all work will be processed now
getHandler().removeMessages(QueuedWorkHandler.MSG_RUN);
}
if (work.size() > 0) {
for (Runnable w : work) {
w.run();
}
if (DEBUG) {
Log.d(LOG_TAG, "processing " + work.size() + " items took " +
+(System.currentTimeMillis() - startTime) + " ms");
}
}
}
}
实际上就是遍历sWork,挨个执行任务,
apply的中写入操作也是在异步线程执行,不会导致主线程卡顿,但是如果异步任务执行时间过长,当ActvityThread执行了handleStopActivity或者handleServiceArgs或者handlePauseActivity 等方法的时候都会调用QueuedWork.waitToFinish()方法,而此方法中会在异步任务执行完成前一直阻塞住主线程,所以卡顿问题就产生了
public static void waitToFinish() {
long startTime = System.currentTimeMillis();
boolean hadMessages = false;
Handler handler = getHandler();
synchronized (sLock) {
if (handler.hasMessages(QueuedWorkHandler.MSG_RUN)) {
// Delayed work will be processed at processPendingWork() below
handler.removeMessages(QueuedWorkHandler.MSG_RUN);
if (DEBUG) {
hadMessages = true;
Log.d(LOG_TAG, "waiting");
}
}
// We should not delay any work as this might delay the finishers
sCanDelay = false;
}
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskWrites();
try {
processPendingWork();
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
try {
while (true) {
Runnable finisher;
synchronized (sLock) {
//关键代码
finisher = sFinishers.poll();
}
if (finisher == null) {
break;
}
finisher.run();
}
} finally {
sCanDelay = true;
}
synchronized (sLock) {
long waitTime = System.currentTimeMillis() - startTime;
if (waitTime > 0 || hadMessages) {
mWaitTimes.add(Long.valueOf(waitTime).intValue());
mNumWaits++;
if (DEBUG || mNumWaits % 1024 == 0 || waitTime > MAX_WAIT_TIME_MILLIS) {
mWaitTimes.log(LOG_TAG, "waited: ");
}
}
}
}
会从sFinishers队列中取出数据然后执行run方法,我们别忘了在apply的方法中,我们还添加了QueuedWork.addFinisher(awaitCommit);这个awaitCommit 就得到执行了但是awaitCommit中的代码确实是阻塞的代码,等待写入线程执行完毕才能唤起此线程
final Runnable awaitCommit = new Runnable() {
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");
}
}
};
如果 apply中的写入代码不执行完,主线程就一直卡住了,也就出现了我们上面的问题
我们看一下SharedPreferences 的初始化代码
SharedPreferencesImpl(File file, int mode) {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
mLoaded = false;
mMap = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk();
}
}.start();
}
看一下获取数据的get方法
public int getInt(String key, int defValue) {
synchronized (mLock) {
awaitLoadedLocked();
Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
关键awaitLoadedLocked 这个方法,当数据没有加载完,就让调用的线程处于等待中,阻塞住了
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) {
}
}
}
SharePrefereces这玩意能不用就不用,坑还是比较隐晦的,如果不查看源代码是根本不可能知道的,一般大家用这个类主要是图简单省事 了,我就是存一个数据搞那么复杂干什么呢?我建议如果大家在线上的项目中存储数据还是用自己实现的方案吧,不要用这个了,如果非要用这个我建议开启一个线程然后在线程中调用commit方式更新数据,至少这个方案不会卡住主线程
链接:https://www.jianshu.com/p/40e42da910e2