IOS版的MMKV框架解析,看这一篇就够了?

IOS版的MMKV框架解析,看这一篇就够了?

  • MMKV
    • 了解MMKV前需要了解的基础知识
      • ProtocolBuffer(下文检查PB)
        • demo:
        • 基础知识
          • 二进制串
          • Tag - Length - Value 的数据存储方式
          • 在MMKV中,使用的是Length-Value的方式进行存储!!!!
          • PB相对于json的优点:
          • PB相对于json的缺点:
      • CRC(Cyclic Redundancy Check,循环冗余校验)算法
        • 奇偶校验算法((CRC-1))
        • 累加和校验算法
        • 求余校验算法(CRC-m)
        • 总结求余校验算法的具体步骤:
    • 注意:mmap()函数在MMKV的作用是:你只需要往映射内存里面写入数据就可以了,操作系统会帮你把写入的数据同步到映射文件中(这里的映射文件指的是沙盒中的mmkv文件)。
  • 以上内容均来自网上,来自文章底部的参考文献
  • MMKV
    • MMKV的initialize()方法源码解析
    • CScopedLock类
    • MMKV的defaultMMKV()方法源码解析
    • MMKV的setLogLevel()方法源码解析
    • MMKV的registerHandler()方法源码解析
    • MMKV的setMMKVBasePath()方法源码解析
    • MMKV的mmkvWithID: relativePath:()方法源码解析
    • MMKV的setBool: forKey:()方法源码解析
      • 请先看大致的流程图:
      • 接下来进行源码解析:
    • MMKV的setInt32: forKey:()方法源码解析
      • 请先看大致的流程图:
      • 接下来进行源码解析:
    • MMKV的getBoolForKey方法源码解析
      • 请先看大致的流程图:
      • 接下来进行源码解析:
    • MMKV的getInt32ForKey方法源码解析
      • 请先看大致的流程图:
      • 接下来进行源码解析:
    • MMKV的prepareMetaFile方法源码解析
    • MMKV的loadFromFile方法源码解析
    • MMKV的ensureMemorySize方法源码解析
    • MMKV的recaculateCRCDigest方法和updateCRCDigest方法的源码解析
  • MMKV文件的内容组织结构是怎么样的?如果多次保存相同的键值对时,内存和mmkv文件是怎么被保存的?
    • 请看下面的Demo,先看test1方法。
    • 请看上面的Demo,看test2方法,

MMKV

了解MMKV前需要了解的基础知识

ProtocolBuffer(下文检查PB)

   PB是一种轻便的高效的结构化数据存储的格式。它适用于数据传输和数据存储的场景。

   JSON和XML最终在网络上传输时都会将字符串转为二进制数据并进行传输的。

demo:

   1 Person.proto文件的代码如下。通过protoc --objc_out=./ ./Person.proto 即可根据Person.proto文件生成对应的Person.pbobjc.h和Person.pbobjc.m文件。

syntax = "proto3"; //使用proto3的语法

//message代表一个数据结构,也就相当于一个类,类名是Person
message Person {
    //下面声明的属性中,等号右边的数字用于标识Person的属性。比如数字1用来表示Person中的name属性。
    string name = 1;
    int32 age = 2;
    repeated int32 friends = 3;
}

   2 把Person.proto、Person.pbobjc.h和Person.pbobjc.m文件拖入xcode工程里面,结果如下图。
IOS版的MMKV框架解析,看这一篇就够了?_第1张图片

   3 在ViewController.m中使用Person类,ViewController.m的代码如下:

#import "ViewController.h"
#import "Person.pbobjc.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = UIColor.whiteColor;
    
    Person *person = [Person new];
    person.name = @"仕兴啊";
    person.age = 23;
    person.friendsArray = [GPBInt32Array array];
    [person.friendsArray addValue:10];
    
    
    
    //将person实例对象序列化成data类型
    NSData *pbData = [person data];
    
    
    NSString* jsonStr = @"{\"name\":\"仕兴啊\",\"age\":23,\"friendsArray\":[10]}";
    NSData *jsonData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];
    
    NSLog(@"pbData.length = %ld, jsonData.length = %ld", pbData.length, jsonData.length);
    
    NSError *error = nil;
    //将data类型的数据反序列化为Person类型的实例
    Person *newPerson = [Person parseFromData:pbData error:&error];
    if (error) {
        NSLog(@"error = %@", error);
    } else {
        NSLog(@"person = %p, %@ ... newPerson = %p , %@", person, person, newPerson, newPerson);
    }
}

@end

   运行结果如下图。由下图可知,相同的数据,通过PB转化成的NSData实例比JSON转化成的NSData实例所占用的空间少很多。
IOS版的MMKV框架解析,看这一篇就够了?_第2张图片

基础知识

二进制串

   对于C++而言,C++的字符串本身就可以被传输层使用,因为其本质上就是以 ‘\0’ 结尾的存储在内存中的二进制串。
   对于java而言,二进制串=字节数组=byte[]

Tag - Length - Value 的数据存储方式

   每一个数据都是以 Tag-Length-Value 方式表示,然后把所有数据拼接成一个字节流,最终实现数据存储和传输的功能。
IOS版的MMKV框架解析,看这一篇就够了?_第3张图片
   这种存储方式的优点是:①不需要分隔符就能分隔开字段,减少了分隔符的使用;②各字段存储的非常紧凑,存储空间利用率高;③若该字段没有值,则该字段在序列化后的二进制数据流中是完全不存在的,即该字段不会被编码进二进制串中。

在MMKV中,使用的是Length-Value的方式进行存储!!!!

两个结论
   1 PB是将消息(可以理解为一个类)里面的每一个字段编码后,再进行 T-L-V 数据存储方式进行数据的存储,最后形成一个二进制字节流。所以 序列化 = 数据编码 + 数据存储。
   2 PB对于不同的数据类型采用不同的 序列化方式(编码方式+数据存储方式)
