亲宝软件园·资讯

展开

Android SharedPreferences性能瓶颈解析

冰河本尊 人气:0

正文

想必大家对SharedPreferences都已经很熟悉了,大型应用使用SharedPreferences开发很容易出现性能瓶颈,相信很多开发者已经迁移到MMKV进行配置存储

说到MMKV我们总是会看到如下这张图

在模拟1000次写入的情况下,MMKV大幅度领先SharedPreferences,我们都知道MMKV使用了mmap方式进行存储,而SharedPreferences还是使用传统的文件系统,以xml的方式进行配置存储,mmap确实具备较好的性能和稳定性,但是真的两种不同的存储方式可以带来如此巨大的性能差异吗?

测试

因此我编写代码进行了一次测试

        findViewById(R.id.test5).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                long time2 = System.currentTimeMillis();
                SharedPreferences mSharedPreferences = WebTurboConfiguration.getInstance().mContext.getSharedPreferences(WebTurboConfigSp.Key.SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE);
                SharedPreferences.Editor editor = mSharedPreferences.edit();
                for (int i = 0; i < 1000; i++) {
                    editor.putString(i + "", 1000 + "");
                    editor.apply();
                }
                long time3 = System.currentTimeMillis();
                Log.e("模拟写入", "sp存储耗时 = " + (time3 - time2));
                MMKV mmkv = MMKV.defaultMMKV();
                for (int i = 0; i < 1000; i++) {
                    mmkv.putString(i + "", 1000 + "");
                }
                long time4 = System.currentTimeMillis();
                Log.e("模拟写入", "mmkv 存储耗时 = " + (time4 - time3));
            }
        });

输出如下

E/模拟写入: sp存储耗时 = 82ms

E/模拟写入: mmkv 存储耗时 = 6ms

MMKV确实性能显著强于SharedPreferences

apply方法的注释

SharedPreferences在使用的时候是推荐使用apply进行保存,我们来看一下apply方法的注释

注释中明确说明apply方法是先将存储数据提交到内存,然后异步进行磁盘写入,既然是异步写入,理论上IO不会拖后腿,我们可以认为时间都被消耗在了将数据提交到内存上,在写入内存上面SharedPreferences与MMKV会有这么大的性能差距吗?

这激起了我的兴趣

我使用AS自带的性能分析工具对SharedPreferences存储过程进行一次trace分析 分析图如下

可以轻松的从图中看到

数据存储put方法的主要耗时在puMapEntries上

代码调用如下

SharedPreferences的实际实现代码在SharedPreferencesImpl中

        @Override
        public void apply() {
            final long startTime = System.currentTimeMillis();
            final MemoryCommitResult mcr = commitToMemory();
            final Runnable awaitCommit = new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mcr.writtenToDiskLatch.await();
                        } catch (InterruptedException ignored) {
                        }
                        if (DEBUG && mcr.wasWritten) {
                            Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration
                                    + " applied after " + (System.currentTimeMillis() - startTime)
                                    + " ms");
                        }
                    }
                };
            QueuedWork.addFinisher(awaitCommit);
            Runnable postWriteRunnable = new Runnable() {
                    @Override
                    public void run() {
                        awaitCommit.run();
                        QueuedWork.removeFinisher(awaitCommit);
                    }
                };
            SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
            notifyListeners(mcr);
        }

主要看

final MemoryCommitResult mcr = commitToMemory();

代码比较长

        private MemoryCommitResult commitToMemory() {
            long memoryStateGeneration;
            boolean keysCleared = false;
            List<String> keysModified = null;
            Set<OnSharedPreferenceChangeListener> listeners = null;
            Map<String, Object> mapToWriteToDisk;
            synchronized (SharedPreferencesImpl.this.mLock) {
                if (mDiskWritesInFlight > 0) {
                    mMap = new HashMap<String, Object>(mMap);
                }
                mapToWriteToDisk = mMap;
                mDiskWritesInFlight++;
                boolean hasListeners = mListeners.size() > 0;
                if (hasListeners) {
                    keysModified = new ArrayList<String>();
                    listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
                }
                synchronized (mEditorLock) {
                    boolean changesMade = false;
                    if (mClear) {
                        if (!mapToWriteToDisk.isEmpty()) {
                            changesMade = true;
                            mapToWriteToDisk.clear();
                        }
                        keysCleared = true;
                        mClear = false;
                    }
                    for (Map.Entry<String, Object> e : mModified.entrySet()) {
                        String k = e.getKey();
                        Object v = e.getValue();
                        if (v == this || v == null) {
                            if (!mapToWriteToDisk.containsKey(k)) {
                                continue;
                            }
                            mapToWriteToDisk.remove(k);
                        } else {
                            if (mapToWriteToDisk.containsKey(k)) {
                                Object existingValue = mapToWriteToDisk.get(k);
                                if (existingValue != null && existingValue.equals(v)) {
                                    continue;
                                }
                            }
                            mapToWriteToDisk.put(k, v);
                        }
                        changesMade = true;
                        if (hasListeners) {
                            keysModified.add(k);
                        }
                    }
                    mModified.clear();
                    if (changesMade) {
                        mCurrentMemoryStateGeneration++;
                    }
                    memoryStateGeneration = mCurrentMemoryStateGeneration;
                }
            }
            return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                    listeners, mapToWriteToDisk);
        }

