探索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中

《探索SharedPreferences commit() OR apply()的心路历程》 pic7.png

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

《探索SharedPreferences commit() OR apply()的心路历程》 pic9.png

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

《探索SharedPreferences commit() OR apply()的心路历程》 pic3.png

进入commitToMemory的()详情

《探索SharedPreferences commit() OR apply()的心路历程》 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。

第二个耗时点

《探索SharedPreferences commit() OR apply()的心路历程》 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 文件锁,个人感觉坑会比较多,欢迎各位趟坑。

    原文作者:Rainbow冰糖葫芦娃
    原文地址: https://www.jianshu.com/p/df1012d5ee73
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