IOS版的MMKV框架解析,看这一篇就够了?_第4张图片

PB相对于json的优点:

   1 压缩效率高,进而导致压缩后的数据体积变小;

  • 压缩效率高的原因:①采用了独特的编码方式,如Varint、Zigzag编码方式等;②采用T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑。

   2 因为编码时只是使用位移、按位&、| 、^等数学运算来实现编码,并结合TLV数据存储方式进行存储或者传输,进而导致序列化和反序列化的速度快,进而导致传输效率比json、xml也高一些.

PB相对于json的缺点:

   1 代码的可读性差;
   2 生成的文件较多,影响app的包体积, 并且需要使用PB这个第三方库。

CRC(Cyclic Redundancy Check,循环冗余校验)算法

   在数据通讯过程中,用于检测接收到的数据是否正确。所谓的校验就是在传输的数据中加入一些附加信息,这些附加信息用于检验接收方接收到的数据是否和发送发发送的数据是否相同。

奇偶校验算法((CRC-1))

   所谓奇偶校验就是在发送数据里面的每一个byte后面添加一个bit,使得每个byte的1的个数为奇数个或者偶数个。举例如下:
   比如我们要发送的字节是0x1a,其二进制表示为 0001 1010 。

  • ① 如果采用奇校验法,则在数据后面补0,接收方接收到的数据就变成了 0001 1010 0,即数据中的1的个数为3个(奇数个)。
  • ②如果采用偶数校验法,则在数据后面补1,接收方接收到的数据就变成了 0001 1010 1,即数据中的1的个数为4个(偶数个)。

   接收方根据接收到的数据中的1的个数是否满足奇偶性来校验数据是否正确。这种算法优点是简单,但缺点是对错误的检测率不高,并且会造成数据非常冗余。

累加和校验算法

   累加和校验算法是在一次通讯数据包的最后面添加一个byte的校验数据。这个byte的内容是前面的数据包中全部数据的忽略进位的按字节累加和。举例如下:
比如我们要传输的数据包的信息为 6、23、4 。那么加上校验和后的数据包的信息为 6、23、4、33 。这里的33是前面3个byte的校验和。接收方收到全部数据后对前3个数据进行同样的累加计算,如果累加和与数据包的最后一个字节相同的话就认为接收到的数据没有错误。这种算法优点是简单,但缺点是对错误的检测率不高。

求余校验算法(CRC-m)

   求余校验算法的基本思想是:把传输的数据当成一个位数很长的二进制数,然后把这个数除以另一个数(模2除法,并不是我们小学所学的除法),最后把得到的余数作为校验数据附加到原数据后面。举例:
把 6、23、4 看作一个很长的2进制数 00000110 00010111 00000010 ,假如除数是9,而9的二进制表示为 1001 。则模2除法运算为:

IOS版的MMKV框架解析,看这一篇就够了?_第5张图片
   从上图可以看到,运算结果的余数为1。如果我们将这个余数作为校验和的话,传输的数据用十进制表示为:6、23、4、1 。但具体的校验码是 001。因为余数是 1(是二进制的1而不是十进制的1),而校验码的长度是 (除数的长度 - 1),而除数(本例中为1001)的长度为4,所以余数1前面要补两个0,所以具体的校验码是001。然后拼接到要发送的数据后面,所以发送方最终发送的数据为:
00000110 00010111 00000010 001

   对于接收方来说,怎么验证接收到的 “00000110 00010111 00000010 001 ” 二进制串是否正确?答案是:将“00000110 00010111 00000010 001 ”二进制串 和 发送方的除数(本例中为1001)进行模2除法运算,然后发送运算结果的余数为0,说明接收到的数据没有问题。

总结求余校验算法的具体步骤:

   对于发送方,先做如下运算:

  • ①选定一个除数(网上的文献里面的多项式中的每一个项的指数的顺序组合),比如多项式为x8+x7+x6+x4+1,因为1等于2的0次幂,那么这个多项式对应的除数是 11101001 ;
  • ②把要发送的数据变成一个二进制串,并在这个二进制串的后面添加(除数的二进制位数的长度 - 1)个0,进而形成新的二进制串,这个新的二进制串就是被除数;
  • ③将被除数和除数进行模2除法运算,这个运算的过程就是一直进行异或运算,最终得到一个的余数;
  • ④如果这个余数对应的二进制串的长度等于(除数对应的二进制串的长度 - 1),那么请看第5步,而如果不等于,则在余数的二进制串的前面添加0,直到该二进制串的长度等于(除数对应的二进制串的长度 - 1);
  • ⑤把第④步得到的二进制串添加到要发送的数据的后面。
       对于接收方,把接收到的数据和 前面提到的除数(该除数和发送方计算用的除数相同)进行同样的模2除法运算。如果运算结果是0,则说明数据传输过程没有问题,而如果运算结果不是0,则说明传输过程有问题。

Mmap
   1 首先,请先放弃 虚拟内存 这个概念,这个概念在开发中没有意义。开发中只有虚拟空间的概念,进程看到的所有地址组成的空间,就是虚拟空间。虚拟空间是某个进程对分配给它的所有物理地址(已经分配的和将会分配的)重新映射。
   2 mmap的作用,是在应用这一层,是让你把文件的某一段,当做内存一样来访问。至于内核和驱动是如何实现的,性能高不高这些问题,不管你的事。你给予功能决定怎么用它就好了,别想太多。
   3 mmap的工作原理:当你发起这个调用的时候,它只是在你的虚拟空间中分配了一段空间,连真实的物理地址都不会分配的。当你访问这段空间,CPU会陷入OS内核执行异常处理(切换到内核态),然后异常处理会在这个时间分配物理内存,并用文件的内容填充这块内存,然后才返回你进程的上下文(切换到用户态),这时你的程序才会感知到这块内存里面有数据。
   4 驱动每次读入多少页面, 页面的分配算法等等,都不是系统对你的承诺,所以不能作为你写代码的依赖。这就是前面所说的:别想太多!

   mmap实现进程间通信的原理如下图。
