SharedPreference与mmkv

2021-09-25  本文已影响0人  四月苜蓿

一、持久化数据Key-Values存储方案,如何设计?

1、新建磁盘文件:涉及IO读写 (效率问题)
2、选取数据格式:xml,json,protocol (增删改查问题)
3、映射到内存:map集合(内存占用问题)
4、提供get put 方法,修改内存,修改文件,(数据一致性问题)

SharedPreference原理

1、初始化

private void loadFromDisk() {
    synchronized (mLock) {
        if (mLoaded) {
            return;
        }
        if (mBackupFile.exists()) {
            mFile.delete();
            mBackupFile.renameTo(mFile);
        }
   ...
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
    synchronized (mLock) {
        awaitLoadedLocked();
        String v = (String)mMap.get(key);
        return v != null ? v : defValue;
    }
}

问题:如果没有初始化完成,出现线程阻塞问题

public void apply() {
    final long startTime = System.currentTimeMillis();

    // 1、先更新内存数据
    final MemoryCommitResult mcr = commitToMemory();
    // 2、更新磁盘数据
    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");
                }
            }
        };
        ....
        }
@GuardedBy("mWritingToDiskLock")
private void writeToFile(MemoryCommitResult mcr, boolean isFromSyncCommit) {
   ...
    try {
        FileOutputStream str = createFileOutputStream(mFile);
        XmlUtils.writeMapXml(mcr.mapToWriteToDisk, str);
                ...
        try {
            final StructStat stat = Os.stat(mFile.getPath());
            synchronized (mLock) {
                mStatTimestamp = stat.st_mtim;
                mStatSize = stat.st_size;
            }
        } catch (ErrnoException e) {
            // Do nothing
        }
                ...
        return;
    } catch (XmlPullParserException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    } catch (IOException e) {
        Log.w(TAG, "writeToFile: Got exception:", e);
    }
    
}

问题:

1、写磁盘存在失败情况,出现磁盘数据和内存数据不一致的情况
2、出现ANR风险
anr原因分析:
QueuedWork.addFinisher(awaitCommit);
QueuedWork.waitToFinish();
查看QueuedWork 以及 ActivityThread 源码

总结:
SharedPreference的问题

SharedPreference 问题 解决方案
IO读写 传统IO读写,效率慢
数据格式xml 不支持局部更新
内存映射 饿汉模式,初始化就创建map
可靠性 存在数据不一致性,存在ANR风险
多进程 不支持多进程数据共享 ?

二、文件拷贝相关知识补充

应用运行时,存在用户空间内存,和内核空间,两个空间的内存是隔离的,互相不影响。下图所示:


image.png

应用层的任何代码执行都是在用户空间执行的,如果要进行系统调用,比如IO操作,那么就需要进入内核空间,在内核空间实现系统调用。

那么,用户空间和内核空间如何实现数据共享呢?答案就是通过CPU拷贝来实现的。

下图显示了,一次传统IO操作的过程发生的内存拷贝操作:

    File source = new File("/Users/dw/applogs.zip");
        File dest = new File("/Users/dw/applogs_io.zip");
        java.io.InputStream input = null;
        java.io.OutputStream output = null;
         
        input = new FileInputStream(source);
        output = new FileOutputStream(dest);
        byte[] buf = new byte[1024];
        int bytes;
        while ((bytes = input.read(buf)) > 0) {
             output.write(buf, 0, bytes);
        }
image.png

1、read系统调用,DMA执行了一次数据拷贝,从磁盘拷贝到内核空间
2、read结束后,发生了第二次数据拷贝,由CPU将数据从内核空间拷贝到用户空间
3、write系统调用,CPU将用户空间的数据拷贝到内核空间
4、write结束,DMA执行了一次数据拷贝,从磁盘拷贝到内核空间

以上过程,发生了4次数据拷贝,两次上下文切换。

三、零拷贝技术

四、mmap

4.1 mmap的write

1.进程(用户态)将需要写入的数据直接copy到对应的mmap地址(内存copy)
2.若mmap地址未对应物理内存,则产生缺页异常,由内核处理
3.若已对应,则直接copy到对应的物理内存
4.由操作系统调用,将脏页回写到磁盘(通常是异步的)

4.2 mmap的read

image.png

4.3 mmap函数

mmap函数
映射文件

 void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);

