MMKV 源碼詳解

MMKV 簡介

MMKV——基于 mmap 的高性能通用 key-value 組件
MMKV 是基于 mmap 內存映射的 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。從 2015 年中至今在微信上使用,其性能和穩定性經過了時間的驗證。目前已移植到 Android / macOS / Win32 / POSIX 平臺,一并開源。

MMKV 實現方案

?   內存準備
通過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往里面寫數據,由操作系統負責將內存回寫到文件,不必擔心 crash 導致數據丟失。
?      數據組織
數據序列化方面我們選用 protobuf 協議,pb 在性能和空間占用上都有不錯的表現。
?   寫入優化
考慮到主要使用場景是頻繁地進行寫入更新,我們需要有增量更新的能力。我們考慮將增量 kv 對象序列化后,append 到內存末尾。
?   空間增長
使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。我們需要在性能和空間上做個折中。
  ? 數據有效性
考慮到文件系統、操作系統都有一定的不穩定性,另外增加了 crc 校驗,對無效數據進行甄別。在 iOS 微信現網環境上,有平均約 70 萬/日 次的數據校驗不通過。

MMAP 原理簡介

mmap是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用read,write等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。如下圖所示:


MMAP 原理.png

由上圖可以看出,進程的虛擬地址空間,由多個虛擬內存區域構成。虛擬內存區域是進程的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址范圍。上圖中所示的text數據段(代碼段)、初始數據段、BSS數據段、堆、棧和內存映射,都是一個獨立的虛擬內存區域。而為內存映射服務的地址空間處在堆棧之間的空余部分。

mmap 和常規文件操作的區別
常規文件操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內核空間,不能被用戶進程直接尋址,所以還需要將頁緩存中數據頁再次拷貝到內存對應的用戶空間中。寫操作也是一樣,待寫入的 buffer 在內核空間不能直接訪問,必須要先拷貝至內核空間對應的主存儲器,再寫回磁盤中(延遲寫回),也是需要兩次數據拷貝。

而使用 mmap 操作文件中,創建新的虛擬內存區域和建立文件磁盤地址和虛擬內存區域映射這兩步,沒有任何文件拷貝操作。而之后訪問數據時發現內存中并無數據而發起的缺頁異常過程,可以通過已經建立好的映射關系,只使用一次數據拷貝,就從磁盤中將數據傳入內存的用戶空間中,供進程使用。

mmap 使用細節
使用 mmap 需要注意的一個關鍵點是,mmap 映射區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是 4k 字節)。原因是,內存的最小粒度是頁,而進程虛擬地址空間和內存的映射也是以頁為單位。為了匹配內存的操作,mmap 從磁盤到虛擬地址空間的映射也必須是頁。

CRC校驗原理簡介

CRC(Cyclic Redundancy Check)原理:在 K 位信息碼(目標發送數據)后再拼接 R 位校驗碼,使整個編碼長度為 N 位,因此這種編碼也叫(N,K)碼。通俗的說,就是在需要發送的信息后面附加一個數(即校驗碼),生成一個新的發送數據發送給接收端。這個數據要求能夠使生成的新數據被一個特定的數使用模2除法(即異或運算)整除。


CRC校驗原理.png

ProtoBuf 編碼原理簡介

ProtoBuf 是 Google 出品的一種結構數據序列化方法,可簡單類比于 XML,其具有以下特點:

  1. 語言無關、平臺無關。即 ProtoBuf 支持 Java、C++、Python 等多種語言,支持多個平臺
  2. 高效。即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更為簡單
  3. 擴展性、兼容性好。你可以更新數據結構,而不影響和破壞原有的舊程序
  4. 采用 獨特編碼方式 & T - L - V 的數據存儲方式。即 Tag - Length - Value,標識 - 長度 - 字段值 存儲方式


    ProtoBuf 編碼原理.png