IOS版的MMKV框架解析,看这一篇就够了?_第6张图片

   当调用mmap函数后,进程的某块虚拟地址空间和磁盘上的文件内容空间一一对应。但此时物理内存上面还没有磁盘上该映射文件的任何内容。
IOS版的MMKV框架解析,看这一篇就够了?_第7张图片

   只有当进程读取文件内容时,操作系统才会把磁盘上的文件内容加载到内存里面以供进程访问。
IOS版的MMKV框架解析,看这一篇就够了?_第8张图片

注意:mmap()函数在MMKV的作用是:你只需要往映射内存里面写入数据就可以了,操作系统会帮你把写入的数据同步到映射文件中(这里的映射文件指的是沙盒中的mmkv文件)。

以上内容均来自网上,来自文章底部的参考文献

MMKV

   下图是MMKV内部的主要类的关系图。

MMKV的initialize()方法源码解析

   我们知道,在OC中,一个类的initialize()方法只会在该类第一次收到消息时会被调用。如下图所示,initialize()方法只是初始化全局变量(g_instanceDic、g_instanceLock和DEFAULT_MMAP_SIZE)、监听app进入后台和进入活跃状态。
IOS版的MMKV框架解析,看这一篇就够了?_第9张图片

CScopedLock类

   如下图所示,CScopedLock类只有一个类型为NSRecursiveLock的成员变量m_oLock,该类的构造方法用于调用[m_oLock lock]来获取锁,目的是保证线程安全。而该类的析构函数用于调用[m_oLock unlock]来释放锁。该类实现了类似java的synchronize的功能。使用场景请看上一张图(initialize()方法源码解析)里面的didEnterBackground()方法。当调用CScopedLock lock(g_instanceLock) 时,会调用g_instanceLock的lock()方法来获取锁,确保线程安全,因为lock变量是局部变量,所以当didEnterBackground()方法结束后,lock局部变量就会被释放,此时CScopedLock类的析构函数就会被调用,而该析构函数会调用g_instanceLock的unlock()方法来释放锁。
IOS版的MMKV框架解析,看这一篇就够了?_第10张图片

MMKV的defaultMMKV()方法源码解析

   下图是defaultMMKV方法的执行流程的时序图
IOS版的MMKV框架解析,看这一篇就够了?_第11张图片
   下图是上图所示的mmkv.default文件和mmkv.default.crc文件所在的目录。如上图所示,在3.2.1步骤之前,这两个文件都为空,即大小为0B。当执行完3.2.1步骤后,mmkv.default.crc文件变成4KB。当执行完3.2.2之后,文件大小为4KB(即4096Byte)。
IOS版的MMKV框架解析,看这一篇就够了?_第12张图片

MMKV的setLogLevel()方法源码解析

   以 在ViewController的viewDidLoad()中调用[MMKV setLogLevel:MMKVLogInfo];为例分析其内部实现过程。时序图如下:
IOS版的MMKV框架解析,看这一篇就够了?_第13张图片

   CScopedLock类的源码如下:

class CScopedLock {
    NSRecursiveLock *m_oLock; //1

public:
    CScopedLock(NSRecursiveLock *oLock) : m_oLock(oLock) { [m_oLock lock]; } //2

    ~CScopedLock() { //3
        [m_oLock unlock];
        m_oLock = nil;
    }
};

   下面对上述代码进行讲解:

  • 1(1代表上面代码的注释1) m_oLock是递归锁。
  • 2 声明一个CScopedLock类型的变量或者创建一个CScopedLock对象时,会调用CScopedLock的构造函数,而该构造函数里面会调用lock()来获取锁,目的是保证线程安全。
  • 3 当CScopedLock变量或者CScopedLock对象被销毁时,其析构函数 ~CScopedLock() 会被调用,该析构函数里面会通过调用unlock()方法来释放锁。

   setLogLevel()方法的源码如下:

+ (void)setLogLevel:(MMKVLogLevel)logLevel {
        CScopedLock lock(g_instanceLock); //1
        g_currentLogLevel = logLevel; //2
} //3

   下面对上述代码进行讲解:

  • 1 创建CScopedLock类型的lock局部变量时,CScopedLock lock的构造函数会被调用,而该构造函数里面会调用g_instanceLock的lock()方法,确保线程安全,g_instanceLock是在MMKV类的initialize()方法中初始化 。
  • 2 设置日志级别。
  • 3 执行到右花括号时,即方法调用结束时,lock变量被销毁,此时lock对于的CScopedLock的析构函数被调用,该析构函数会调用g_instanceLock的unlock()方法,进而释放锁。

MMKV的registerHandler()方法源码解析

   以 在ViewController的viewDidLoad()中调用 [MMKV registerHandler:self]; 为例分析其内部实现过程。时序图如下:
IOS版的MMKV框架解析,看这一篇就够了?_第14张图片

   registerHandler方法的源码如下:

+ (void)registerHandler:(id<MMKVHandler>)handler {
        CScopedLock lock(g_instanceLock); //1
        g_callbackHandler = handler; //2

        if ([g_callbackHandler respondsToSelector:@selector(mmkvLogWithLevel:file:line:func:message:)]) {
                g_isLogRedirecting = true;

                // some logging before registerHandler
                MMKVInfo(@"pagesize:%d", DEFAULT_MMAP_SIZE); //3
        }
} //4

   下面对上述代码进行讲解:

  • 1 创建CScopedLock类型的lock局部变量时,CScopedLock lock的构造函数会被调用,而该构造函数里面会调用g_instanceLock的lock()方法,确保线程安全,g_instanceLock是在MMKV类的initialize()方法中初始化 。
  • 2 保存MMKVHandler实例(本例中指的是ViewController实例,因为该VC实现了MMKVHandler协议)到g_callbackHandler全局变量中。
  • 3 该宏最终会调用到ViewController的mmkvLogWithLevel:file:line:func:message:方法,进而输出日志到终端。
  • 4 当CScopedLock变量或者CScopedLock对象被销毁时,其析构函数 ~CScopedLock() 会被调用,该析构函数里面会通过调用unlock()方法来释放锁。

