之前在做项目开发的时候曾经遇到过一个坑,我们的业务需求是点击相应的国家图标进行国家切换包含汇率、url等的切换,所以当时我们考虑的是切换的时候用SharedPreferences来对存储当前的国家代码,所以我们有了以下的代码:
SharedPreferences.Editor editor = PreferenceUtil.getDefaultPreference(DgApplication.getInstance()).edit();
//value是切换的国家代码
editor.putInt(PreferenceUtil.STRING_COUNTRY_CODE, value);
editor.apply();
在完成切换国家动作之后,为了清除之前的缓存数据更新服务器url,我们选择了像微信退出登录那样重启应用。那个时候我们只开放了两个国家,这个坑在最开始的时候并没有出现问题,后来我们开放了第三个国家、第四个国家…然后突然有一天这个潜在的隐患爆发了,测试人员跑来说为什么我切换到新加坡但是我的界面显示的是马币(马来西亚的货币)?那时候我们才开始注意到这个问题。后来一路追踪排查,发现是这句 editor.apply();
出的锅。解决办法就是将apply改为commit,也就是 editor.commit();
就可以了。那么现在来想想,都是提交,为什么用apply会出问题,而commit就可以成功处理呢?他们俩之间到底有什么区别呢?google一下发现大家的结论都是:
- apply没有返回值而commit返回boolean表明修改是否提交成功
- apply是将修改数据原子提交到内存,而后异步真正提交到硬件磁盘;而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
- apply方法不会提示任何失败的提示
虽然这些说法都对,但是我们还是追其根源,看看他们各自的源码里都做了什么“手脚”。
apply和commit都是SharedPreferences的内部接口Editor的一个方法,而他们的实现都在SharedPreferencesImpl类里。
首先我们看看在SharedPreferences.Editor接口里commit方法的定义:
/** * Commit your preferences changes back from this Editor to the * {@link SharedPreferences} object it is editing. This atomically * performs the requested modifications, replacing whatever is currently * in the SharedPreferences. * * <p>Note that when two editors are modifying preferences at the same * time, the last one to call commit wins. * * <p>If you don't care about the return value and you're * using this from your application's main thread, consider * using {@link #apply} instead. * * @return Returns true if the new values were successfully written * to persistent storage. */
boolean commit();
这里注释的意思大概是这个提交方法会返回一个修改后的结果,会自动执行修改的请求,替换掉当前SharedPreferences里的东西。需要注意的是当两个commit动作同时发生时,最后一个动作会成功。如果不考虑结果并且使用在主线程可以使用apply方法替代,如果成功写入硬件磁盘则会返回true。
综合一下这个注释也就是google上说的:(1)会返回执行结果(2)如果不考虑结果并且是在主线程执行可以考虑apply
下面看看apply方法的定义:
/** * <p>Unlike {@link #commit}, which writes its preferences out * to persistent storage synchronously, {@link #apply} * commits its changes to the in-memory * {@link SharedPreferences} immediately but starts an * asynchronous commit to disk and you won't be notified of * any failures. If another editor on this * {@link SharedPreferences} does a regular {@link #commit} * while a {@link #apply} is still outstanding, the * {@link #commit} will block until all async commits are * completed as well as the commit itself. * * <p>As {@link SharedPreferences} instances are singletons within * a process, it's safe to replace any instance of {@link #commit} with * {@link #apply} if you were already ignoring the return value. * * <p>You don't need to worry about Android component * lifecycles and their interaction with <code>apply()</code> * writing to disk. The framework makes sure in-flight disk * writes from <code>apply()</code> complete before switching * states. * * <p class='note'>The SharedPreferences.Editor interface * isn't expected to be implemented directly. However, if you * previously did implement it and are now getting errors * about missing <code>apply()</code>, you can simply call * {@link #commit} from <code>apply()</code>. */
void apply();
略微有点长,大概意思就是apply跟commit不一样的地方是,它使用的是异步而不是同步,它会立即将更改提交到内存,然后异步提交到硬盘,并且如果失败将没有任何提示。
读了以上的注释似乎只了解了他们的区别及简单的工作方式,我们接着去看看具体实现:
SharedPreferencesImpl.EditorImpl.java#commit&apply
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;
}
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);
}
这两个方法都是首先修改内存中缓存的mMap的值,然后将数据写到磁盘中。它们的主要区别是commit会等待写入磁盘后再返回,而apply则在调用写磁盘操作后就直接返回了,但是这时候可能磁盘中数据还没有被修改。
再来看看这两个方法都要调用的commitToMemory
SharedPreferencesImpl.EditorImpl.java#commitToMemory
private MemoryCommitResult commitToMemory() {
MemoryCommitResult mcr = new MemoryCommitResult();
synchronized (SharedPreferencesImpl.this) {
...
synchronized (this) {
...
for (Map.Entry<String, Object> e : mModified.entrySet()) {
String k = e.getKey();
Object v = e.getValue();
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 mcr;
}
这里使用了HashMap对写入的key进行检索比较,如果之前有同样的key且value不同则用新的valu覆盖旧的value,如果没有存在同样的key则完整写入。需要注意的是这里使用了同步锁住edtor对象,保证了当前数据正确存入。
最后比较重要的就是SP的写磁盘操作。之前介绍的apply和commit都调用了enqueueDiskWrite()方法。以下为其具体实现代码。writeToDiskRunnable中调用writeToFile写文件。如果参数中的postWriteRunable为null,则该Runnable会被同步执行,而如果不为null,则会将该Runnable放入线程池中异步执行。在这里也验证了之前提到的commit和apply的区别。
private void enqueueDiskWrite(final MemoryCommitResult mcr,final Runnable postWriteRunnable) {
final Runnable writeToDiskRunnable = new Runnable() {
public void run() {
synchronized (mWritingToDiskLock) {
writeToFile(mcr);
}
synchronized (SharedPreferencesImpl.this) {
mDiskWritesInFlight--;
}
if (postWriteRunnable != null) {
postWriteRunnable.run();
}
}
};
final boolean isFromSyncCommit = (postWriteRunnable == null);
// Typical #commit() path with fewer allocations, doing a write on
// the current thread.
if (isFromSyncCommit) {
boolean wasEmpty = false;
synchronized (SharedPreferencesImpl.this) {
wasEmpty = mDiskWritesInFlight == 1;
}
if (wasEmpty) {
writeToDiskRunnable.run();
return;
}
QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);
}
看到这里,我想关于commit和apply他们之间的区别已经很明确了,而对于我遇到的坑也有了合理的解释,因为我用的是apply进行存入提交,当我来回切换国家的时候,可能数据还没有正确存入应用就重启了,导致用户看到界面显示的数据错乱。
因此总结一下,如果关心存入结果则使用commit如果不关心存入结果则使用apply。