Varints 編碼的規則

  1. 在每個字節開頭的 bit 設置了 msb(most significant bit ),標識是否需要繼續讀取下一個字節
  2. 存儲數字對應的二進制補碼
  3. 補碼的低位排在前面
    int32 val = 666; // 設置一個 int32 的字段的值 val = 666; 這時編碼的結果如下
    原碼:000 ... 101 0011010 // 666 的源碼
    補碼:000 ... 101 0011010 // 666 的補碼
    Varints 編碼:1#0011010 0#000 0101 (9a 05) // 666 的 Varints 編碼
    編碼數字 666,Varints 只使用了 2 個字節。而正常情況下 int32 需要使用 4 個字節

Wire_Type

Wire_Type.png

String 類型編碼

message Test {
    required string str = 2;
}
Test.setStr(“testing”)
// 經過protobuf編碼序列化后的數據以二進制的方式輸出
// 輸出為:18, 7, 116, 101, 115, 116, 105, 110, 103
String 類型編碼.png

Protobuf 協議在 MMKV 中 MiniCodedOutputData 實現的差異

  1. Protobuf 采用 獨特編碼方式 & Tag - Length - Value 的數據存儲方式。MiniCodedOutputData 采用的是 Length - Value 的數據存儲方式。MiniCodedOutputData 不需要 Tag (field_number+wire_type) 是因為: key-value 不需要唯一標識,用戶讀或寫 key-value 時,是知道 value 的類型的,因此不需要通過 tag 讀取。
  2. Protobuf 推薦對負數使用 sint32 或 sint64 編碼,可以減少字節使用。MiniCodedOutputData 對負數統一使用 Varints 編碼,分配 10 字節。

源碼概覽(MMKV version:1.0.23)

[MMKV defaultMMKV]

defaultMMKV.png

ViewController.mm

- (void)viewDidLoad {
    [super viewDidLoad];
    // 設置日志級別,只有當輸出日志的級別高于或等于此日志級別時,才會輸出日志
    [MMKV setLogLevel:MMKVLogInfo];

    // 日志級別為 MMKVLogNone 時,不輸出日志,此級別是最高日志級別
    //[MMKV setLogLevel:MMKVLogNone];

    // register handler
    [MMKV registerHandler:self];

    // not necessary: set MMKV's root dir
    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES);
    NSString *libraryPath = (NSString *) [paths firstObject];
    if ([libraryPath length] > 0) {
        NSString *rootDir = [libraryPath stringByAppendingPathComponent:@"mmkv"];
        [MMKV setMMKVBasePath:rootDir];
    }

    [self funcionalTest];
    [self testReKey];
    //[self testImportFromUserDefault];
    //[self testCornerSize];
    //[self testFastRemoveCornerSize];

    DemoSwiftUsage *swiftUsageDemo = [[DemoSwiftUsage alloc] init];
    [swiftUsageDemo testSwiftFunctionality];

    m_loops = 10000;
    m_arrStrings = [NSMutableArray arrayWithCapacity:m_loops];
    m_arrStrKeys = [NSMutableArray arrayWithCapacity:m_loops];
    m_arrIntKeys = [NSMutableArray arrayWithCapacity:m_loops];
    for (size_t index = 0; index < m_loops; index++) {
        NSString *str = [NSString stringWithFormat:@"%s-%d", __FILE__, rand()];
        [m_arrStrings addObject:str];

        NSString *strKey = [NSString stringWithFormat:@"str-%zu", index];
        [m_arrStrKeys addObject:strKey];

        NSString *intKey = [NSString stringWithFormat:@"int-%zu", index];
        [m_arrIntKeys addObject:intKey];
    }
}

[MMKV setLogLevel:MMKVLogInfo] 詳解

MMKV.mm

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

此方法用來設置日志級別,只有當輸出日志的級別高于或等于此日志級別時,才會輸出日志,代碼實現在文件 MMKVLog.mm 中,如下所示:

void _MMKVLogWithLevel(MMKVLogLevel level, const char *file, const char *func, int line, NSString *format, ...) {
    if (level >= g_currentLogLevel) {
        // 輸出日志
    }
}

。 CScopedLock 類的實現在 ScopedLock.hpp 文件中。如下代碼所示:

class CScopedLock {
    NSRecursiveLock *m_oLock;

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

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

lock(g_instanceLock) 是構造方法,在此方法里初始化了一個遞歸鎖,并調用遞歸鎖的 lock 方法,起到安全的輸出日志的功能。構造方法中的傳參 g_instanceLock 是一個全局的靜態變量,在 MMKV.mm 文件的 + (void)initialize 方法中賦值為一個遞歸鎖實例對象。

+ (void)initialize {
    if (self == MMKV.class) {
        ...
        g_instanceLock = [[NSRecursiveLock alloc] init];
        ...
    }
}

[MMKV registerHandler:self] 詳解

MMKV.mm

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

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

此方法注冊 self(即:ViewController) 為 MMKVHandler 協議的實現類,該協議的定義在文件 MMKVHandler.h 中,如下所示:

@protocol MMKVHandler <NSObject>
@optional

// by default MMKV will discard all datas on crc32-check failure
// return `MMKVOnErrorRecover` to recover any data on the file
- (MMKVRecoverStrategic)onMMKVCRCCheckFail:(NSString *)mmapID;

// by default MMKV will discard all datas on file length mismatch
// return `MMKVOnErrorRecover` to recover any data on the file
- (MMKVRecoverStrategic)onMMKVFileLengthError:(NSString *)mmapID;

// by default MMKV will print log using NSLog
// implement this method to redirect MMKV's log
- (void)mmkvLogWithLevel:(MMKVLogLevel)level file:(const char *)file line:(int)line func:(const char *)funcname message:(NSString *)message;

@end

[MMKV setMMKVBasePath:rootDir] 詳解

MMKV.mm

+ (void)setMMKVBasePath:(NSString *)basePath {
    if (basePath.length > 0) {
        g_basePath = basePath;
        ...
    }
}

此方法用于設置 key-value 存儲的根路徑,可以不必設置,默認的存儲根路徑在 Document/mmkv 目錄下,代碼如下:

static NSString *g_basePath = nil;
+ (NSString *)mmkvBasePath {
    if (g_basePath.length > 0) {
        return g_basePath;
    }

    NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString *documentPath = (NSString *) [paths firstObject];
    if ([documentPath length] > 0) {
        g_basePath = [documentPath stringByAppendingPathComponent:@"mmkv"];
        return g_basePath;
    } else {
        return @"";
    }
}

[self funcionalTest] 詳解

ViewController.mm

- (void)funcionalTest {
        // MMKV *mmkv = [MMKV defaultMMKV];
    auto path = [MMKV mmkvBasePath];
    path = [path stringByDeletingLastPathComponent];
    path = [path stringByAppendingPathComponent:@"mmkv_2"];
    auto mmkv = [MMKV mmkvWithID:@"test/case1" relativePath:path];

    [mmkv setBool:YES forKey:@"bool"];
    NSLog(@"bool:%d", [mmkv getBoolForKey:@"bool"]);
    ...
    [mmkv removeValueForKey:@"bool"];
    NSLog(@"bool:%d", [mmkv getBoolForKey:@"bool"]);
    [mmkv close];
}

此方法是獲取一個指定目錄下、特定 ID 的 MMKV 對象 mmkv,用該對象存儲、移除 key-value,及關閉 mmkv 對象。

close 關閉 mmkv 對象

/ call this method if the instance is no longer needed in the near future
// any subsequent call to the instance is undefined behavior
- (void)close;

auto mmkv = [MMKV mmkvWithID:@"test/case1" relativePath:path] 詳解

// mmapID: any unique ID (com.tencent.xin.pay, etc)
// if you want a per-user mmkv, you could merge user-id within mmapID
// relativePath: custom path of the file, `NSDocumentDirectory/mmkv` by default
+ (instancetype)mmkvWithID:(NSString *)mmapID relativePath:(nullable NSString *)path {
    return [self mmkvWithID:mmapID cryptKey:nil relativePath:path];
}

+ (instancetype)mmkvWithID:(NSString *)mmapID cryptKey:(NSData *)cryptKey relativePath:(nullable NSString *)relativePath {
    ...
    NSString *kvPath = [MMKV mappedKVPathWithID:mmapID relativePath:relativePath];
    ...
    NSString *kvKey = [MMKV mmapKeyWithMMapID:mmapID relativePath:relativePath];

    CScopedLock lock(g_instanceLock);
    MMKV *kv = [g_instanceDic objectForKey:kvKey];
    if (kv == nil) {
        kv = [[MMKV alloc] initWithMMapID:kvKey cryptKey:cryptKey path:kvPath];
        [g_instanceDic setObject:kv forKey:kvKey];
    }
    return kv;
}

此方法返回一個指定目錄下、特定 ID 的 MMKV 對象 mmkv。首先 [MMKV mappedKVPathWithID:mmapID relativePath:relativePath] 獲取 key-value 存儲路徑,然后 [MMKV mmapKeyWithMMapID:mmapID relativePath:relativePath] 構造 kvKey,并從靜態字典 static NSMutableDictionary *g_instanceDic 中,獲取一個 MMKV 對象 kv: [g_instanceDic objectForKey:kvKey] ,若字典中不存在該對象,則創建該對象,并設置到 g_instanceDic 字典中。
創建該對象的方法如下:

- (instancetype)initWithMMapID:(NSString *)kvKey cryptKey:(NSData *)cryptKey path:(NSString *)path {
    if (self = [super init]) {
        m_lock = [[NSRecursiveLock alloc] init];
        m_mmapID = kvKey;
        m_path = path;
        m_crcPath = [MMKV crcPathWithMappedKVPath:m_path];
        if (cryptKey.length > 0) {
            m_cryptor = new AESCrypt((const unsigned char *) cryptKey.bytes, cryptKey.length);
        }
        [self loadFromFile];
        ...
    }
    return self;
}
- (void) loadFromFile {
    [self prepareMetaFile];
    ...
}
- (void)prepareMetaFile {
    if (m_metaFilePtr == nullptr || m_metaFilePtr == MAP_FAILED) {
        if (!isFileExist(m_crcPath)) {
            createFile(m_crcPath);
        }
        m_metaFd = open(m_crcPath.UTF8String, O_RDWR, S_IRWXU);
        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) {
                    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);
            if (m_metaFilePtr == MAP_FAILED) {
                MMKVError(@"fail to mmap [%@], %s", m_crcPath, strerror(errno));
                close(m_metaFd);
                m_metaFd = -1;
            }
        }
    }
}