MMKV的setMMKVBasePath()方法源码解析

   以 在ViewController的viewDidLoad()中调用 [MMKV setMMKVBasePath:rootDir]; 为例分析其内部实现过程。setMMKVBasePath()方法的源码实现如下,比较简单。

+ (void)setMMKVBasePath:(NSString *)basePath {
        if (basePath.length > 0) {
                g_basePath = basePath; //保存到全局变量中
                MMKVInfo(@"set MMKV base path to: %@", g_basePath); //输出日志到控制台
        }
}

MMKV的mmkvWithID: relativePath:()方法源码解析

   以 在ViewController的viewDidLoad()中调用 [MMKV mmkvWithID:@“test/case1” relativePath@“自定义path”] 为例分析其内部实现过程。
IOS版的MMKV框架解析,看这一篇就够了?_第15张图片

MMKV的setBool: forKey:()方法源码解析

   以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] setBool:YES forKey:key] 为例分析其内部实现过程。

请先看大致的流程图:

IOS版的MMKV框架解析,看这一篇就够了?_第16张图片

接下来进行源码解析:

IOS版的MMKV框架解析,看这一篇就够了?_第17张图片
   上图是MMKV的 setBool:value forKey:key 方法的源码,接下来按照注释1、2、3、4解析,先看注释1的pbBoolSize()函数的实现。

  1. 下图1.1是pbBoolSize()函数的实现,可以看到,该函数直接返回1,因为BOOL类型的数据在PB编码方式中,只需要占用一个字节。
    在这里插入图片描述
图 1.1 pbBoolSize()函数
  1. MiniCodedOutputData类的定义如下图2.1所示。
  2. 该类有3个重要的成员变量,分别是m_ptr、m_size 和 m_position。在MMKV中,MiniCodedOutputData类有两个用途:第①个用途就是往映射内存(所谓映射内存就是调用mmap()函数所指定的那块内存)里面写入数据;第②个用途就是往NSData里面写入数据。当使用第1个用途时,m_ptr保存的就是mmap()函数调用的返回值,即指向映射内存的首地址,m_size保存的就是映射内存的大小,m_position就是相对于m_ptr的偏移量,所以m_position表示的是 映射内存里面已经保存的数据的末尾位置。当使用第2个用途时,m_ptr保存的就是NSData.bytes(即NSData类里面的bytes成员变量,该变量是一个byte类型的数组),即指向NSData.bytes的首地址,m_size保存的就是NSData.bytes数组的大小,m_position就是相对于m_ptr的偏移量,所以m_position表示的是 NSData.bytes数组的某个元素下标。
  3. MiniCodedOutputData类的函数基本都是把各种类型(比如BOOL类型)的数据通过PB规范中的某种编码方式 写入到映射内存或者NSData中。图2.2是MiniCodedOutputData类的部分函数实现
    IOS版的MMKV框架解析,看这一篇就够了?_第18张图片
图 2.1 MiniCodedOutputData类

IOS版的MMKV框架解析,看这一篇就够了?_第19张图片

图 2.2 MiniCodedOutputData类的部分函数实现
  1. 调用MiniCodedOutputData类的writeBool()函数把value写入到映射内存中,writeBool()函数的实现如下图3.1所示,如果value是YES,那么传的参数就是1。图3.2是把value写入到映射文件中,同时m_position++。
    在这里插入图片描述
图 3.1 writeBool()函数的实现

在这里插入图片描述

图 3.2 writeRawByte()函数的实现
  1. setRawData:data forKey:key方法如下图4.1所示。第①步是获取m_lock锁;第②步比较核心,在后面会讲;第③步是把 key和包含了value的data 保存到m_dic字典中,方便之后对key的读取,m_dic字典就是一种缓存机制的实现,以空间换时间;第4步是释放m_lock锁。
    IOS版的MMKV框架解析,看这一篇就够了?_第20张图片
图 4.1 setRawData:data forKey:key方法的实现

appendData:data forKey:key 方法的实现如下图5。
IOS版的MMKV框架解析,看这一篇就够了?_第21张图片

图 5 appendData:data forKey:key 方法的实现

   1. key和key的长度(keyLength)都需要保存,所以需要计算key所占用的字节数和keyLength所占用的字节数。keyLength是通过PB协议中的Varint编码方式来存储,所以keyLength所占用的字节数通过pbRawVarint32Size()函数来计算,该函数的实现如下图1.1。
IOS版的MMKV框架解析,看这一篇就够了?_第22张图片

图 1.1 pbRawVarint32Size()函数
   2. data(里面存的是你所要保存的value)和data的长度(data.length)都需要被保存到映射内存里面,所以需要计算data所占用的字节数和data.length所占用的字节数。占用字节数的计算方式和上面的keyLength同理。
   3. 确保映射内存是否有足够的存储空间。因为通过append的方式会导致文件不断增大,因此该方法在性能和空间做了一个折中:以pagesize为单位申请空间,在空间用完之前都是append方式;当append到文件末尾时,会对文件进行文件的key排重,尝试序列化保存排重结果;如果排重空间还是不够用的话,就将文件扩大一倍,直到空间足够为止。
   4. writeActualSize()方法的实现如下图,其作用是:把实际内容的大小(m_actualSize + size)写入到mmkv文件的前4个byte里面(这4个byte的内容就是m_actualSize的值),然后更新m_actualSize的值,即m_actualSize始终表示mmkv文件中的实际内容的大小(该大小并不包含 存放m_actualSize所需要的4个byte,即不包含mmkv文件的前4个byte)。

