iOS MMKV原理整理总结:比UserDefaults快100倍的存储方案是如何炼成的?

作为iOS开发者,你一定体验过NSUserDefaults在频繁读写时的性能瓶颈,它在卡顿列表和WatchDog导致的0x8badf0d类型的abort列表中经常看到,而今天要介绍的MMKV ,以其独特的mmap内存映射和增量更新策略,实现了令人惊艳的存储性能提升,且替代NSUserDefaults后能解决导致的卡顿和abort问题。

一、传统存储方案的痛点

在介绍MMKV之前,让我们先看看iOS开发者常用的几种本地存储方案:

  • NSUserDefaults :适合简单键值对,但性能瓶颈明显,每次写入都需同步到文件
  • SQLite :功能强大但使用复杂,对简单键值存储来说太重了
  • Core Data:面向对象但学习曲线陡峭,性能调优困难
  • 直接文件操作:灵活但需要处理并发、安全、序列化等各种问题

这些方案在存储频繁读写的小数据时,要么性能不足,要么使用过于复杂。那么,有没有一种既简单又高效的键值存储方案呢?

二、MMKV的惊艳表现

MMKV是腾讯开源的一款高性能键值存储组件,最初为微信开发,用于解决跨平台、高性能的本地存储需求。让我们通过一个简单测试来看它的性能优势:

swift 复制代码
// 性能对比测试
let testCount = 1000

// NSUserDefaults测试
let defaults = UserDefaults.standard
let startTime1 = CACurrentMediaTime()
for i in 0..<testCount {
    defaults.set("value\(i)", forKey: "key\(i)")
    defaults.synchronize() // 强制同步到文件
}
let userDefaultsTime = CACurrentMediaTime() - startTime1

// MMKV测试
let mmkv = MMKV.default()!
let startTime2 = CACurrentMediaTime()
for i in 0..<testCount {
    mmkv.set("value\(i)", forKey: "key\(i)")
}
let mmkvTime = CACurrentMediaTime() - startTime2

print("NSUserDefaults耗时:\(userDefaultsTime)秒")
print("MMKV耗时:\(mmkvTime)秒")
print("MMKV比NSUserDefaults快\(userDefaultsTime/mmkvTime)倍")

测试结果显示,在1000次连续写入的场景下,MMKV可以比NSUserDefaults快几十到上百倍。那么,这种惊人性能是如何实现的呢?

三、MMKV核心技术原理揭秘

1. 内存映射(mmap)技术

MMKV性能的核心秘诀在于使用了mmap(memory mapping) 技术。传统的文件写入流程是这样的:

  1. 数据写入应用层缓冲区
  2. 数据拷贝到内核缓冲区
  3. 内核将数据写入磁盘

这个过程涉及两次数据拷贝和多次用户态/内核态切换,效率较低。

而mmap的工作方式完全不同:

c++ 复制代码
// mmap的基本使用方式
int fd = open(filePath, O_RDWR|O_CREAT, S_IRWXU);
void *memory = mmap(NULL, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

mmap将磁盘文件直接映射到进程的虚拟内存空间,使应用可以像操作内存一样操作文件。对映射内存的读写操作,操作系统会在后台自动同步到磁盘文件。

mmap的优势

  • 零拷贝:数据无需在用户空间和内核空间之间来回拷贝
  • 延迟写入:修改操作先写入内存,由操作系统异步刷盘
  • 崩溃安全:即使应用崩溃,操作系统也能保证数据持久化

2. 增量更新与追加写入策略

传统键值存储(如NSUserDefaults)每次写入时,都会重新序列化整个字典并写入文件。而MMKV采用了一种聪明的增量更新策略:

初始文件内容:[KV1, KV2, KV3]

更新key1的值: 传统方式:重新写入[KV1_new, KV2, KV3]

MMKV方式:直接在文件末尾追加[KV1_new]

文件变为:[KV1, KV2, KV3, KV1_new]

读取时,MMKV会从后向前扫描,最后出现的键值对就是最新值

3. Protocol Buffers序列化

MMKV使用Google的Protocol Buffers作为序列化协议,相比传统的JSON或Property List格式:

  • 编码更紧凑:二进制格式,体积更小
  • 解析更快:无需复杂的词法分析
  • 向后兼容:支持字段增减而不破坏现有数据
ini 复制代码
// Protobuf的消息定义
message KVItem {
    optional string key = 1;
    optional bytes value = 2;
}

4. 空间重整与扩容机制

追加写入策略的一个明显问题是文件会无限增长。MMKV通过空间重整解决这个问题:

Swift 复制代码
class MMKV {
    func fullWriteback() {
        // 1. 收集所有键值对,每个key只保留最新值
        var newData = [String: Data]()
        for (key, value) in allKeyValues() {
            newData[key] = value.last // 只保留最新值
        }
        
        // 2. 重新序列化所有数据
        let serializedData = serialize(newData)
        
        // 3. 如果空间不足,执行扩容
        if serializedData.count > currentSize {
            expand(calculateNewSize(serializedData.count))
        }
        
        // 4. 将整理后的数据写入文件开头
        writeToBeginning(serializedData)
        
        // 5. 截断文件,丢弃旧数据
        truncateFile(serializedData.count)
    }
}

当文件剩余空间不足时,MMKV会触发空间重整,如果重整后仍空间不足,则按两倍大小扩容。

5. 多进程与线程安全

MMKV通过文件锁和内存重映射实现多进程访问数据同步,这在iOS扩展(如Today Extension、Share Extension等)中特别有用:

Swift 复制代码
// 多进程初始化
MMKV *mmkv = [MMKV mmkvWithID:@"shared_mmkv" 
                  cryptKey:cryptKey
                  mode:MMKVMultiProcess];

// 进程A写入数据
[mmkv setObject:@(42) forKey:@"shared_key"];

// 进程B读取数据(立即生效)
NSNumber *value = [mmkv getObjectOfClass:NSNumber.class 
                                  forKey:@"shared_key"];

内部通过文件锁(fcntl)读写锁(pthread_rwlock) 保证数据一致性:

  • 进程间同步:使用文件锁
  • 线程间同步:使用读写锁(读共享,写独占)

四、MMKV架构全貌

为了更好地理解MMKV各组件如何协同工作,让我们看一下其整体架构图:

graph TD A[应用层调用] --> B[MMKV C++ Core] B --> C{操作类型} C -->|读操作| D[Memory Map
内存映射] C -->|写操作| E[Protobuf序列化] D --> F[查找算法] F --> G[从后向前扫描
获取最新值] G --> H[返回数据给应用层] E --> I[追加写入] I --> J{空间检查} J -->|空间足够| K[写入mmap内存] J -->|空间不足| L[触发空间重整] L --> M[Key排重
保留最新值] M --> N{重整后空间是否足够} N -->|是| O[写入文件头部] N -->|否| P[文件扩容
2倍增长] P --> O K --> Q[操作系统异步刷盘] O --> Q R[CRC校验] --> S[数据完整性保证] Q --> S

从这个架构图可以看出,MMKV的设计哲学是:

  1. 读操作优先:通过内存映射和高效查找,实现O(n)复杂度读取
  2. 写操作优化:通过追加写入避免全量重写
  3. 空间智能管理:自动重整和扩容,平衡空间和性能
  4. 数据安全保障 :CRC-32算法校验确保数据完整性,MMKV在文件末尾存储了一个循环冗余校验码 。这个值是文件所有有效数据通过CRC算法计算出的一个摘要。

五、实际应用场景与最佳实践

1. 何时使用MMKV?

  • 用户偏好设置:主题、字体大小等配置
  • 登录状态与Token:需要快速读写的认证信息
  • 应用标记位:首次启动、功能引导完成状态
  • 轻量缓存:搜索历史、浏览记录等

2. 何时避免使用MMKV?

  • 大量结构化数据:考虑使用SQLite或Core Data
  • 大型二进制文件:如图片、视频,应使用文件系统直接存储
  • 需要复杂查询的数据:MMKV只支持键值查询

3. 使用示例

Swift 复制代码
import MMKV

class SettingsManager {
    static let shared = SettingsManager()
    private let mmkv: MMKV
    
    private init() {
        // 初始化MMKV
        MMKV.initialize(rootDir: nil)
        mmkv = MMKV.default()!
        
        // 多进程共享(用于App Groups)
        // let groupDir = MMKV.initialize(rootDir: "your_app_group_path")
        // mmkv = MMKV(mmapID: "shared_settings", mode: .multiProcess)
    }
    
    // 存储用户设置
    func saveUserSettings(_ settings: UserSettings) {
        do {
            let data = try JSONEncoder().encode(settings)
            mmkv.set(data, forKey: "user_settings")
        } catch {
            print("保存设置失败: \(error)")
        }
    }
    
    // 读取用户设置
    func loadUserSettings() -> UserSettings? {
        guard let data = mmkv.data(forKey: "user_settings") else {
            return nil
        }
        
        do {
            return try JSONDecoder().decode(UserSettings.self, from: data)
        } catch {
            print("读取设置失败: \(error)")
            return nil
        }
    }
    
    // 存储简单值
    var darkModeEnabled: Bool {
        get { mmkv.bool(forKey: "dark_mode", defaultValue: false) }
        set { mmkv.set(newValue, forKey: "dark_mode") }
    }
    
    // 加密存储敏感数据
    func saveSecureToken(_ token: String) {
        if let cryptMMKV = MMKV(mmapID: "secure_storage", cryptKey: "your_encryption_key".data(using: .utf8)) {
            cryptMMKV.set(token, forKey: "auth_token")
        }
    }

    // 及时释放不用的MMKV实例
    func cleanupUnusedMMKV() {
        MMKV.close(mmapID: "temporary_storage")
    }
}

4. 性能优化技巧

  1. 批量写入:虽然MMKV单次写入很快,但批量操作仍可进一步优化
scss 复制代码
// 不好的做法:多次单独写入
for item in items {
    mmkv.set(item.value, forKey: item.key)
}

// 好的做法:批量处理
mmkv.beginTransaction()
defer { mmkv.commitTransaction() }
for item in items {
    mmkv.set(item.value, forKey: item.key)
}
  1. 合理选择存储类型
  • 简单类型直接存储(Bool、Int、String等)
  • 复杂对象使用JSON或Protobuf序列化
  • 大对象考虑拆分为多个键值存储

六、MMKV的局限性

虽然MMKV性能卓越,但也存在一些限制:

  1. 数据大小限制:单个MMKV实例文件不宜过大(建议不超过几MB)
  2. 无查询功能:只能按键查找,不支持条件查询
  3. iOS版本要求:需要iOS 9.0+
  4. 增加包体积:增加约200KB左右的二进制大小

七、未来展望

MMKV仍在持续演进,未来可能的发展方向包括:

  1. 压缩支持:对存储内容进行透明压缩
  2. 更智能的淘汰策略:自动清理过期数据
  3. 云同步集成:与iCloud等云服务无缝集成
  4. Swift原生API:提供更符合Swift习惯的接口

总结

MMKV通过巧妙结合mmap内存映射、Protobuf序列化和增量更新策略,实现了远超传统方案的存储性能。它的设计哲学是:为频繁读写的小数据场景提供极致优化

对于大多数iOS应用,MMKV是替代NSUserDefaults的理想选择,特别是在需要高性能、多进程共享或简单加密存储的场景。当然,选择存储方案时,还是要根据具体需求:小数据、高频读写选MMKV;结构化、复杂查询选SQLite;大文件选文件系统

希望这篇文章能帮助你深入理解MMKV的工作原理,并在实际项目中合理使用这一强大工具。如果你有任何问题或使用经验分享,欢迎在评论区留言交流。

相关推荐
云里雾里!2 小时前
力扣 209. 长度最小的子数组:滑动窗口解法完整解析
数据结构·算法·leetcode
GISer_Jing2 小时前
jx前端架构学习
前端·学习·架构
CoderYanger3 小时前
递归、搜索与回溯-穷举vs暴搜vs深搜vs回溯vs剪枝:12.全排列
java·算法·leetcode·机器学习·深度优先·剪枝·1024程序员节
憨憨崽&3 小时前
进击大厂:程序员必须修炼的算法“内功”与思维体系
开发语言·数据结构·算法·链表·贪心算法·线性回归·动态规划
chem41114 小时前
C 语言 函数指针和函数指针数组
c语言·数据结构·算法
8***v2574 小时前
使用最广泛的Web应用架构
架构
半吊子全栈工匠4 小时前
Text2SQL的参考架构
架构
p***s914 小时前
MySQL的底层原理与架构
数据库·mysql·架构