m_crcPath = [MMKV crcPathWithMappedKVPath:m_path] m_crcPath 是在 m_path 后拼接上 ".crc" 字符串。
loadFromFile 方法首先調用 prepareMetaFile 方法。 prepareMetaFile m_metaFilePtr 為內存映射的映射區的內存起始地址。若還沒進行內存映射,或映射失敗,則執行以下步驟,開始內存映射:

  1. 判斷文件是否存在,若不存在,則創建文件 createFile(m_crcPath)

  2. 以可讀、可寫的權限打開文件,得到文件描述符 m_metaFd = open(m_crcPath.UTF8String, O_RDWR, S_IRWXU)

  3. 如果 m_metaFd < 0,即文件打開失敗,則取消對該文件的鏈接 removeFile(m_crcPath) ,否則,獲取文件的屬性 fstat(m_metaFd, &st)

  4. 判斷文件的 size 是否等于 CRC_FILE_SIZE (即:getpagesize(), 系統的分頁大小),若不相等,則 size = fileLegth,并將文件大小改變為參數 size 指定的大小 ftruncate(m_metaFd, size)。為何要將 size 設為 getpagesize() ?原因是,內存的最小粒度是頁,而進程虛擬地址空間和內存的映射也是以頁為單位。為了匹配內存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。

  5. 若設置文件 size 失敗,則關閉文件 close(m_metaFd) ,并取消鏈接文件 removeFile(m_crcPath)。 否則,將文件映射到進程地址空間 m_metaFilePtr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, m_metaFd, 0) ,映射區域可被讀取、寫入、對映射區域的寫入數據會復制回文件內,而且允許其他映射該文件的進程共享

  6. 如果映射失敗,則關閉文件

