探索SharedPreferences commit() OR apply()的心路历程

之前就了解过SharedPreferencesapply()commit()的效率要高,因为apply的文件写操作是异步的,放到了一个后台线程中进行。官方文档也是建议我们使用单进程的SharedPreferences时,尽量使用apply()。并且系统会保证异步操作极端情况下(进程被系统回收等)也会执行。

* 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.

看到这里本准备高高兴兴地跑去和领导说,我们把项目里的commit都换成apply吧,官方文档都建议我们这么做啦。领导在开会,那就写个demon把操作的耗时数据对比下,说服力就更强啦。

于是写个小程序,计算一下两个方法连续执行1000次的耗时

    private void spCommit() {
        long time = System.currentTimeMillis();
        SharedPreferences sp = getSharedPreferences("Test", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        for (int i = 0; i < 1000; i++) {
            editor.putInt("number" + i, i).commit();
        }
        Log.d("Preference探索", "time commit cost:" + (System.currentTimeMillis() - time));
    }

    private void spApply() {
        long time = System.currentTimeMillis();
        SharedPreferences sp = getSharedPreferences("Test", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        for (int i = 0; i < 1000; i++) {
            editor.putInt("number" + i, i).apply();
        }
        Log.d("Preference探索", "time apply cost:" + (System.currentTimeMillis() - time));
    }
D/Preference探索: time commit cost:156
D/Preference探索: time apply cost:1092
D/Preference探索: time commit cost:86
D/Preference探索: time apply cost:1261

这个结果让我大吃一惊,换了手机还是如此。又被谷歌给忽悠了?心想SharePreference的代码一定是实习生写的,于是开始自己看源码。

最后定位在SharedPreferencesImpl中enqueueDiskWrite这个方法中。

    private void enqueueDiskWrite(final MemoryCommitResult mcr,
                                  final Runnable postWriteRunnable) {
        ......省去若干行
        if (isFromSyncCommit) {
            boolean wasEmpty = false;
            synchronized (SharedPreferencesImpl.this) {
                wasEmpty = mDiskWritesInFlight == 1;
            }
            if (wasEmpty) {
                writeToDiskRunnable.run();//commit会在此处同步执行
                return;
            }
        }
       QueuedWork.singleThreadExecutor().execute(writeToDiskRunnable);//apply将事务抛到线程池中
    }

writeToDiskRunnable中是将editor中更新内容写到本地文件的核心io操作。很明显,commit操作是同步的,而apply直接将操作抛到了单线程的线程池中。

看到此处就更匪夷所思了,于是决定刨根究底,想到了某前辈介绍的Method Profiling功能,能查看系统方法的执行时间,决定进行初次使用。

Method Profiling功能在Android Device Monitor中


pic7.png

点击这个小按钮记录一段时间内方法的耗时,再点一下结束记录。


pic9.png

查看apply()函数的耗时详情,发现有两个耗时的可疑点,我们一一进行定位。

pic3.png

进入commitToMemory的()详情

pic4.png

发现居然时间都耗在一个HashMap的初始化函数上。

       private MemoryCommitResult commitToMemory() {
                ......
                // We optimistically don't make a deep copy until
                // a memory commit comes in when we're already
                // writing to disk.
                if (mDiskWritesInFlight > 0) {
                    // We can't modify our mMap as a currently
                    // in-flight write owns it.  Clone it before
                    // modifying it.
                    // noinspection unchecked
                    mMap = new HashMap<String, Object>(mMap);
                }
                mcr.mapToWriteToDisk = mMap;
        }

看到这里心中就有点数了,如果mDiskWritesInFlight > 0(还存在没有被处理的apply()操作),就需要copy出一个mMap进行后续操作,否则两个线程同时对一个HashMap进行读写操作就会引起crash。测试程序中连续的apply()一定会导致前面的apply()处理不完,后面的apply()就只能开辟新的HashMap。

第二个耗时点


pic5.png

这个耗时是线程池调度的开销,可见把事务抛到后台线程也会有一定开销,并非一定是环保的。

了解了上面那些特性后,开始猜测,如果将apply()分开操作,就不会因为前面有未完成的apply()而被迫开辟新的HashMap空间。事实胜于雄辩,测试一下吧。

    private void spDoSplit() {
        SharedPreferences sp = getSharedPreferences("link", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        while (true) {
            Random random = new Random();
            for (int i = 0; i < 1000; i++) {
                editor.putInt("number" + i, random.nextInt());
            }

            long time1 = System.currentTimeMillis();
            editor.commit();
            long time2 = System.currentTimeMillis();

            for (int i = 0; i < 1000; i++) {
                editor.putInt("number" + i, random.nextInt());
            }

            long time3 = System.currentTimeMillis();
            editor.apply();

            Log.d("Preference探索", "commit cost:" + (time2 - time1) + ", apply cost:" + (System.currentTimeMillis() - time3));
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

果不其然,结果如下:

D/Preference探索: commit cost:69, apply cost:4
D/Preference探索: commit cost:63, apply cost:2
D/Preference探索: commit cost:63, apply cost:4
D/Preference探索: commit cost:62, apply cost:9
D/Preference探索: commit cost:59, apply cost:2

平时写程序时,短时间内连续进行多个修改,没必要每个都进行apply(),可以将它们合并成一个apply(),在这种普遍情况下,apply()优于commit()是必然的,谷歌没有骗我,心中的疑惑解开啦:)

SharedPreference可以跨进程使用?

在上面研究中发现SharedPreferencesImpl中读取xml文件的函数startLoadFromDisk(),只在实例化SharedPreferencesImpl以及通过Context.getSharedPreferences()(mode为Context.MODE_MULTI_PROCESS)获取时才会执行。
也就是说,使用跨进程的SharedPreferences时,每次读取操作都需要通过Context.getSharedPreferences()拿一遍SharedPreferences,才能保证及时读取到其他进程的改动。每次读操作都牵扯整个XML文件的读取。

写个小程序验证一下

Activity进程写值

    private void changeNumberFrequently() {
        final SharedPreferences sp = getSharedPreferences("link", Context.MODE_MULTI_PROCESS
            | Context.MODE_WORLD_WRITEABLE
            | Context.MODE_WORLD_READABLE);
        Handler handler = new Handler() {
            @Override public void handleMessage(Message msg) {
                super.handleMessage(msg);
                sp.edit().putInt("number", i).commit();
                Log.d("Preference探索", "activity progress write number:" + i++);
                sendEmptyMessageDelayed(0, 3000);
            }
        };
        handler.sendEmptyMessage(0);
    }

Service进程读取
没有每次读取SharedPreferences的实例

  @Override public int onStartCommand(Intent intent, int flags, int startId) {
    final SharedPreferences sp = getSharedPreferences("link",
        Context.MODE_MULTI_PROCESS | Context.MODE_WORLD_READABLE | Context.MODE_WORLD_READABLE);
    final Handler handler = new Handler() {
      @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
        //如果没有每次读取`SharedPreferences`的实例
        int number = sp.getInt("number", -1);
        Log.d("Preference探索", "service progress read number:" + number);
        sendEmptyMessageDelayed(0, 3000);
      }
    };
    handler.sendEmptyMessage(0);
    return super.onStartCommand(intent, flags, startId);
  }
D Preference探索: activity progress write number:0
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:1
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:2
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:3
D Preference探索: service progress read number:0

改成每次读取SharedPreferences的实例

  @Override public int onStartCommand(Intent intent, int flags, int startId) {
    final Handler handler = new Handler() {
      @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
        //每次读取`SharedPreferences`的实例
        final SharedPreferences sp = getSharedPreferences("link",
            Context.MODE_MULTI_PROCESS | Context.MODE_WORLD_READABLE | Context.MODE_WORLD_READABLE);
        int number = sp.getInt("number", -1);
        Log.d("Preference探索", "service progress read number:" + number);
        sendEmptyMessageDelayed(0, 3000);
      }
    };
    handler.sendEmptyMessage(0);
    return super.onStartCommand(intent, flags, startId);
  }
D Preference探索: activity progress write number:0
D Preference探索: service progress read number:0
D Preference探索: activity progress write number:1
D Preference探索: service progress read number:1
D Preference探索: activity progress write number:2
D Preference探索: service progress read number:2
D Preference探索: activity progress write number:3
D Preference探索: service progress read number:3

简直弱爆了,怪不得官方文档已经废弃了跨进程使用SharePreferences

@Deprecated
public static final int MODE_MULTI_PROCESS = 0x0004;

关于跨进程数据的存储

根据个人的经验,想到以下几种方法
1 使用ContentProvider连接数据库是比较传统的方法,数据库自己有同步机制。
2 如果数据结构无法存进数据库,可以开辟一个独立进程进行文件读写,其他进程都绑定到这个进程进行读写。
3 文件锁,个人感觉坑会比较多,欢迎各位趟坑。

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

推荐阅读更多精彩内容