这是别人在面试过程中遇到的一个问题, SharedPreferences的apply()
为什么没有返回结果.根据官方文档我们已经知道commit()
是有返回结果的, apply()
是没有返回结果的, 且官方推荐使用apply()
,这个结论好像谁都知道. 为什么要这么设计呢?直接看源码
加载SP
@Override
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 SharedPreferences getSharedPreferences(File file, int mode) {
SharedPreferencesImpl sp;
synchronized (ContextImpl.class) {
//获取当前包名对应的所有SP
final ArrayMap cache = getSharedPreferencesCacheLocked();
sp = cache.get(file);
if (sp == null) {
checkMode(mode);
//Android O在解锁前不能访问应用内部存储
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
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;
}
getSharedPreferencesCacheLocked()
方法使用ArrayMap保存当前包名对应的所有SP, sSharedPrefsCache
实例是static修饰的,一直存在于内存中
/**
* Map from package name, to preference name, to cached preferences.
*/
@GuardedBy("ContextImpl.class")
private static ArrayMap> sSharedPrefsCache;
@GuardedBy("ContextImpl.class")
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;
}
SP的构造方法里,调用startLoadFromDisk()
方法,可以看到开了一个子线程去加载
SharedPreferencesImpl(File file, int mode) {
mFile = file; //SP文件路径
mBackupFile = makeBackupFile(file); //备份SP文件
mMode = mode;
mLoaded = false; //是否加载完成
mMap = null; //存放数据的Map
startLoadFromDisk();
}
private void startLoadFromDisk() {
synchronized (mLock) {
mLoaded = false;
}
new Thread("SharedPreferencesImpl-load") {
public void run() {
loadFromDisk(); //新开了一个子线程去加载磁盘上的SP
}
}.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;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
//读取SP的xml文件,以的形式保存到map里
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; //到这里加载完成了
if (map != null) {
mMap = map; //保存从SP中读取的map集合
mStatTimestamp = stat.st_mtime;
mStatSize = stat.st_size;
} else {
mMap = new HashMap<>();
}
//这是用到notifyAll(), 肯定是和wait()成对出现的
mLock.notifyAll();
}
}
这里使用了notifyAll()
, 他和wait()
肯定是成对出现的
查看源码,在SP的写入操作edit()
, putXXX()
和读取操作getXXX()
时,都调用了awaitLoadedLocked()
方法, 里面调用了wait()
方法阻塞并释放了锁,等待子线程的loadFromDisk()
完成后再继续自己的读写操作
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.
//这里没看懂,loadFromDisk()已经在子线程中了,还需要BlockGuard()检测??
BlockGuard.getThreadPolicy().onReadFromDisk();
}
while (!mLoaded) { //如果load没有完成,将会阻塞在这里
try {
mLock.wait(); //释放了mLock锁,等待notifyAll()将唤醒
} catch (InterruptedException unused) {
}
}
}
读取
以getString()
为例,mMap
就是上一步从SP的xml解析出来的,这里要注意了, mMap
并非是实时磁盘上的结果. 这里要解释一下
SP写入的操作分为两步: 1. 写入内存 2. 写入磁盘
所以当你commit()
或者apply()
提交的时候, 在写入内存后, mMap
就会发生改变,这时候你就可以通过getXXX
来获取你要的结果, 而不用等待写入磁盘完成(写入磁盘是IO操作,在子线程完成的)
其实很好解释,在应用存活期间直接可以读取内存里的数据,并不需要等待写入磁盘完成. 在应用重启后,把持久化到磁盘里的内容写入内存,继续操作内存里的数据.
@Nullable
public String getString(String key, @Nullable String defValue) {
synchronized (mLock) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
写入
写入分为commit
和apply
commit
@Override
public boolean commit() {
long startTime = 0;
if (DEBUG) {
startTime = System.currentTimeMillis();
}
//1.提交到内存
MemoryCommitResult mcr = commitToMemory();
//2.加入写磁盘的任务队列;(这里有句注释,在当前线程同步写OK,等会看怎么实现的)
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是否成功,apply是没有这一句的
}
-
commitToMemory()
提交到内存
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); //进行深拷贝,为啥是深拷贝,因为SP保存的是基本数据类型和String
}
mapToWriteToDisk = mMap; //保存map的拷贝,可以理解为磁盘快照
mDiskWritesInFlight++; //写磁盘的任务+1
boolean hasListeners = mListeners.size() > 0;
if (hasListeners) {
keysModified = new ArrayList();
listeners = new HashSet(mListeners.keySet());
}
synchronized (mEditorLock) {
boolean changesMade = false;
//如果调用了clear方法,就清空这个硬盘快照
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.
// 调用remove方法里传的mModified.put(key, this); 或者put方法里传的null值,都视作删除的操作
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); //覆盖原值
}
//修改完成标记 = true
changesMade = true;
if (hasListeners) {
keysModified.add(k); //保存修改了哪些key的值
}
}
//清空待修改内容
mModified.clear();
//修改一次计数器就+1,这里用的是long类型,防止次数过多越界
if (changesMade) {
mCurrentMemoryStateGeneration++;
}
memoryStateGeneration = mCurrentMemoryStateGeneration;
}
}
return new MemoryCommitResult(memoryStateGeneration, keysModified, listeners,
mapToWriteToDisk);
}
每次提交到内存时,调用mLock
锁, 深拷贝mMap
到mapToWriteToDisk
中, 把putString
,putInt
等等创建的待修改的值mModified
的值写入到mapToWriteToDisk
, 使用mDiskWritesInFlight
来统计写磁盘的任务数量
-
enqueueDiskWrite(final MemoryCommitResult mcr, final Runnable postWriteRunnable))
加入写磁盘的任务队列
注意第二个参数commit()
的时候传的是null,apply()
的时候传了一个postWriteRunnable
private void enqueueDiskWrite(final MemoryCommitResult mcr,
final Runnable postWriteRunnable) {
//判断是否是同步提交 (commit同步,apply异步)
final boolean isFromSyncCommit = (postWriteRunnable == null);
//构建写入磁盘的任务writeToDiskRunnable
final Runnable writeToDiskRunnable = new Runnable() {
@Override
public void run() {
synchronized (mWritingToDiskLock) {
//3. 写入磁盘的操作
writeToFile(mcr, isFromSyncCommit);
}
synchronized (mLock) {
mDiskWritesInFlight--; //写入完成后,磁盘任务-1
}
if (postWriteRunnable != null) {
postWriteRunnable.run(); //调用apply()传入的postRunnable
}
}
};
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
//如果是同步提交 且只有一个任务,直接调用run()方法在当前线程执行不用加入任务队列了
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (mLock) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
}
//如果不是同步提交,或者有多个任务,就加入任务队列
QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit);
}
这里创建了写入磁盘的任务writeToDiskRunnable
并加入到了任务队列中, 写入完成后写磁盘任务减1mDiskWritesInFlight--
, 查看QueuedWork.queue(writeToDiskRunnable, !isFromSyncCommit)
方法
public static void queue(Runnable work, boolean shouldDelay) {
Handler handler = getHandler();
synchronized (sLock) {
sWork.add(work);
//apply()的延迟100ms延迟
if (shouldDelay && sCanDelay) {
handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
} else { //commit()的马上执行
handler.sendEmptyMessage(QueuedWorkHandler.MSG_RUN);
}
}
}
这里调用getHandler()
创建了HandlerThread
在子线程来串行的执行任务, 这里的shouldDelay == !isFromSyncCommit
.结合前面enqueue的代码可以发现使用commit()
的时候马上执行, apply()
写入会有100ms的延迟. 这个延迟具有一定的优化作用,在writeToFile
中会说明
-
writeToFile(mcr, isFromSyncCommit)
写入磁盘的操作
- 在看写操作前,先来看看
apply()
和commit()
是怎么处理写任务的队列的
他们都是通过CountDownLatch来实现的
CountDownLatch是JDK提供的一个并发编程类,是通过一个计数器来实现的,计数器的初始值为线程的数量。每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。
每个等待被写入的MemoryCommitResult
里都维护了一个计数器值为1的CountDownLatchnew CountDownLatch(1)
, 当我们调用mcr.writtenToDiskLatch.await()
时,实际把线程阻塞在这里,写入完成后又调用了MemoryCommitResult
的setDiskWriteResult()
方法把计数器置为0,解除阻塞
private static class MemoryCommitResult {
final CountDownLatch writtenToDiskLatch = new CountDownLatch(1);
void setDiskWriteResult(boolean wasWritten, boolean result) {
this.wasWritten = wasWritten;
writeToDiskResult = result;
writtenToDiskLatch.countDown();
}
}
不同的是commit()
和apply()
阻塞的线程不同
查看mcr.writtenToDiskLatch.await()
调用的位置. 在commit()
中enqueueDiskWrite()
加入队列后,阻塞的是commit所在的线程
@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(); //这里await()发生在操作commit的线程
} 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;
}
而apply()
中, mcr.writtenToDiskLatch.await()
在Runnable awaitCommit
的run()方法中,这个runnable又是在enqueue的Runnable postWriteRunnable
中被执行的,所以阻塞发生在队伍队列执行的线程HandlerThread
中
@Override
public void apply() {
final long startTime = System.currentTimeMillis();
final MemoryCommitResult mcr = commitToMemory();
final Runnable awaitCommit = new Runnable() {
@Override
public void run() {
try { //这里的runnable在下面的postWriteRunnable中被执行
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);
//postWriteRunnable在enqueue()后被任务队列调用
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);
}
- 写入之前的判断
// 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;
}
}
}
needsWirte
是用来决定是否写入的:
先对比磁盘版本和内存版本
mDiskStateGeneration < mcr.memoryStateGeneration
,磁盘版本小于内存版本才写入对比当前要写入的内存版本和全局的内存版本
mCurrentMemoryStateGeneration == mcr.memoryStateGeneration
才写入.
- 文件备份的作用
在初始化loadFromDisk()
和写入磁盘writeToFile()
中都用到了文件备份mBackupFile
,大概逻辑就是: 每次写入成功就会删除备份; 如果备份没有被删除,说明上次写入失败了,这时直接把mBackupFile
覆盖到mFile
里作为正式数据
apply
apply()
的流程和commit()
一样: 写入内存, 加入任务队列, 写入磁盘
看看不同点
- 上面介绍
commit()
已经提到了apply()
在执行任务队列的任务时,commit()
的阻塞发生在调用他的线程中(一般是主线程),而apply()
阻塞在工作线程中,执行完了才执行下一个.
@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) {
}
}
};
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);
}
- 加入任务队列中,和
commit
相比多了一步操作QueuedWork.addFinisher(awaitCommit);
当apply()
的任务正常写入时,又移除QueuedWork.removeFinisher(awaitCommit);
这里的finiisher队列
相当于缓存,写入前添加写入后删除. 存在这样一种情况当页面即将被销毁的时候工作队列sWork
中的内容还没被完全写入,可以到ActivityThread
中搜一下QueuedWork.waitToFinish
,会发现在 Activity/Service stop 的时候调用了QueuedWork.waitToFinish()
来取出addFinisher
加入的任务,保证在页面退出前SP全部写入
public final class ActivityThread extends ClientTransactionHandler {
private void handleServiceArgs(ServiceArgsData data) {
QueuedWork.waitToFinish();
}
}
- 为什么
apply()
加入队列的任务任务执行要加100ms的延迟handler.sendEmptyMessageDelayed(QueuedWorkHandler.MSG_RUN, DELAY);
因为apply()
对应的就是异步的情况,主线程不会像commit()
一样阻塞来等待结果.
但是当频繁apply
的时候,如果前后的 apply 间隔小于 100 毫秒,由于mCurrentMemoryStateGeneration
是实时更新到内存的, 那么这个条件判断只在最后的写任务会为 true,从而避免了过多的无用的IO操作
总结
- SP由ContextImpl中的
private static ArrayMap
维护,他是静态的一直存在于内存中> sSharedPrefsCache; - 不管是
commit()
还是apply()
写入, 都是分为两步的:1.写内存 2.写磁盘
在写内存完成后mMap
就会发生改变,这时我们就能通过getXXX
来获得结果而不需要等待写磁盘完成. - 写磁盘都是通过系统
QueueWork
工作队列完成的,他内部维护了一个HandlerThread
来执行每次写入内存后的结果作为任务. 不同的是commit()
会阻塞住当前的线程,直到写入完成解除阻塞,所以当前线程能感知到写入是否完成的时机,也就是有返回值. 而apply()
阻塞的是工作的线程HandlerThread
,解除阻塞也在HandlerThread
,当前线程是无法得知什么时候结束的.所以apply()
无法返回结果. 这也符合文档里的说法:commit()
是同步执行的,队列里的每个任务要等待上个任务写入磁盘成功解除阻塞. 而apply()
不会阻塞当前线程,且不是每个写内存的结果都会被写入磁盘的,apply()
通过延迟100ms执行和对比最新的内存写入版本来过滤掉不必要的写磁盘任务. -
apply()
由于是异步的,在Activity以及 Service 处理onStop
,onStartCommand
时,执行 QueuedWork.waitToFinish() 等待所有的等待锁释放,他里面维护了一个sFinishers
队列LinkedList
,会通过while循环不停的取出队列头部的任务直到任务执行完成为止.这有可能会引发ANR问题.更详细的请参照apply 引起的 ANR 问题sFinishers = new LinkedList<>()
总结
commit()
通过阻塞你当前线程,来拿到HandlerThread
内的磁盘IO结果作为返回值.
而apply()
的写磁盘操作是完全异步的, 你当前线程可以继续处理你的事情不用在这里阻塞等待结果.现在使用commit
会有提示使用apply
的提示. 这是因为:
- 不管是
commit
还是apply
,你使用getXXX
的时候使用的内存里的结果, 和是否写入磁盘成功没有任何关系 - 单次的磁盘持久化成功对于 下次启动应用使用 没有任何影响,谁能保证你存在磁盘上的东西不会被修改呢不会被删除呢, 我们每次getXXX前, 还是要做判断是否存在的