loadFromFile
介紹完 loadFromFile 方法中的 prepareMetaFile 方法,我們繼續往下看代碼:

- (void) loadFromFile {
    [self prepareMetaFile];
    if (m_metaFilePtr != nullptr && m_metaFilePtr != MAP_FAILED) {
        m_metaInfo.read(m_metaFilePtr);
    }
    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);
        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) {
                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 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];
                    if (m_cryptor) {
                        inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
                    }
                    m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer];
                    m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize);
                    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;
}

上面的代碼歸納為以下幾個步驟:

  1. 讀取元文件的信息 m_metaInfo.read(m_metaFilePtr) ,內部使用 void * memcpy ( void * dest, const void * src, size_t num ) 函數,該函數的作用為:復制 src 所指的內存內容的前 num 個字節到 dest 所指的內存地址上。如果 MMKV 對象初始化的時,傳了 AES 加密的 key : cryptKey ,且當前的 m_metaInfo.m_version >= 2 時,則將元文件的 AES 加密重置 m_cryptor->reset(m_metaInfo.m_vector, sizeof(m_metaInfo.m_vector))
  2. 打開 m_path 路徑下的文件 m_fd ,給 m_fd 文件分配適合的 m_size ,然后進行內存映射。
  3. 從文件中讀取前 4 個字節 pbFixed32Size(0),將前四個字節翻轉后得到的字節 readFixed32() ,得到存儲的數據實際占用的空間 。
  4. 若數據為空 m_actualSize == 0,則重置 m_output m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset) ,并重新計算 crc32 校驗碼 [self recaculateCRCDigest]
  5. 若數據不為空 m_actualSize > 0 :
    • m_actualSize + offset <= m_size 時,如果通過了 crc32 校驗,則可以加載文件。否則, 如果代理對文件校驗失敗的處理策略為 MMKVOnErrorRecover ,則也可以加載文件,并且對文件重寫。
    • m_actualSize > m_size || m_actualSize + offset > m_size 時,如果文件長度錯誤,可以加載文件,并且對文件重寫。
  6. 加載文件的過程 loadFromFile : 讀取-->解密-->解碼-->重置輸出數據對象,如果需要對文件重寫,則執行重寫過程。
NSData *inputBuffer = [NSData dataWithBytesNoCopy:m_ptr + offset length:m_actualSize freeWhenDone:NO];
if (m_cryptor) {
    inputBuffer = decryptBuffer(*m_cryptor, inputBuffer);
}
m_dic = [MiniPBCoder decodeContainerOfClass:NSMutableDictionary.class withValueClass:NSData.class fromData:inputBuffer];
m_output = new MiniCodedOutputData(m_ptr + offset + m_actualSize, m_size - offset - m_actualSize);
if (needFullWriteback) {
    [self fullWriteBack];
}
  1. 不加載文件的話,則重置 m_output m_output = new MiniCodedOutputData(m_ptr + offset, m_size - offset) ,并重新計算 crc32 校驗碼 [self recaculateCRCDigest]
  2. m_pathm_crcPath 文件嘗試重置文件保護。

[mmkv setBool:YES forKey:@"bool"]