IOS版的MMKV框架解析,看这一篇就够了?_第23张图片

图 4.1writeActualSize()方法的实现
   在上图4.1中,通过调研output.writeFixed32((int32_t) actualSize);来把actualSize的值写入到映射内存的前4个字节。writeFixed32()函数的实现如下图4.2,writeRawLittleEndian32()函数是把value以小端字节序的方式写入m_ptr所指向的内存中。

IOS版的MMKV框架解析,看这一篇就够了?_第24张图片

图4.2 writeFixed32()函数的实现

   5. 通过调用writeString()函数(如下图5.1所示)先把key的长度按照PB协议的Varint编码方式写入到映射内存中,然后把key写入到映射内存中。由于这块映射内存是与文件形成了映射的,所以这块映射内存会被系统自动写到被映射的那个文件中。IOS版的MMKV框架解析,看这一篇就够了?_第25张图片

图 5.1writeString()函数的实现

   6. 通过调用writeData()函数(如下图6所示)先把data(data保存了你所设置的value)的长度按照PB协议的Varint编码方式写入到映射内存中,然后把key写入到映射内存中。由于这块映射内存是与文件形成了映射的,所以这块映射内存会被系统自动写到被映射的那个文件中。
IOS版的MMKV框架解析,看这一篇就够了?_第26张图片

图 6 writeData()函数

MMKV的setInt32: forKey:()方法源码解析

   以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] setBool:YES forKey:key] 为例分析其内部实现过程。

请先看大致的流程图:

IOS版的MMKV框架解析,看这一篇就够了?_第27张图片

接下来进行源码解析:

IOS版的MMKV框架解析,看这一篇就够了?_第28张图片

   上图是MMKV的setInt32: forKey:方法的源码,接下来按照注释1、2、3、4解析,先看注释1的pbBoolSize()函数的实现。

  1. 下图1.1是pbInt32Size()函数的实现,可以看到,当value为自然数时,会调用pbRawVarint32Size()函数(下图1.2所示),该函数的返回值就是 value通过以PB规范中的Varint编码方式(在前面的ProtocolBuffer中介绍)进行编码时 所需要的字节数。
    IOS版的MMKV框架解析,看这一篇就够了?_第29张图片
图 1.1 pbInt32Size()函数

IOS版的MMKV框架解析,看这一篇就够了?_第30张图片

图 1.2 pbRawVarint32Size()函数
  1. MiniCodedOutputData类的定义如下图2.1所示。
        a. 该类有3个重要的成员变量,分别是m_ptr、m_size 和 m_position。在MMKV中,MiniCodedOutputData类有两个用途:第①个用途就是往映射内存(所谓映射内存就是调用mmap()函数所指定的那块内存)里面写入数据;第②个用途就是往NSData里面写入数据。当使用第1个用途时,m_ptr保存的就是mmap()函数调用的返回值,即指向映射内存的首地址,m_size保存的就是映射内存的大小,m_position就是相对于m_ptr的偏移量,所以m_position表示的是 映射内存里面已经保存的数据的末尾位置。当使用第2个用途时,m_ptr保存的就是NSData.bytes(即NSData类里面的bytes成员变量,该变量是一个byte类型的数组),即指向NSData.bytes的首地址,m_size保存的就是NSData.bytes数组的大小,m_position就是相对于m_ptr的偏移量,所以m_position表示的是 NSData.bytes数组的某个元素下标。
        b. MiniCodedOutputData类的函数基本都是把各种类型(比如BOOL类型)的数据通过PB规范中的某种编码方式 写入到映射内存或者NSData中。图2.2是MiniCodedOutputData类的部分函数实现

IOS版的MMKV框架解析,看这一篇就够了?_第31张图片

图 2.1 MiniCodedOutputData类

IOS版的MMKV框架解析,看这一篇就够了?_第32张图片

图 2.2 MiniCodedOutputData类的部分函数实现
  1. 通过调用writeInt32()函数将value以PB协议中的Varint编码方式写入到NSData.bytes数组中。writeInt32()函数的源码如下图3.
    IOS版的MMKV框架解析,看这一篇就够了?_第33张图片
图 3 writeInt32()函数的实现
  1. 通过setRawData:data forKey:key 方法将key和data(data里面包含了你要保存的value)写入到映射内存中,该方法在前面的MMKV的setBool: forKey:()方法源码解析中已经介绍,这里不再赘述。

MMKV的getBoolForKey方法源码解析

    以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] getBoolForKey:key] 为例分析其内部实现过程。

请先看大致的流程图:

IOS版的MMKV框架解析,看这一篇就够了?_第34张图片

接下来进行源码解析:

IOS版的MMKV框架解析,看这一篇就够了?_第35张图片
    上图是MMKV的 getBoolForKey:key 方法的源码,接下来按照注释1、2、3解析,先看注释1的getRawDataForKey方法的实现。

  1. 下图1.1是getRawDataForKey方法的实现,具体解析在图中。
    IOS版的MMKV框架解析,看这一篇就够了?_第36张图片
图 1.1 getRawDataForKey方法的实现
  1. MiniCodedInputData类的定义如下图2.1所示。
        a. 该类的作用是以PB协议中的某种编码方式(比如Varint编码方式)解析NSData中的数据。有3个重要的成员变量,分别是m_ptr、m_size 和 m_position。在MMKV中,MiniCodedInputData类有1个用途:往NSData里面写入数据。m_ptr保存的就是NSData.bytes(即NSData类里面的bytes成员变量,该变量是一个byte类型的数组),即指向NSData.bytes的首地址,m_size保存的就是NSData.bytes数组的大小,m_position就是相对于m_ptr的偏移量,所以m_position表示的是 NSData.bytes数组的某个元素下标。
        b. MiniCodedInputData类的函数基本都是把各种类型(比如BOOL类型)的数据通过PB规范中的某种编码方式解析出NSData.bytes数组中的数据。图2.2是MiniCodedInputData类的部分函数实现.
    IOS版的MMKV框架解析,看这一篇就够了?_第37张图片