执行逻辑

step1:可能需要对现有的数据mMap进行一次深度拷贝,生成新的mMap对象

step2:对存储了已修改数据的map(mModified)进行遍历,写入mMap

step3:返回包含了全部数据的map用于存入文件系统

上文提到的大量耗时的puMapEntries方法就发生在step1中map的深度拷贝代码中

if (mDiskWritesInFlight > 0) {
    mMap = new HashMap<String, Object>(mMap);
}
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

为什么step1中说可能需要进行一次深度拷贝呢,因为mDiskWritesInFlight的值,在有配置需要写入时,他就会+1,只有完全写入磁盘,也就是此次配置已经被持久化,mDiskWritesInFlight才会-1,也就是说深度拷贝在上文提到的1000次写入的场景下是一定会发生的,除了第一次可能不需要深度拷贝,后面999次大概率会发生深度拷贝,因为在整个1000次的写入过程中,线程一直在不断的将配置写入磁盘,一直到1000次apply完成,数据可能还需要一段时间才能往磁盘里面写完

我们代码来模拟深度拷贝的场景,看深度拷贝map到底有多耗时,在代码中我们模拟了1000次深度拷贝

E/模拟写入: map深度拷贝耗时 = 52ms

E/模拟写入: sp存储耗时 = 59ms

E/模拟写入: mmkv 存储耗时 = 4ms

可以看到1000次深度拷贝的耗时已经接近SP1000次写入的耗时

因此我们得到如下结论 在开发者使用SharedPreferences的apply方法进行存储时,高频次的apply调用会导致每次apply时进行map的深度拷贝,导致耗时,如果只是一次调用,或者低频次的调用,那么SharedPreferences依然可以具备较好的性能

下面是一次调用的模拟,可以看到单次场景下与MMKV的性能差距不明显

E/模拟写入: sp存储耗时 = 231192ns

E/模拟写入: mmkv 存储耗时 = 229154ns

那么如果需要高频次写入SharedPreferences,如何保证较好的性能呢,比如在一个循环中写入SharedPreferences,那就要想办法避免map被频繁的深度拷贝,解决办法就是多次put完成后再apply

示例代码如下

        findViewById(R.id.test5).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                long time1 = System.currentTimeMillis();
                HashMap<String, String> map = new HashMap<>();
                for (int i = 0; i < 1000; i++) {
                    map.put(i + "", 1000 + "");
                    new HashMap<>(map);
                }
                long time2 = System.currentTimeMillis();
                Log.e("模拟写入", "map深度拷贝耗时 = " + (time2 - time1));
                SharedPreferences mSharedPreferences = WebTurboConfiguration.getInstance().mContext.getSharedPreferences(WebTurboConfigSp.Key.SHARED_PREFS_FILE_NAME, Context.MODE_PRIVATE);
                SharedPreferences.Editor editor = mSharedPreferences.edit();
                for (int i = 0; i < 1000; i++) {
                    editor.putString(i + "", 1000 + "");
                }
                editor.apply();
                long time3 = System.currentTimeMillis();
                Log.e("模拟写入", "sp存储耗时 = " + (time3 - time2));
                MMKV mmkv = MMKV.defaultMMKV();
                for (int i = 0; i < 1000; i++) {
                    mmkv.putString(i + "", 1000 + "");
                }
                long time4 = System.currentTimeMillis();
                Log.e("模拟写入", "mmkv 存储耗时 = " + (time4 - time3));
            }
        });

输出结果如下,SharedPreferences的存储耗时甚至低于MMKV

E/模拟写入: map深度拷贝耗时 = 55

E/模拟写入: sp存储耗时 = 1

E/模拟写入: mmkv 存储耗时 = 4

本文只针对循环保存配置这一种场景进行分析,无论如何使用,MMKV性能强于SharedPreferences是不争的事实,如果开发者开发的只是一个小工具,小应用,推荐使用SharedPreferences,他足够的轻量,如果开发商用中大型应用,MMKV依然是最好的选择,至于jetpack中的DataStore,并未使用过,不做评价

加载全部内容

相关教程
猜你喜欢
用户评论