- (BOOL)setBool:(BOOL)value forKey:(NSString *)key {
    if (key.length <= 0) {
        return NO;
    }
    size_t size = pbBoolSize(value);
    NSMutableData *data = [NSMutableData dataWithLength:size];
    MiniCodedOutputData output(data);
    output.writeBool(value);

    return [self setRawData:data forKey:key];
}

[self setRawData:data forKey:key]
BOOL 值占 1 字節,寫入二進制文件。將 keydata 寫入內存映射文件 。若寫入成功,則更新內存里的 key-value 字典 m_dic

- (BOOL)setRawData:(NSData *)data forKey:(NSString *)key {
    if (data.length <= 0 || key.length <= 0) {
        return NO;
    }
    CScopedLock lock(m_lock);

    auto ret = [self appendData:data forKey:key];
    if (ret) {
        [m_dic setObject:data forKey:key];
        m_hasFullWriteBack = NO;
    }
    return ret;
}

- (BOOL)appendData:(NSData *)data forKey:(NSString *)key
keydata 寫入內存映射文件 的步驟:

  1. 計算 key 的 size, 以及為存儲 key 的 size 所占的 size;計算 value 的 size, 以及為存儲 value 的 size 所占的 size;
  2. 確保內存映射的空間足以存儲數據,若不足,則分配一個更大的空間,重新進行內存映射 [self ensureMemorySize:size]
  3. keydata 寫入內存映射文件。
  4. 寫入成功后,若需要加密,將剛寫入的數據進行加密。
  5. 更新 CRC32 驗證碼。
- (BOOL)appendData:(NSData *)data forKey:(NSString *)key {
    size_t keyLength = [key lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
    auto size = keyLength + pbRawVarint32Size((int32_t) keyLength); // size needed to encode the key
    size += data.length + pbRawVarint32Size((int32_t) data.length); // size needed to encode the value

    BOOL hasEnoughSize = [self ensureMemorySize:size];
    if (hasEnoughSize == NO || [self isFileValid] == NO) {
        return NO;
    }

    BOOL ret = [self writeActualSize:m_actualSize + size];
    if (ret) {
        ret = [self protectFromBackgroundWriting:size
                                      writeBlock:^(MiniCodedOutputData *output) {
                                          output->writeString(key);
                                          output->writeData(data); // note: write size of data
                                      }];
        if (ret) {
            static const int offset = pbFixed32Size(0);
            auto ptr = (uint8_t *) m_ptr + offset + m_actualSize - size;
            if (m_cryptor) {
                m_cryptor->encrypt(ptr, ptr, size);
            }
            [self updateCRCDigest:ptr withSize:size increaseSequence:KeepSequence];
        }
    }
    return ret;
}

[self ensureMemorySize:size]
官方說明 :使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增長得不可控。例如同一個 key 不斷更新的話,是可能耗盡幾百 M 甚至上 G 空間,而事實上整個 kv 文件就這一個 key,不到 1k 空間就存得下。這明顯是不可取的。我們需要在性能和空間上做個折中:以內存 pagesize 為單位申請空間,在空間用盡之前都是 append 模式;當 append 到文件末尾時,進行文件重整、key 排重,嘗試序列化保存排重結果;排重后空間還是不夠用的話,將文件擴大一倍,直到空間足夠。

- (BOOL)ensureMemorySize:(size_t)newSize {
    ...
    // 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) {
        // try a full rewrite to make space
        static const int offset = pbFixed32Size(0);
        NSData *data = [MiniPBCoder encodeDataWithObject:m_dic];
        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);
            ...
            m_ptr = (char *) mmap(m_ptr, m_size, PROT_READ | PROT_WRITE, MAP_SHARED, m_fd, 0);
            ...
        }

        ...
        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);
                                           }];
        if (ret) {
            [self recaculateCRCDigest];
        }
        return ret;
    }
    return YES;
}