图 2.1 MiniCodedInputData类

IOS版的MMKV框架解析,看这一篇就够了?_第38张图片

图 2.2 MiniCodedInputData类的部分函数实现
  1. 下图是readBool()函数的实现。因为本例中获取的是BOOL类型的value,而BOOL类型的value只有0和1 这两种取值,所以value肯定小于127,所以在下图的第64行直接返回结果。

IOS版的MMKV框架解析,看这一篇就够了?_第39张图片

图 3 readBool()函数的实现

MMKV的getInt32ForKey方法源码解析

    以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] getInt32ForKey:key] 为例分析其内部实现过程。

请先看大致的流程图:

IOS版的MMKV框架解析,看这一篇就够了?_第40张图片

接下来进行源码解析:

IOS版的MMKV框架解析,看这一篇就够了?_第41张图片

   上图是MMKV的 getInt32ForKey:key 方法的源码,由于里面的代码在前面的getBoolForKey源码解析中已经解析过,这里不再赘述,接下来解析上图中的注释1,先看注释1的readInt32方法的实现。

  1. 下图1是readInt32方法的实现,具体解析在图中。
    IOS版的MMKV框架解析,看这一篇就够了?_第42张图片
图 1 readInt32方法的实现

MMKV的prepareMetaFile方法源码解析

   prepareMetaFile方法的源码如下。prepareMetaFile方法主要读取mmkv文件对应的crc文件的内容,并通过调用mmap()函数来建立内存和crc文件的映射,将这块映射内存的首地址保存在m_metaFilePtr变量中。在MMKV中,每生成一个mmkv文件时都会生成一个对应的crc文件。

- (void)prepareMetaFile {
        if (m_metaFilePtr == nullptr || m_metaFilePtr == MAP_FAILED) {
                if (!isFileExist(m_crcPath)) { //如果crc文件不存在就创建
                        createFile(m_crcPath);
                }
                m_metaFd = open(m_crcPath.UTF8String, O_RDWR, S_IRWXU); //打开crc文件(mmkv文件对应的crc文件,每生成一个mmkv文件时都会生成一个对应的crc文件)
                if (m_metaFd < 0) {
                        MMKVError(@"fail to open:%@, %s", m_crcPath, strerror(errno));
                        removeFile(m_crcPath);
                } else {
                        size_t size = 0;
                        struct stat st = {};
                        if (fstat(m_metaFd, &st) != -1) { //获取文件大小
                                size = (size_t) st.st_size;
                        }
                        int fileLegth = CRC_FILE_SIZE;
                        if (size != fileLegth) {
                                size = fileLegth;
                                if (ftruncate(m_metaFd, size) != 0) { //指定 文件m_metaFd 的大小为CRC_FILE_SIZE(默认是4096)字节
                                        MMKVError(@"fail to truncate [%@] to size %zu, %s", m_crcPath, size, strerror(errno));
                                        close(m_metaFd);
                                        m_metaFd = -1;
                                        removeFile(m_crcPath);
                                        return;
                                }
                        }
                        m_metaFilePtr = (char *) mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_metaFd, 0); //建立内存和crc文件的映射,将这块映射内存的首地址保存在m_metaFilePtr变量中
                        if (m_metaFilePtr == MAP_FAILED) {
                                MMKVError(@"fail to mmap [%@], %s", m_crcPath, strerror(errno));
                                close(m_metaFd);
                                m_metaFd = -1;
                        }
                }
        }
}

MMKV的loadFromFile方法源码解析

   loadFromFile方法的源码如下。loadFromFile方法先读取mmkv文件对应的crc文件的内容,然后建立mmkv文件和内存的映射关系,m_ptr保存的是这块映射内存的首地址,然后读取mmkv文件的前4个字节来获取mmkv文件的第5个字节开始到文件末尾的内容(即mmkv文件的实际内容)的大小,并把该大小保存在m_actualSize变量中,然后调用checkFileCRCValid方法来判断沙盒上的mmkv文件是否被损坏,如果文件没有损坏,就通过MiniPBCoder类的decodeContainerOfClass:(Class)cls withValueClass:(Class)valueClass fromData:方法把mmkv文件里面的实际内容(你之前设置的所有键值对)读取到m_dic字典中,最后把m_output指向mmkv文件内容结尾的下一个字节,以便后续的键值对的写入。在MMKV中,每生成一个mmkv文件时都会生成一个对应的crc文件。

