PB是一种轻便的高效的结构化数据存储的格式。它适用于数据传输和数据存储的场景。
JSON和XML最终在网络上传输时都会将字符串转为二进制数据并进行传输的。
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工程里面,结果如下图。
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实例所占用的空间少很多。
对于C++而言,C++的字符串本身就可以被传输层使用,因为其本质上就是以 ‘\0’ 结尾的存储在内存中的二进制串。
对于java而言,二进制串=字节数组=byte[]
每一个数据都是以 Tag-Length-Value 方式表示,然后把所有数据拼接成一个字节流,最终实现数据存储和传输的功能。
这种存储方式的优点是:①不需要分隔符就能分隔开字段,减少了分隔符的使用;②各字段存储的非常紧凑,存储空间利用率高;③若该字段没有值,则该字段在序列化后的二进制数据流中是完全不存在的,即该字段不会被编码进二进制串中。
两个结论
1 PB是将消息(可以理解为一个类)里面的每一个字段编码后,再进行 T-L-V 数据存储方式进行数据的存储,最后形成一个二进制字节流。所以 序列化 = 数据编码 + 数据存储。
2 PB对于不同的数据类型采用不同的 序列化方式(编码方式+数据存储方式)
1 压缩效率高,进而导致压缩后的数据体积变小;
2 因为编码时只是使用位移、按位&、| 、^等数学运算来实现编码,并结合TLV数据存储方式进行存储或者传输,进而导致序列化和反序列化的速度快,进而导致传输效率比json、xml也高一些.
1 代码的可读性差;
2 生成的文件较多,影响app的包体积, 并且需要使用PB这个第三方库。
在数据通讯过程中,用于检测接收到的数据是否正确。所谓的校验就是在传输的数据中加入一些附加信息,这些附加信息用于检验接收方接收到的数据是否和发送发发送的数据是否相同。
所谓奇偶校验就是在发送数据里面的每一个byte后面添加一个bit,使得每个byte的1的个数为奇数个或者偶数个。举例如下:
比如我们要发送的字节是0x1a,其二进制表示为 0001 1010 。
接收方根据接收到的数据中的1的个数是否满足奇偶性来校验数据是否正确。这种算法优点是简单,但缺点是对错误的检测率不高,并且会造成数据非常冗余。
累加和校验算法是在一次通讯数据包的最后面添加一个byte的校验数据。这个byte的内容是前面的数据包中全部数据的忽略进位的按字节累加和。举例如下:
比如我们要传输的数据包的信息为 6、23、4 。那么加上校验和后的数据包的信息为 6、23、4、33 。这里的33是前面3个byte的校验和。接收方收到全部数据后对前3个数据进行同样的累加计算,如果累加和与数据包的最后一个字节相同的话就认为接收到的数据没有错误。这种算法优点是简单,但缺点是对错误的检测率不高。
求余校验算法的基本思想是:把传输的数据当成一个位数很长的二进制数,然后把这个数除以另一个数(模2除法,并不是我们小学所学的除法),最后把得到的余数作为校验数据附加到原数据后面。举例:
把 6、23、4 看作一个很长的2进制数 00000110 00010111 00000010 ,假如除数是9,而9的二进制表示为 1001 。则模2除法运算为:
从上图可以看到,运算结果的余数为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,说明接收到的数据没有问题。
对于发送方,先做如下运算:
Mmap
1 首先,请先放弃 虚拟内存 这个概念,这个概念在开发中没有意义。开发中只有虚拟空间的概念,进程看到的所有地址组成的空间,就是虚拟空间。虚拟空间是某个进程对分配给它的所有物理地址(已经分配的和将会分配的)重新映射。
2 mmap的作用,是在应用这一层,是让你把文件的某一段,当做内存一样来访问。至于内核和驱动是如何实现的,性能高不高这些问题,不管你的事。你给予功能决定怎么用它就好了,别想太多。
3 mmap的工作原理:当你发起这个调用的时候,它只是在你的虚拟空间中分配了一段空间,连真实的物理地址都不会分配的。当你访问这段空间,CPU会陷入OS内核执行异常处理(切换到内核态),然后异常处理会在这个时间分配物理内存,并用文件的内容填充这块内存,然后才返回你进程的上下文(切换到用户态),这时你的程序才会感知到这块内存里面有数据。
4 驱动每次读入多少页面, 页面的分配算法等等,都不是系统对你的承诺,所以不能作为你写代码的依赖。这就是前面所说的:别想太多!
当调用mmap函数后,进程的某块虚拟地址空间和磁盘上的文件内容空间一一对应。但此时物理内存上面还没有磁盘上该映射文件的任何内容。
只有当进程读取文件内容时,操作系统才会把磁盘上的文件内容加载到内存里面以供进程访问。
下图是MMKV内部的主要类的关系图。
我们知道,在OC中,一个类的initialize()方法只会在该类第一次收到消息时会被调用。如下图所示,initialize()方法只是初始化全局变量(g_instanceDic、g_instanceLock和DEFAULT_MMAP_SIZE)、监听app进入后台和进入活跃状态。
如下图所示,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()方法来释放锁。
下图是defaultMMKV方法的执行流程的时序图
下图是上图所示的mmkv.default文件和mmkv.default.crc文件所在的目录。如上图所示,在3.2.1步骤之前,这两个文件都为空,即大小为0B。当执行完3.2.1步骤后,mmkv.default.crc文件变成4KB。当执行完3.2.2之后,文件大小为4KB(即4096Byte)。
以 在ViewController的viewDidLoad()中调用[MMKV setLogLevel:MMKVLogInfo];为例分析其内部实现过程。时序图如下:
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;
}
};
下面对上述代码进行讲解:
setLogLevel()方法的源码如下:
+ (void)setLogLevel:(MMKVLogLevel)logLevel {
CScopedLock lock(g_instanceLock); //1
g_currentLogLevel = logLevel; //2
} //3
下面对上述代码进行讲解:
以 在ViewController的viewDidLoad()中调用 [MMKV registerHandler:self]; 为例分析其内部实现过程。时序图如下:
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
下面对上述代码进行讲解:
以 在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); //输出日志到控制台
}
}
以 在ViewController的viewDidLoad()中调用 [MMKV mmkvWithID:@“test/case1” relativePath@“自定义path”] 为例分析其内部实现过程。
以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] setBool:YES forKey:key] 为例分析其内部实现过程。
上图是MMKV的 setBool:value forKey:key 方法的源码,接下来按照注释1、2、3、4解析,先看注释1的pbBoolSize()函数的实现。
appendData:data forKey:key 方法的实现如下图5。
1. key和key的长度(keyLength)都需要保存,所以需要计算key所占用的字节数和keyLength所占用的字节数。keyLength是通过PB协议中的Varint编码方式来存储,所以keyLength所占用的字节数通过pbRawVarint32Size()函数来计算,该函数的实现如下图1.1。
5. 通过调用writeString()函数(如下图5.1所示)先把key的长度按照PB协议的Varint编码方式写入到映射内存中,然后把key写入到映射内存中。由于这块映射内存是与文件形成了映射的,所以这块映射内存会被系统自动写到被映射的那个文件中。
6. 通过调用writeData()函数(如下图6所示)先把data(data保存了你所设置的value)的长度按照PB协议的Varint编码方式写入到映射内存中,然后把key写入到映射内存中。由于这块映射内存是与文件形成了映射的,所以这块映射内存会被系统自动写到被映射的那个文件中。
以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] setBool:YES forKey:key] 为例分析其内部实现过程。
上图是MMKV的setInt32: forKey:方法的源码,接下来按照注释1、2、3、4解析,先看注释1的pbBoolSize()函数的实现。
以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] getBoolForKey:key] 为例分析其内部实现过程。
上图是MMKV的 getBoolForKey:key 方法的源码,接下来按照注释1、2、3解析,先看注释1的getRawDataForKey方法的实现。
以 在ViewController的viewDidLoad()中调用 [ [MMKV defaultMMKV] getInt32ForKey:key] 为例分析其内部实现过程。
上图是MMKV的 getInt32ForKey:key 方法的源码,由于里面的代码在前面的getBoolForKey源码解析中已经解析过,这里不再赘述,接下来解析上图中的注释1,先看注释1的readInt32方法的实现。
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;
}
}
}
}
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]
的源码实现如下图所示。
上图的的注释2 所做的事情是:解析oData(mmkv的实际内容,即你之前设置的键值对)成一个保存了之前设置的键值对的字典。
- (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;
}
recaculateCRCDigest方法的主要调用updateCRCDigest方法,而updateCRCDigest方法则是通过crc32算法来根据mmkv文件的实际内容计算出一个crc校验和,并把该crc校验和保存到mmkv文件对应的crc文件中,以便下一次进程启动时,进程可以根据这个crc校验和 和 根据所加载的mmkv文件内容所重新计算的crc校验和是否相同来判断mmkv文件是否被损坏,以便读取正确的mmkv文件。
- (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]时产生的,如果你知道这个字节的含义,请留言告诉我。
把断点打在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个字节开始及以后的字节中
参考:
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