探索SharedPreferences commit() OR

作者: Rainbow冰糖葫芦娃 | 来源:发表于2016-11-04 17:51 被阅读334次

之前就了解过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 文件锁,个人感觉坑会比较多,欢迎各位趟坑。

相关文章

网友评论

    本文标题:探索SharedPreferences commit() OR

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