- (void)loadFromFile {
        [self prepareMetaFile]; //读取mmkv文件对应的crc文件的内容,通过mmap()函数建立crc文件和内存的映射,并将这块映射内存的首地址保存在m_metaFilePtr变量中
        if (m_metaFilePtr != nullptr && m_metaFilePtr != MAP_FAILED) {
                m_metaInfo.read(m_metaFilePtr); //把crc文件里面的内容copy到m_metaInfo对象里面
        }
        if (m_cryptor) {
                if (m_metaInfo.m_version >= 2) {
                        m_cryptor->reset(m_metaInfo.m_vector, sizeof(m_metaInfo.m_vector));
                }
        }

        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;
                        }
                }
                m_ptr = (char *) mmap(nullptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); //建立mmkv文件和内存的映射关系,m_ptr保存的是这块映射内存的首地址
                if (m_ptr == MAP_FAILED) {
                        MMKVError(@"fail to mmap [%@], %s", m_mmapID, strerror(errno));
                } else {
                        const int offset = pbFixed32Size(0); //offset = 4;
                        NSData *lenBuffer = [NSData dataWithBytesNoCopy:m_ptr length:offset freeWhenDone:NO]; //lenBuffer保存的是映射内存的前4个byte,即保存的是映射文件的前4个byte。
                        @try {
                                m_actualSize = MiniCodedInputData(lenBuffer).readFixed32(); //读取m_path(假设为mmkv.default文件)文件的前4个byte,目的是获取该文件的实际内容的大小,如果之前还没有调用MMKV的setValue()时,该值为0
                        } @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) {
                                bool loadFromFile, needFullWriteback = false;
                                if (m_actualSize < m_size && m_actualSize + offset <= m_size) {
                                        if ([self checkFileCRCValid] == YES) { // 返回YES说明mmkv文件经过crc校验后,发现该文件没有被损坏,即说明该文件可用
                                                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 writeActualSize:m_size - offset];
                                                }
                                        }
                                }
                                if (loadFromFile) {
                                        MMKVInfo(@"loading [%@] with crc %u sequence %u", m_mmapID, m_metaInfo.m_crcDigest, m_metaInfo.m_sequence);
                                        NSData *inputBuffer = [NSData dataWithBytesNoCopy:m_ptr + offset length:m_actualSize freeWhenDone:NO]; //把文件中的数据读取到缓冲区(inputBuffer)里
                                        if (m_cryptor) { //如果之前把数据加过密,则现在把数据解密
                                                inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
                                        }
                                        m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer]; //1  将inputBuffer进行反序列化(PB的反序列化)操作,将反序列化的结果放到m_dic字典里面
                                        m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize); //指向所映射的mmkv文件的内容的末尾
                                        if (needFullWriteback) {
                                                [self fullWriteBack];
                                        }
                                } else {
                                        [self writeActualSize:0];
                                        m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
                                        [self recaculateCRCDigest];
                                }
                        } else {
                                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;
}

   上面diam的注释1 的[MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer]的源码实现如下图所示。
IOS版的MMKV框架解析,看这一篇就够了?_第43张图片

   上图的的注释2 所做的事情是:解析oData(mmkv的实际内容,即你之前设置的键值对)成一个保存了之前设置的键值对的字典。

IOS版的MMKV框架解析,看这一篇就够了?_第44张图片
   上面的注释3的源码实现如下图

IOS版的MMKV框架解析,看这一篇就够了?_第45张图片

MMKV的ensureMemorySize方法源码解析

- (BOOL)ensureMemorySize:(size_t)newSize { //里面写入了mmkv文件中的第5个字节的内容,该内容未知,第6个字节开始才是存储你所设置的key和value以及它们的长度。
        [self checkLoadData];

        if (![self isFileValid]) { // 检查mmkv文件是否可用
                MMKVWarning(@"[%@] file not valid", m_mmapID);
                return NO;
        }

        // make some room for placeholder
        constexpr uint32_t /*ItemSizeHolder = 0x00ffffff,*/ ItemSizeHolderSize = 4;
        if (m_dic.count == 0) {
                newSize += ItemSizeHolderSize;
        }
        if (newSize >= m_output->spaceLeft() || m_dic.count == 0) { //2 若空间不够,则尝试一次文件重整,将所有数据序列化后进行了一次计算
                // try a full rewrite to make space
                static const int offset = pbFixed32Size(0);
                NSData *data = [MiniPBCoder encodeDataWithObject:m_dic];//把m_dic里面的所有键值都保存在data中。
                size_t lenNeeded = data.length + offset + newSize;
                size_t avgItemSize = lenNeeded / std::max<size_t>(1, m_dic.count);
                size_t futureUsage = avgItemSize * std::max<size_t>(8, m_dic.count / 2);
                // 1. no space for a full rewrite, double it
                // 2. or space is not large enough for future usage, double it to avoid frequently full rewrite
                if (lenNeeded >= m_size || (lenNeeded + futureUsage) >= m_size) { //如果存储空间不够,则将文件扩容
                        size_t oldSize = m_size;
                        do {
                                m_size *= 2;
                        } while (lenNeeded + futureUsage >= m_size);  // 每次空间扩大两倍,直到数据能够写入
                        MMKVInfo(@"extending [%@] file size from %zu to %zu, incoming size:%zu, future usage:%zu",
                                 m_mmapID, oldSize, m_size, newSize, futureUsage);

                        // if we can't extend size, rollback to old state
                        if (ftruncate(m_fd, m_size) != 0) { //改变文件大小
                                MMKVError(@"fail to truncate [%@] to size %zu, %s", m_mmapID, m_size, strerror(errno));
                                m_size = oldSize;
                                return NO;
                        }

                        if (munmap(m_ptr, oldSize) != 0) { //释放之前的映射
                                MMKVError(@"fail to munmap [%@], %s", m_mmapID, strerror(errno));
                        }
                        m_ptr = (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0); //4 重新建立mmkv文件和内存的映射
                        if (m_ptr == MAP_FAILED) {
                                MMKVError(@"fail to mmap [%@], %s", m_mmapID, strerror(errno));
                        }

                        // check if we fail to make more space
                        if (![self isFileValid]) {
                                MMKVWarning(@"[%@] file not valid", m_mmapID);
                                return NO;
                        }
                        // keep m_output consistent with m_ptr -- writeAcutalSize: may fail
                        delete m_output;
                        m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
                        m_output->seek(m_actualSize); //m_output执行mmkv文件的实际内容结尾的下一个地址
                }

                if (m_cryptor) {
                        [self updateIVAndIncreaseSequence:KeepSequence];
                        m_cryptor->reset(m_metaInfo.m_vector, sizeof(m_metaInfo.m_vector));
                        auto ptr = (unsigned char *) data.bytes;
                        m_cryptor->encrypt(ptr, ptr, data.length);
                }

                if ([self writeActualSize:data.length] == NO) { //data里面保存的是m_dic字典里面的所有键值对
                        return NO;
                }

                delete m_output;
                m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset);
                BOOL ret = [self protectFromBackgroundWriting:m_actualSize
                                                   writeBlock:^(MiniCodedOutputData *output) {
                                                           output->writeRawData(data); //如果m_dic为空,则里面会写入mmkv文件中的第5个字节的内容,该内容未知,第6个字节开始才是存储你所设置的key和value以及它们的长度。
            //而如果m_dic不为空,即newSize >= m_output->spaceLeft(),会把m_dic里面的全部数据(你所设置的键值对)写入到mmkv文件中(从该文件的第5个字节开始写入)
                                                   }];
                if (ret) {
                        [self recaculateCRCDigest]; //重新计算mmkv文件的实际内容(从mmkv文件的第5个字节开始算起,直到文件末尾)的crc校验和,然后保存到mmkv文件对应的crc文件中。具体实现看下文
                }
                return ret;
        }
        return YES;
}

