iOS进阶——微信开源存储框架MMKV(一)

前言

MMKV是微信开源的数据持久化框架,现在已经支持Android/iOS/PC 平台。该框架是基于mmap映射内存的key—value组件,使用protobuf实现数据的序列化和反序列化,性能高,稳定性强。微信在2015就在微信应用上使用了该框架。实验证明MMKV是数据持久化的首选。

mmap内存映射是什么?

我们知道数据的的读取与写入都是操作沙盒内的文件,每个应用程序都有限定的沙盒。读取写入数据的操作步骤:获取沙盒文件夹路径 ->创建文件路径 -> 使用文件管理对象创建文件 -> 创建文件对接对象 ->读取或写入数据 -> 关闭文件。需要频繁写入读取时,这样就非常消耗资源,mmap就是提高写入读取效率的,mmap映射内存是将磁盘里的文件映射到进程的虚拟内存中,根据映射文件指针读取数据时,系统会返回内核中的数据,通过 mmap 内存映射磁盘上的文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由 iOS 负责将内存回写到文件,不必担心 crash 导致数据丢失,大大降低了程序异常带来的数据丢失率。
mmap内存映射

protobuf是什么?

ProtoBuf是由google公司用于数据交换的序列结构化数据格式,具有跨平台、跨语言、可扩展特性,类型于常用的XML及JSON,但具有更小的传输体积、更高的编码、解码能力,特别适合于数据存储、网络数据传输等对存储体积、实时性要求高的领域。

优点:空间效率搞,时间效率要高,对于数据大小敏感,传输效率高的。
缺点:消息结构可读性不高,目前使用不广泛。

MMKV 源码分析

MMKV设计

MMKV维护了一个<String,AnyObject>的dic,在写入数据时,会在dit和mmap映射区写入相同的数据,最后由内核同步到文件。因为dic和文件数据同步,所以读取时直接去dit中的值。MMKV数据持久化的步骤:mmap 内存映射 -> 写数据 -> 读数据 -> crc校验 -> aes加密。
在MMKV的源码中,是怎么样内存映射的呢?

