美文网首页
SharedPreferencesCompat的使用

SharedPreferencesCompat的使用

作者: seph_von | 来源:发表于2017-08-25 15:04 被阅读559次

    写在前面

    最近看了不少大牛的推文,再加上最近工作的一些心得,发现做了这么久的Android开发,到最后还是最基础的知识点才是往高级进阶的重中之重。因此也萌生了做一个介绍基础知识的系列文章。目前来说先从support包里的一些控件,工具类开始然后延伸出去。
    前几天写项目无意间IDE提示了SharedPreferencesCompat类,然后就多看了两眼...

    关于SharedPreferencesCompat

    SharedPreferencesCompat是V4包里的类,讲真,这个类并没有什么卵用。它本身对SharedPreferences并没有什么操作,还是需要通过传入SharedPreferences.Editor来实现数据的存储。
    既然没有什么卵用,那为什么要出现这么一个鸡肋呢?还得从Editor.apply()Editor.commit()的区别说起。这个后面再讲,先来看下SharedPreferencesCompat的用法

    SharedPreferencesCompat的用法

       var editor = getPreferences(Context.MODE_PRIVATE).edit()
       editor.putString(DEFAULT_KEY,value_sharedpref.text.toString())
       SharedPreferencesCompat.EditorCompat.getInstance().apply(editor)
    

    上面是用kotlin写的,java的实现差不多。可以看到的是,还是需要获取Editor的对象,然后将键值对存放在Editor中,而SharedPreferencesCompat的使用仅仅是代替了最后一步apply或者是commit的操作。那是不是呢?答案是肯定的!

    SharedPreferencesCompat源码解析

     public void apply(@NonNull SharedPreferences.Editor editor) {
           try {
                editor.apply();
            } catch (AbstractMethodError unused) {
                // The app injected its own pre-Gingerbread
                // SharedPreferences.Editor implementation without
                // an apply method.
                editor.commit();
           }
    }
    

    核心的代码部分就是上面这些了,可以看到,实际上就是先调用了editor.apply()方法,如果发生异常的话,则再调用editor.commit()的方法。简单说就是优先使用apply()方法。

    Commit与Apply的对比

    在看源码之前,我们先来分别实现下,来看下在时间效率上的差异
    commit存储数据

            var editor = getPreferences(Context.MODE_PRIVATE).edit()
            for (i in 1..1000){
                editor.putString(DEFAULT_KEY+i,"value$i")
            }
            var startTime = System.currentTimeMillis()
            editor.commit()
            Toast.makeText(this@SharedPreferencesCompatActivity,"commit use time ${System.currentTimeMillis()-startTime}",Toa
    

    apply存储数据

            var editor = getPreferences(Context.MODE_PRIVATE).edit()
            for (i in 1..1000){
                editor.putString(DEFAULT_KEY+i,"value$i")
            }
            var startTime = System.currentTimeMillis()
            editor.apply()
            Toast.makeText(this@SharedPreferencesCompatActivity,"apply use time ${System.currentTimeMillis()-startTime}",Toast.LENGTH_LONG).show()
    

    在三星S6 edge plus上对比发现,1000条数据,commit大概需要10ms左右,而apply只需要1ms

    居然有将近10倍的差距,那么commit和apply究竟是怎么实现的呢?

    源码分析

    SharedPreferences本身是一个接口,其实现的代码都在android.app.SharedPreferencesImpl类中,而Editor的实现都在EditorImpl类中,这是SharedPreferencesImpl的内部类。commit 和 apply 函数本身并不复杂,先来看源码:

    • commit
            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;
            }
    
    • apply
            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);
            }
    

    从上面的实现就可以看出为什么效率有这么大差距了,原因就是commit是同步的,而apply是异步执行的

    commit的操作是在主线程中执行的,在enqueueDiskWrite方法中,如果第二个参数为null,并且mDiskWritesInFlight为1的时候,那么写操作则在当前线程中直接完成:

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

    等写操作完成后再继续下面的步骤通知listener,最后返回写操作成功与否的标记

    apply的实现是直接把写任务抛给了子线程,然后就通知listener,然后就没有然后了。所有的写操作都在子线程了,函数并不关心是否成功

    这里有个疑问的地方,就是notifyListeners(mcr)。之前一直认为apply的时候,写磁盘和notify是在两个线程同时运行的,应该会出现读取的时候,还没写入的情况;但实际测试中并没有出现这种情况。原因是因为在获取内容值的时候,会先判断当前的SharepdPreferences是否加载,加载了,则直接返回mMap中的值,因为apply函数中在获取MemoryCommitResult的时候,已经把需要修改的值放到mMap中了,因此这个值并没有从磁盘里读出来,而是直接从内存里返回的。
    主要的代码如下:

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

    再看下awaitLoadedLocked()方法里

    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.
                BlockGuard.getThreadPolicy().onReadFromDisk();
            }
            while (!mLoaded) {
                try {
                    wait();
                } catch (InterruptedException unused) {
                }
            }
        }
    

    可以看到,如果mLoaded==true的话这个方法是直接跳过的;而mLoaded的赋值是在loadFromDisk()的方法里,而这个在SharedPreferencesImpl初始化的时候,或者在调用getSharedPreferences的时候都会调用的。因此回调里是可以接受到正确的值

    总结

    • 对于存储结果不需要太关注时,可以使用apply的方法,否则使用commit的方法
    • 如果使用apply的方法,则可以考虑使用SharedPreferencesCompat来避免兼容的问题,毕竟apply的方法是API9 才添加的。

    相关文章

      网友评论

          本文标题:SharedPreferencesCompat的使用

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