MMKV的recaculateCRCDigest方法和updateCRCDigest方法的源码解析

   recaculateCRCDigest方法的主要调用updateCRCDigest方法,而updateCRCDigest方法则是通过crc32算法来根据mmkv文件的实际内容计算出一个crc校验和,并把该crc校验和保存到mmkv文件对应的crc文件中,以便下一次进程启动时,进程可以根据这个crc校验和 和 根据所加载的mmkv文件内容所重新计算的crc校验和是否相同来判断mmkv文件是否被损坏,以便读取正确的mmkv文件。
IOS版的MMKV框架解析,看这一篇就够了?_第46张图片

MMKV文件的内容组织结构是怎么样的?如果多次保存相同的键值对时,内存和mmkv文件是怎么被保存的?

请看下面的Demo,先看test1方法。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self test1]; //多次给相同的key赋值时,mmkv文件里面会保存多份,因为内部是通过append进行添加的,但是内部的m_dic字典里面只存在一份。
    
//    [self test2]; //当太多次给相同的key赋值时,一旦超过当前映射文件的大小,就会重新把m_dic字典里面的内容写入到mmkv文件里面,此时mmkv文件的实际内容会变小,但是该文件占用的磁盘大小不变
}

- (void)test1 {
    MMKV *mmkv = [MMKV defaultMMKV];
    for (int i = 0; i < 3; ++i) { //循环了3次
        [mmkv setInt32:2 forKey:@"a"];
    }
    
    NSLog(@"%s, a = %d", __func__, [mmkv getInt32ForKey:@"a"]);
}

- (void)test2 {
    MMKV *mmkv = [MMKV defaultMMKV];
    for (int i = 0; i < 10000; ++i) { //可以把遍历次数调整成1024试试,此时mmkv文件里面的实际内容会变小。
        [mmkv setInt32:2 forKey:@"a"];
    }
    
    NSLog(@"%s, a = %d", __func__, [mmkv getInt32ForKey:@"a"]);
}

   把断点打在getInt32ForKey方法上,然后运行,运行结果如下图1。m_dic字典里面只有一个键值对。
   此时再看下图2 的mmkv文件的内容,红色方框是文件开头的前4个byte,用来标识 你所保存的所有键值对的大小 + 1,就是前面源码里面所讲的m_actualSize变量的值,mmkv文件是以小端字节序的方式来保存,0d转换成10进制数是13。再看红色横线部分, 01标识字符串“a”的长度,61是16进制数,转换成十进制数是97,其ASCII码对应是a,接着的01标识value的长度,接着的02转换成十进制数是2,是你所设置value。接着又是01, 01标识字符串“a”的长度,61是16进制数,转换成十进制数是97,其ASCII码对应是a。因为本例中,通过3次循环调用[mmkv setInt32:2 forKey:@“a”]; 所以一共有12个字节,对应图2的横线部分。而红色方框和红色横线之间的 00 这个字节在每次调用[MiniPBCoder encodeDataWithObject:m_dic]时产生的,如果你知道这个字节的含义,请留言告诉我。

图 1 断点

IOS版的MMKV框架解析,看这一篇就够了?_第47张图片

图 2 mmkv文件的内容

请看上面的Demo,看test2方法,

   把断点打在MMKV.mm文件的ensureMemorySize方法上,然后运行,运行结果如下图3。此时m_dic字典里面只有一个键值对。虽然运行了多次[mmkv setInt32:2 forKey:@“a”]; 但是m_dic字典里面还是只有一个键值对,即内存占用并没有增加。此时再看下图4的mmkv文件的内容,发现保存了多次这个键值对。前面提到,MMKV是通过append方式来添加你所设置的键值对的。但是MMKV当append的数据超过某个阈值时(刚开始是4092字节,因为一个内存页的大小是4096字节,而其中的4个字节用来保存mmkv文件的实际大小,所以阈值是4092),就会触发ensureMemorySize方法的调用,此时该方法里面先把m_dic里面的所有键值对所占用的大小 写入到MMKV文件开头的前4个字节中,然后会把现在的m_dic字典里面的所有键值对重新写入到MMKV文件开头的第6个字节开始及以后的字节中

图 3

IOS版的MMKV框架解析,看这一篇就够了?_第48张图片

图 4 部分MMKV文件的截图

参考:

PB OC安装:https://www.jianshu.com/p/c17260b36928
protobuf原理:https://blog.csdn.net/carson_ho/article/details/70568606
CRC算法:https://blog.csdn.net/liyuanbhu/article/details/7882789
mmap:https://www.zhihu.com/question/48161206
mmap:https://juejin.im/post/5caaf564f265da24d60e9c8d
mmap:https://www.cnblogs.com/huxiao-tee/p/4660352.html
mmap:https://blog.csdn.net/yusiguyuan/article/details/23388771
Android mmkv:https://juejin.im/post/5d55284d6fb9a06aee362b07

你可能感兴趣的:(ios,MMKV)