解除映射

int munmap(void *addr, size_t length);

函数参数解析

4.3 mmap总结

优点:

缺点:

mmap读写数据速度


image.png

五、MMKV实现

mmkv 是腾讯开源的使用mmap原理实现的高效的Key-Values存储方案,在2015年时候开始在微信使用。

mmkv效率演示

 
    private fun spTest() {
        val start = System.currentTimeMillis()
        val sharedPreferences = this.getSharedPreferences("sp_name", Context.MODE_PRIVATE)
        val edit = sharedPreferences.edit()
        for (i in 0..3000) {
            edit.putInt("key$i", i).apply()
        }
        Log.i(TAG, "spTest cost ${System.currentTimeMillis() - start} ms")
    }

    private fun mmkvTest() {
        val start = System.currentTimeMillis()
        val defaultMMKV = MMKV.defaultMMKV()
        for (i in 0..3000) {
            defaultMMKV.putInt("key$i", i).apply()
        }
        Log.i(TAG, "mmkvTest cost ${System.currentTimeMillis() - start} ms")
    }

// 调用方
 Thread {
            mmkvTest()
            spTest()
        }.start()
  

2021-09-25 16:26:25.680 18488-18564/com.douwan.launchdemo I/MainActivity: mmkvTest cost 52 ms
2021-09-25 16:26:27.599 18488-18564/com.douwan.launchdemo I/MainActivity: spTest cost 1919 ms

5.1、mmkv数据结构

数据格式使用 protobuf 协议,数据结构更加精简,key类型是String,values类型统一系列化为buffer。

message KV {
    string key = 1;
    buffer value = 2;
}

mmkv选择的数据结构是Key-Values链表结构


image.png

5.2 mmkv写入方式

5.3 mmkv

官方文档

映射完成之后,直接操作内存中的map集合,完成数据读写操作。
对这片映射空间进行了读写操作,会引发缺页异常,系统会自动回写脏页面到对应的文件磁盘上,实现数据持久化存储

//get方法
float MMKV::getFloat(MMKVKey_t key, float defaultValue) {
    if (isKeyEmpty(key)) {
        return defaultValue;
    }
    SCOPED_LOCK(m_lock);
    auto data = getDataForKey(key);
    if (data.length() > 0) {
        try {
            CodedInputData input(data.getPtr(), data.length());
            return input.readFloat();
        } catch (std::exception &exception) {
            MMKVError("%s", exception.what());
        }
    }
    return defaultValue;
}


MMBuffer MMKV::getDataForKey(MMKVKey_t key) {
    checkLoadData();
#ifndef MMKV_DISABLE_CRYPT
    if (m_crypter) { // 有加密
        auto itr = m_dicCrypt->find(key);
        if (itr != m_dicCrypt->end()) {
            auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
            return itr->second.toMMBuffer(basePtr, m_crypter);
        }
    } else
#endif
    { //未加密
        auto itr = m_dic->find(key);
        if (itr != m_dic->end()) {
            auto basePtr = (uint8_t *) (m_file->getMemory()) + Fixed32Size;
            return itr->second.toMMBuffer(basePtr);
        }
    }
    MMBuffer nan;
    return nan;
}

// m_dicCrypt , m_dic 定义:
    mmkv::MMKVMap *m_dic;
    mmkv::MMKVMapCrypt *m_dicCrypt;

// MMKVMap , MMKVMapCrypt 结构体,
// unordered_map: C++定义的map,内部实现了哈希表,其查找速度非常的快
using MMKVMap = std::unordered_map<NSString *, mmkv::KeyValueHolder, KeyHasher, KeyEqualer>;
using MMKVMapCrypt = std::unordered_map<NSString *, mmkv::KeyValueHolderCrypt, KeyHasher, KeyEqualer>;

六、MMKV vs SharedPreference

SharedPreference 问题 MMKV
IO读写 传统IO读写,效率慢 mmap方式读写磁盘
数据格式xml 不支持局部更新 protocol结构体 Key-Values链表方式
内存映射 饿汉模式,初始化就创建map 初始化只是映射一个内存地址
可靠性 存在数据不一致性,存在ANR风险 直接操作内存,不存在数据一致性问题
多进程 不支持多进程数据共享 支持多进程数据共享
上一篇 下一篇

猜你喜欢

热点阅读