SharedPreferencesCompat的使用

写在前面

最近看了不少大牛的推文,再加上最近工作的一些心得,发现做了这么久的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 才添加的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容