美文网首页技术收藏
深入理解Android SharedPreferences的co

深入理解Android SharedPreferences的co

作者: Jerry2015 | 来源:发表于2017-07-17 09:34 被阅读1547次

    文章在说什么

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

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

    • 都知道SharedPreferences最终是保存到文件的,文件IO一般会是一个耗时操作。那么read的时候我们需不需要自己再做一层内存cache缓存数据提高读效率?
    • 文件IO是个耗时操作,所以写方法问号那里有commit和apply两个方法,可能大家都知道一个是同步提交,一个是异步提交,而且IDE提示让我们用apply,那么这个异步的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里把数据读出来了。看到这里一下子就全部清楚了。
    我们再重新梳理下整个过程。

    • SharedPreferences是个单例,所以任意Context拿到的都是同一个实例。
    • SharedPreferences在实例化的时候会把SharedPreferences对应的xml文件内容全部读取到内存。
    • 对于非多进程兼容的SharedPreferences的读操作是从内存读取的,不涉及IO操作。写入的时候由于内存已经保存了完整的xml数据,然后新写入的数据也会同步更新到内存,所以无论是用commit还是apply都不会影响立即读取。
    • 除非你需要关心xml是否写入文件成功,否则你应该在所有调用commit的地方改用apply。
    • 我们需要对SharedPreferences在包装一层内存缓存来提高性能吗?完全不需要,因为SharedPreferences本身已经做了内存缓存。

    相关文章

      网友评论

        本文标题:深入理解Android SharedPreferences的co

        本文链接:https://www.haomeiwen.com/subject/izmnhxtx.html