- (void)loadFromFile {
   // open  得到文件描述符m_fd
    m_fd = open(m_path.UTF8String, O_RDWR, S_IRWXU);    
    if (m_fd < 0) {
        MMKVError(@"fail to open:%@, %s", m_path, strerror(errno));
    } else {
        m_size = 0;
        struct stat st = {};
        if (fstat(m_fd, &st) != -1) {
            m_size = (size_t) st.st_size;   // 获取文件大小,为按页对齐做准备
        }
        // round up to (n * pagesize)  按页对齐
        if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
            m_size = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
            if (ftruncate(m_fd, m_size) != 0) { //  按页对齐
                MMKVError(@"fail to truncate [%@] to size %zu, %s", m_mmapID, m_size, strerror(errno));
                m_size = (size_t) st.st_size;
                return;
            }
        }
        //  1: 映射内存,用来将某个文件内容映射到内存中,对该内存区域的存取即是直接对该文件内容的读写 
        //参数1 :nullptr 对应内存的起始地址
        //参数2 :m_size 按页对齐后的文件大小
        //参数3:m_fd 映射到内存的文件
        m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);    
        if (m_ptr == MAP_FAILED) {
            MMKVError(@"fail to mmap [%@], %s", m_mmapID, strerror(errno));
        } else {    
            const int offset = pbFixed32Size(0);
            NSData *lenBuffer = [NSData dataWithBytesNoCopy:m_ptr length:offset freeWhenDone:NO];
            @try {
            // 文件中真正使用的空间有多大,因为文件被按页对齐后,真正使用的空间清楚,所以在文件开始做了记录
                m_actualSize = MiniCodedInputData(lenBuffer).readFixed32(); 
            } @catch (NSException *exception) {
                MMKVError(@"%@", exception);
            }
            MMKVInfo(@"loading [%@] with %zu size in total, file size is %zu", m_mmapID, m_actualSize, m_size);
            if (m_actualSize > 0) { // 当文件中有记录时,如果第一次使用或是已经清理过,实际使用空间将为0
                bool loadFromFile, needFullWriteback = false;
                if (m_actualSize < m_size && m_actualSize + offset <= m_size) { // 检查文件是否正常
                    if ([self checkFileCRCValid] == YES) {  
                        loadFromFile = true;
                    } else {    // 校验失败后的行为
                        loadFromFile = false;
                        if (g_callbackHandler && [g_callbackHandler respondsToSelector:@selector(onMMKVCRCCheckFail:)]) {
                            auto strategic = [g_callbackHandler onMMKVCRCCheckFail:m_mmapID];
                            if (strategic == MMKVOnErrorRecover) {  // 如果校验失败后要继续使用
                                loadFromFile = true;    
                                needFullWriteback = true;
                            }
                        }
                    }
                } else {    // 根据文件中记录,文件不正常
                    MMKVError(@"load [%@] error: %zu size in total, file size is %zu", m_mmapID, m_actualSize, m_size);
                    loadFromFile = false;
                    if (g_callbackHandler && [g_callbackHandler respondsToSelector:@selector(onMMKVFileLengthError:)]) {
                        auto strategic = [g_callbackHandler onMMKVFileLengthError:m_mmapID];
                        if (strategic == MMKVOnErrorRecover) {  // 文件不正常后要继续使用
                            loadFromFile = true;
                            needFullWriteback = true;
                            [self writeAcutalSize:m_size - offset]; // 重新记录下文件的相关信息
                        }
                    }
                }
                if (loadFromFile) { // 假定文件是正常的,从文件中读取
                    NSData *inputBuffer = [NSData dataWithBytesNoCopy:m_ptr + offset length:m_actualSize freeWhenDone:NO];
                    if (m_cryptor) {
                      //对文件数据进行AES加密(对称加密算法,加密与解密使用相同的秘钥)
                        inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
                    }
                    // 2. 初始化m_dic
                    //  如果文件存在错误(例如crc校验不通过),会导致数据错误或是丢失
                    m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer];
                     //  使用MiniCodedOutputData将数据按字节拷贝到指定区域
                    m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize);
                    // 如果文件存在错误,decode到m_dic过程中可能会丢弃部分数据,所以要将m_dic,保证m_dic与文件的同步
                    if (needFullWriteback) {    
                        [self fullWriteback];
                    }
                } else {    // 文件不正常且不打算恢复,需要重建,丢弃原来的数据
                    [self writeAcutalSize:0];
                    m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
                    [self recaculateCRCDigest];
                }
            } else {   
                 //  文件中没有kv,没有必要读入dic
                m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
                [self recaculateCRCDigest];
            }
            MMKVInfo(@"loaded [%@] with %zu values", m_mmapID, (unsigned long) m_dic.count);
        }
    }
    if (m_dic == nil) {
        m_dic = [NSMutableDictionary dictionary];
    }

    
    if (![self isFileValid]) { 
        MMKVWarning(@"[%@] file not valid", m_mmapID);
    }

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

推荐阅读更多精彩内容

  • MMKV MMKV——基于 mmap 的高性能通用 key-value 组件,底层序列化/反序列化使用 proto...
    Tim_Hu阅读 17,747评论 0 9
  • 说到轻量级的数据持久化,大家最先想到的就是SharedPreferences(以下简称SP)了,SP存储方式为xm...
    PanGeng阅读 1,993评论 0 2
  • kv数据持久化需要的功能 假设要设计一个kv的存储功能: 首先是可靠性,在各种情况下能够将kv保存 性能的要求,当...
    sunshinfight阅读 2,217评论 1 4
  • 本文转自微信开发团队凌国的分享。原文 MMKV 是基于 mmap 内存映射的移动端通用 key-value 组件,...
    那样风采阅读 42,249评论 6 35
  • 转做在线英语少儿培训销售快6个月了。 在这个过程中学到一个很好的策略是问题前置化。 刚刚开始做这个岗位会想办法避免...
    素墨_阅读 3,570评论 0 0