[mmkv getBoolForKey:@"bool"]
key 獲取 bool 值的步驟:

  1. 檢查存儲的文件是否已經加載進內存 checkLoadData ,若未加載,則調用 loadFromFile 加載,加載成功后會存入字典 m_dict
  2. m_dict 中獲取編碼過的二進制數據;
  3. 解碼 input.readBool()。
- (BOOL)getBoolForKey:(NSString *)key {
    return [self getBoolForKey:key defaultValue:FALSE];
}
- (BOOL)getBoolForKey:(NSString *)key defaultValue:(BOOL)defaultValue {
    if (key.length <= 0) {
        return defaultValue;
    }
    NSData *data = [self getRawDataForKey:key];
    if (data.length > 0) {
        @try {
            MiniCodedInputData input(data);
            return input.readBool();
        } @catch (NSException *exception) {
            MMKVError(@"%@", exception);
        }
    }
    return defaultValue;
}
- (NSData *)getRawDataForKey:(NSString *)key {
    CScopedLock lock(m_lock);
    [self checkLoadData];
    return [m_dic objectForKey:key];
}
- (void)checkLoadData {
    //  CScopedLock lock(m_lock);

    if (m_needLoadFromFile == NO) {
        return;
    }
    m_needLoadFromFile = NO;
    [self loadFromFile];
}

[mmkv removeValueForKey:@"bool"]
移除 key-value 的過程:

  1. m_dic 字典調用 removeObjectForKey:key
  2. 將 key 對應的 value 寫入空二進制數據 ,如此在 getBoolForKey: 時,由于空二進制數據的 length 不大于零,從而返回默認值
- (BOOL)getBoolForKey:(NSString *)key defaultValue:(BOOL)defaultValue {
    ...
    NSData *data = [self getRawDataForKey:key];
    if (data.length > 0) { ... }
    return defaultValue;
}
- (void)removeValueForKey:(NSString *)key {
    if (key.length <= 0) {
        return;
    }
    CScopedLock lock(m_lock);
    [self checkLoadData];

    if ([m_dic objectForKey:key] == nil) {
        return;
    }
    [m_dic removeObjectForKey:key];
    m_hasFullWriteBack = NO;

    static NSData *data = [NSData data];
    [self appendData:data forKey:key];
}

[mmkv setObject:(nullable NSObject<NSCoding> *)object forKey:(NSString *)key]

setObject_forKey.png

[MiniPBCoder encodeDataWithObject:object]

encodeDataWithObject.png

getObjectOfClass:(Class)cls forKey:(NSString *)key

getObjectOfClass_forKey.png

參考文獻

  1. 認真分析mmap:是什么 為什么 怎么用
  2. 獲取文件信息 stat函數(stat、fstat、lstat)
  3. 自動判斷變量類型,類似于 swift 中的 let 淺析C語言auto關鍵字和C++ 中的auto關鍵字
  4. 根據不同的開發環境編譯不同的代碼 關于__IPHONE_OS_VERSION_MAX_ALLOWED和__IPHONE_OS_VERSION_MIN_REQUIRED
  5. 創建指定屬性的文件 iOS小記--NSFileProtectionKey
  6. Linux C ftruncate 函數修改文件大小
  7. 共享內存映射之mmap()函數詳解
  8. constexpr關鍵字的作用
  9. C語言 mmap()函數(建立內存映射) 與 munmap()函數(解除內存映射)
  10. C memcpy()用法
  11. C++ 類構造函數 & 析構函數
  12. C++中的.hpp文件
  13. C++ open 打開文件
  14. 文件處理常用方法及link和unlink講解
  15. linux C語言 getpagesize() 獲得頁內存大小
  16. C++基礎學習】C++中union結構
  17. crc校驗原理
  18. 深入 ProtoBuf - 編碼
  19. ProtoBuf 官網
  20. 常見的序列化框架及Protobuf序列化原理
  21. ProtoBuf的使用以及原理分析
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容