SharedPreferences 的commit和apply分析

之前在做项目开发的时候曾经遇到过一个坑,我们的业务需求是点击相应的国家图标进行国家切换包含汇率、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一下发现大家的结论都是:

  1. apply没有返回值而commit返回boolean表明修改是否提交成功
  2. apply是将修改数据原子提交到内存,而后异步真正提交到硬件磁盘;而commit是同步的提交到硬件磁盘,因此,在多个并发的提交commit的时候,他们会等待正在处理的commit保存到磁盘后在操作,从而降低了效率。而apply只是原子的提交到内存,后面有调用apply的函数的将会直接覆盖前面的内存数据,这样从一定程度上提高了很多效率。
  3. 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。

    原文作者:碎格子
    原文地址: https://blog.csdn.net/u010198148/article/details/51706483
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