技术收藏

深入理解Android SharedPreferences的co

2017-07-17  本文已影响1547人  Jerry2015

文章在说什么

Android的SharedPreferences在保存的时候会有commit和apply两个方法,这里主要解释下这两个方法详细区别,以及这两个方法的使用场景。
如果不关心具体实现,你可以直接记住结论,在不涉及到跨进程通过SharedPreferences共享数据的情况,就永远使用apply,apply方法执行后可以立即在任意地方读取到更新后对应的key值,不会出现因为apply是异步就需要等待一会儿,再读取的情况。

详细分析

Android提供了一个简单快捷的保存键值对到文件的类SharedPreferences。通过SharedPreferences读写参数,你大概会写出下面这样的代码。

    private static void read(Context context) {
        SharedPreferences sharedPreferences = context.getSharedPreferences("文件名", MODE_PRIVATE);
        String result = sharedPreferences.getString("key", "default_value");
        Log.d(TAG, "read: " + result);
    }

    private static void write(Context context) {
        SharedPreferences.Editor editor = context.getSharedPreferences("文件名", MODE_PRIVATE).edit();
        editor.putString("key", "value");
        ? editor.commit();
        ? editor.apply();
    }

上面两个代码如果深入思考可能会遇到这样几个问题。

        /**
         * 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 apply wins.
         *
         * <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();

上面是API25中,apply方法的注释。大致意思就是apply是一个原子请求(不需要担心多线程同步问题)。commit将同步的把数据写入磁盘和内存缓存。而apply会把数据同步写入内存缓存,然后异步保存到磁盘,可能会执行失败,失败不会收到错误回调。如果你忽略了commit的返回值,那么可以使用apply替换任何commit的实例。
简单说就是如果你不考虑保存失败的情况,那么你可以把所有使用commit的代码,替换成apply。
注释是这么写了,那如果我们要深究原因的话继续往下从源码一步步看。
我们获取SharedPreferences示例是从Context拿到的。Context有个ContextImpl的实现类。所以进入ContextImpl类看下SharedPreferences是如何创建的。

    @Override
    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;
    }

最终getShredPreferences会重载到上面这个方法。然后从getSharedPreferencesCacheLocked方法来获取SharedPreferences实例。

    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;
    }

因为SharedPreferences是支持自定义文件名的,所以这里的缓存是个Map。(这里有个比较奇怪的写法是用packageName又做了一个缓存。这是为什么呢?因为以前SharedPreferences是可以设置为MODE_WORLD_READABLE
和MODE_WORLD_WRITEABLE
的。就是可以被跨App访问。Google为了方便跨App偷数据真是煞费苦心,后来Google就禁止使用这两个FLAG了,但是这个写法还是继续保留。)到这里,如果缓存没有需要的SharedPreferences,就创建一个SharedPreferencesImpl,最终总是可以拿到一个SharedPreferences实例,而且是个单例。所以无论从哪个Context最终都会得到同一个SharedPreferences实例。既然SharedPreferences的实现类是SharedPreferencesImpl,那我们去看下SharedPreferencesImpl的构造方法都干了什么。

    SharedPreferencesImpl(File file, int mode) {
        mFile = file;
        mBackupFile = makeBackupFile(file);
        mMode = mode;
        mLoaded = false;
        mMap = null;
        startLoadFromDisk();
    }

构造方法除了初始化一些基本参数外,调用了一个startLoadFromDisk方法。

    private void startLoadFromDisk() {
        synchronized (this) {
            mLoaded = false;
        }
        new Thread("SharedPreferencesImpl-load") {
            public void run() {
                loadFromDisk();
            }
        }.start();
    }

    private void loadFromDisk() {
         ......
         map = XmlUtils.readMapXml(str);
         ...... 
         mMap = map;
         ......
    }

loadFromDisk代码比较多,但大致意思就是从磁盘把SharedPreferences文件里保存的xml信息读取到内存的Map里。
好,接着我们去看下实际保存数据的时候是怎么处理的。 保存的时候我们会用到Editor,Editor的实现类叫EditorImpl。

    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) {
            synchronized (this) {
                mModified.put(key, value);
                return this;
            }
        }
        ......
        public void apply() {
            final MemoryCommitResult mcr = commitToMemory();
            ......// 异步保存到磁盘,通知所有观察者有数据变化。
        }

        public boolean commit() {
            MemoryCommitResult mcr = commitToMemory();
            ......// 同步保存到磁盘,通知所有观察者有数据变化,并返回执行结果。
            return mcr.writeToDiskResult;
        }
    }

这时候我们再去看看读取是怎么处理的。

    public String getString(String key, @Nullable String defValue) {
        synchronized (this) {
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }

读取根本没有涉及到磁盘操作,直接从内存mMap里把数据读出来了。看到这里一下子就全部清楚了。
我们再重新梳理下整个过程。

上一篇下一篇

猜你喜欢

热点阅读