
1. 为什么我们需要MMKV从SharedPreferences的痛点说起如果你是一个Android开发者那么对SharedPreferences简称SP一定不会陌生。作为Android SDK内置的轻量级键值对存储方案它几乎是每个App存储简单配置、用户偏好的首选。然而随着项目迭代和业务复杂度的提升SP的诸多问题开始暴露尤其是在性能、稳定性和多进程场景下它常常成为应用的“性能瓶颈”和“崩溃之源”。我经历过不止一次因为SP导致的线上问题。最典型的一次是在一个用户量不小的App里我们使用SP存储了用户的登录态和一些配置信息。在应用启动时多个线程几乎同时去读取和写入SP结果在主线程等待SP的commit或apply操作时触发了ANR应用无响应。排查日志发现SP的写入操作特别是commit()是同步且可能阻塞UI线程的。即便使用apply()它虽然异步但最终仍会触发一个等待写入完成的QueuedWork锁在某些ROM上如果Activity的onStop里等待这个锁同样可能导致ANR。另一个头疼的问题是多进程。当你的App有多个进程比如主进程、推送进程、WebView独立进程需要共享同一份配置时SP的MODE_MULTI_PROCESS标志位在Android N及以上版本已被标记为废弃且其实现本身并不可靠存在数据不同步的风险。我们曾遇到过推送进程修改了配置但主进程读取到的仍是旧值的诡异问题。此外SP在存储大量数据或频繁读写时其基于XML的序列化与反序列化、全量写入的机制会导致I/O效率低下。每次apply哪怕只改了一个键值对它也会将整个文件重写一遍。正是这些切肤之痛让我们开始寻找替代方案。腾讯开源的MMKV正是在这种背景下进入了我们的视野。它并非另一个简单的键值对封装而是从底层存储模型上进行了重构主打高性能、高可靠、多进程支持。简单来说MMKV通过将数据序列化后以内存映射mmap的方式写入文件并采用增量更新的策略完美避开了SP的绝大多数缺陷。接下来我们就深入它的内部看看它是如何做到的。2. MMKV的核心原理内存映射与协议设计MMKV的高性能并非魔法其核心建立在两个关键技术之上内存映射Memory Mapping和自研的二进制编码协议。理解这两点你就能明白它为何能快如闪电。2.1 内存映射mmap绕过内核缓冲区的直接通道传统文件I/O如Java的FileOutputStream的路径是用户空间 - 内核缓冲区 - 磁盘。这个过程涉及多次上下文切换和数据拷贝是主要的性能开销所在。而内存映射则是将磁盘文件的一部分或全部直接“映射”到进程的虚拟内存地址空间。完成映射后应用程序读写这段内存区域就如同在操作内存一样操作系统会在后台自动完成数据与磁盘文件的同步。这带来了几个根本性的优势极高的读写速度数据操作直接在用户态内存中进行避免了系统调用如read/write和内核缓冲区的拷贝。对于MMKV这种小数据量、高频访问的场景提升是数量级的。数据持久化自动化开发者无需手动调用flush操作系统有机制如脏页回写来确保映射内存中的数据最终落盘既保证了性能也兼顾了数据安全。多进程共享基石多个进程可以将同一个文件映射到各自的地址空间从而实现内存级别的数据共享。一个进程修改了映射内存其他进程可以立刻“看到”变化这是实现高效、可靠多进程同步的底层保障。在Android上MMKV通过mmap系统调用实现这一机制。它一次性将整个文件映射到内存后续所有的读写都在内存中进行极大地减少了I/O操作。2.2 二进制编码与增量更新告别全量写入SP使用XML格式文本解析序列化/反序列化慢且每次更新都要重写整个文件。MMKV则采用了自定义的二进制编码格式。你可以把MMKV文件想象成一个在内存中连续的数据块。它主要包含两部分文件头和数据区。文件头保存一些元信息例如数据区的有效长度、CRC校验码等。数据区由一条条记录Key-Value对顺序排列组成。每条记录都按特定的二进制格式编码包含了Key的长度、Value的长度、实际的Key字符串、以及序列化后的Value数据。当需要写入一个新数据时MMKV会执行以下步骤将Key和Value按照协议序列化成二进制数据计算这条记录的总长度。在数据区的末尾当前有效数据之后寻找一块足够大的空闲空间写入这条新记录。更新一个内部的索引字典通常在内存中例如HashMap将Key与该条记录在文件中的位置偏移量和大小关联起来。更新文件头中的有效数据长度。关键在于第2步。如果数据区末尾有足够的空间例如之前删除数据产生的碎片空间被整理过这就是一次纯粹的追加写入Append速度极快。只有当剩余空间不足时MMKV才会触发一次全量重整Defragmentation它遍历所有有效记录将它们紧凑地重写到一块新的内存或文件中然后切换文件映射。这避免了SP那种“每次修改都全量写”的粗暴方式。对于修改操作MMKV的处理同样巧妙它不会去原位更新旧记录而是将新的Key-Value作为一条新记录追加到末尾并更新内存索引指向新位置。旧记录就变成了“垃圾数据”。这种“追加写”模式非常契合mmap的特性能获得最高的写入吞吐。垃圾空间会在下次空间不足触发重整时被回收。2.3 多进程同步的实现文件锁与状态通知多进程共享是MMKV的招牌功能。其实现依赖于文件锁flock在进行文件重整、空间扩容等关键写入操作时MMKV会使用跨进程文件锁来确保同一时间只有一个进程能执行这些操作防止数据损坏。状态同步当一个进程通过mmap修改了内存数据后其他进程如何感知MMKV依赖于操作系统对mmap的同步机制。但为了更及时MMKV在Android上通常会结合SharedPreferences的OnSharedPreferenceChangeListener通过一个很小的SP文件或自定义的ContentProvider来广播数据变更通知。其他进程收到通知后会重新加载reloadmmap文件由于文件内容已被修改重新映射后就能读到最新数据。这个“通知-重载”的周期非常短保证了数据的强一致性。3. 从入门到精通MMKV的详细使用指南理解了原理使用起来就心中有数了。MMKV提供了非常简洁的API。3.1 基础集成与初始化首先在模块的build.gradle中添加依赖dependencies { implementation com.tencent:mmkv:1.3.4 // 请使用最新版本 }然后在Application的onCreate中进行初始化建议指定一个根目录class MyApp : Application() { override fun onCreate() { super.onCreate() val rootDir MMKV.initialize(this) Log.i(MMKV, MMKV root dir: $rootDir) // 或者使用自定义路径 // val dir filesDir.absolutePath /mmkv // MMKV.initialize(dir) } }initialize方法会返回MMKV默认的存储根路径。初始化只需要做一次。3.2 核心API使用示例获取MMKV实例默认是单进程模式// 获取默认的、进程私有的MMKV实例 val kv MMKV.defaultMMKV() // 存储数据 kv.encode(bool, true) kv.encode(int, 42) kv.encode(string, Hello MMKV) kv.encode(float, 3.14f) kv.encode(byteArray, byteArrayOf(1, 2, 3)) // 支持存储SetString kv.encode(stringSet, setOf(a, b, c)) // 支持存储实现了Parcelable的对象 kv.encode(user, User(John)) // 读取数据 val bValue kv.decodeBool(bool, false) // 第二个参数是默认值 val iValue kv.decodeInt(int, 0) val sValue kv.decodeString(string, ) val fValue kv.decodeFloat(float, 0f) val arrayValue kv.decodeBytes(byteArray) val setValue kv.decodeStringSet(stringSet, emptySet()) val userValue kv.decodeParcelableUser(user, null) // 删除数据 kv.removeValueForKey(bool) // 或删除多个 kv.removeValuesForKeys(arrayOf(int, float)) // 清空所有数据 // kv.clearAll()注意encode和decode系列方法是线程安全的。但请注意对于Parcelable对象其序列化/反序列化的性能取决于对象本身复杂度不适合存储过大或过复杂的对象。3.3 多进程模式的使用如果需要多进程共享在获取实例时指定一个MMKV.MULTI_PROCESS_MODE标志即可val multiProcessKV MMKV.mmkvWithID(multi_process_config, MMKV.MULTI_PROCESS_MODE)这样不同进程通过相同的MMKVID这里是multi_process_config获取到的实例操作的就是同一个物理文件数据变更可以跨进程同步。重要提示多进程模式下由于存在“通知-重载”的延迟虽然极短在写入后立即读取可能读到的仍是旧数据在当前进程重载完成前。对于强一致性要求的场景写入后应调用sync()方法或使用commit模式见下文确保数据落盘并在读取前确认已完成同步。3.4 存储加密与自定义路径MMKV支持AES CFB-128加密保护敏感数据val cryptKey My-Encryption-Key123.toByteArray() // 密钥必须是16字节 val encryptedKV MMKV.mmkvWithID(encrypted_id, MMKV.SINGLE_PROCESS_MODE, cryptKey)加密发生在数据被序列化之后写入内存之前全程在内存中进行性能损失很小。你也可以指定自定义的文件路径val customPath ${getExternalFilesDir(null)}/my_mmkv val customKV MMKV.mmkvWithID(custom_id, customPath)3.5 性能调优SyncMode与日志MMKV提供两种同步模式通过MMKV.SYNC_MODE和MMKV.ASYNC_MODE常量设置val syncKV MMKV.mmkvWithID(sync_kv, MMKV.SINGLE_PROCESS_MODE, null, MMKV.SYNC_MODE)SYNC_MODE类似于SP的commit()数据会同步写入mmap内存并尽可能快地触发内核同步到磁盘。数据安全性最高但写入延迟稍高。ASYNC_MODE默认类似于SP的apply()数据先写入内存同步到磁盘的操作由系统异步调度。性能更好但在极端崩溃情况下可能有极小概率丢失最后一次写入。在调试时可以开启MMKV的日志MMKV.setLogLevel(MMKVLogLevel.LevelInfo) // 或 LevelDebug, LevelWarning, LevelError这有助于了解其内部操作如空间扩容、文件重整等。4. 实战封装构建一个健壮、易用的KV存储组件直接使用MMKV的API虽然简单但在大型项目中直接散落在各处会导致代码难以维护、无法统一监控和替换。一个好的封装层能带来以下好处统一管理所有KV操作入口唯一便于做数据迁移、加密策略统一切换。类型安全与默认值避免手敲Key字符串出错为每个Key强类型化并提供合理的默认值。监听与回调统一实现数据变更监听方便业务模块响应。兼容与降级为未来可能的存储方案迁移或SP向MMKV迁移预留接口。下面我分享一个在生产环境中经过验证的封装方案。4.1 定义存储键与类型安全接口首先定义一个密封类或枚举来管理所有存储的Key避免魔法字符串。// 使用密封类定义所有Key并关联其类型和默认值 sealed class StorageKeyT(val key: String, val defaultValue: T) { // 用户相关 object UserToken : StorageKeyString(user_token, ) object IsLoggedIn : StorageKeyBoolean(is_logged_in, false) object UserId : StorageKeyLong(user_id, -1L) // 应用配置 object AppLaunchCount : StorageKeyInt(app_launch_count, 0) object LastVersion : StorageKeyString(last_version, ) // 复杂对象 object UserProfile : StorageKeyString(user_profile, {}) // 使用JSON字符串存储 // 或者使用Parcelable这里用String示例 }4.2 核心存储接口与MMKV实现定义一个通用的存储接口这样未来替换存储核心比如换用DataStore时业务层无需改动。interface IKVStorage { fun T put(key: StorageKeyT, value: T) fun T get(key: StorageKeyT): T fun T getOrElse(key: StorageKeyT, defaultValue: T): T fun remove(key: StorageKey*) fun contains(key: StorageKey*): Boolean fun clear() // 可选添加变更监听 fun addOnChangeListener(listener: (StorageKey*) - Unit) fun removeOnChangeListener(listener: (StorageKey*) - Unit) }然后实现基于MMKV的具体类class MMKVStorage private constructor() : IKVStorage { companion object { Volatile private var instance: MMKVStorage? null fun getInstance(): MMKVStorage instance ?: synchronized(this) { instance ?: MMKVStorage().also { instance it } } } // 使用多进程实例确保各组件数据同步 private val mmkv: MMKV MMKV.mmkvWithID(app_main_storage, MMKV.MULTI_PROCESS_MODE) // 监听器列表 private val listeners mutableSetOf(StorageKey*) - Unit() private val listenerLock Any() init { // 注册MMKV自己的全局回调简化版实际需处理线程切换 MMKV.registerHandler { mmapID, key - if (mmapID this.mmkv.mmapID()) { // 根据key找到对应的StorageKey并通知监听者 // 这里需要维护一个反向映射示例省略 notifyListenersForKey(key) } } } override fun T put(key: StorageKeyT, value: T) { when (value) { is Int - mmkv.encode(key.key, value) is Long - mmkv.encode(key.key, value) is Boolean - mmkv.encode(key.key, value) is Float - mmkv.encode(key.key, value) is Double - mmkv.encode(key.key, value) is String - mmkv.encode(key.key, value) is ByteArray - mmkv.encode(key.key, value) is Set* - { Suppress(UNCHECKED_CAST) mmkv.encode(key.key, value as SetString) } // 对于其他复杂类型如Parcelable或自定义对象建议转为JSON字符串存储 else - { // 这里使用Gson或kotlinx.serialization等库序列化 val json Gson().toJson(value) mmkv.encode(key.key, json) } } // 通知监听者 notifyListeners(key) } Suppress(UNCHECKED_CAST) override fun T get(key: StorageKeyT): T { return when (key.defaultValue) { is Int - mmkv.decodeInt(key.key, (key.defaultValue as Int)) as T is Long - mmkv.decodeLong(key.key, (key.defaultValue as Long)) as T is Boolean - mmkv.decodeBool(key.key, (key.defaultValue as Boolean)) as T is Float - mmkv.decodeFloat(key.key, (key.defaultValue as Float)) as T is Double - mmkv.decodeDouble(key.key, (key.defaultValue as Double)) as T is String - mmkv.decodeString(key.key, (key.defaultValue as String)) as T is ByteArray - mmkv.decodeBytes(key.key) ?: (key.defaultValue as ByteArray) as T is Set* - { val set mmkv.decodeStringSet(key.key, null) (set ?: (key.defaultValue as SetString)) as T } else - { // 复杂类型的反序列化 val json mmkv.decodeString(key.key, ) if (json.isNotEmpty()) { try { Gson().fromJson(json, (key as StorageKeyAny).defaultValue::class.java) as T } catch (e: Exception) { key.defaultValue } } else { key.defaultValue } } } } override fun T getOrElse(key: StorageKeyT, defaultValue: T): T { return if (contains(key)) get(key) else defaultValue } override fun contains(key: StorageKey*): Boolean { return mmkv.containsKey(key.key) } override fun remove(key: StorageKey*) { mmkv.removeValueForKey(key.key) notifyListeners(key) } override fun clear() { mmkv.clearAll() synchronized(listenerLock) { listeners.forEach { listener - // 清空时通知所有Key或者定义一个特殊的Clear事件。这里简单处理。 // 实际项目中可能需要更精细的事件定义。 } } } override fun addOnChangeListener(listener: (StorageKey*) - Unit) { synchronized(listenerLock) { listeners.add(listener) } } override fun removeOnChangeListener(listener: (StorageKey*) - Unit) { synchronized(listenerLock) { listeners.remove(listener) } } private fun notifyListeners(key: StorageKey*) { synchronized(listenerLock) { listeners.forEach { it.invoke(key) } } } private fun notifyListenersForKey(keyString: String) { // 根据keyString找到对应的StorageKey这里需要维护映射关系示例省略 // findKey(keyString)?.let { notifyListeners(it) } } }4.3 提供全局易用的访问点为了方便使用可以创建一个顶层的object或依赖注入如Hilt Module来提供实例。// 使用委托属性使访问更 Kotlin 化 object AppStorage { private val storage: IKVStorage by lazy { MMKVStorage.getInstance() } var userToken: String get() storage.get(StorageKey.UserToken) set(value) storage.put(StorageKey.UserToken, value) var isLoggedIn: Boolean get() storage.get(StorageKey.IsLoggedIn) set(value) storage.put(StorageKey.IsLoggedIn, value) var appLaunchCount: Int get() storage.get(StorageKey.AppLaunchCount) set(value) storage.put(StorageKey.AppLaunchCount, value) // 对于复杂对象可以提供专门的序列化/反序列化方法 fun saveUserProfile(profile: UserProfile) { storage.put(StorageKey.UserProfile, Gson().toJson(profile)) } fun getUserProfile(): UserProfile? { val json storage.get(StorageKey.UserProfile) return try { Gson().fromJson(json, UserProfile::class.java) } catch (e: Exception) { null } } // 提供监听能力 fun addLoginStateListener(listener: (Boolean) - Unit) { storage.addOnChangeListener { key - if (key StorageKey.IsLoggedIn) { listener(isLoggedIn) } } } }这样在业务代码中就可以非常清晰和安全地访问了// 写入 AppStorage.userToken new_token AppStorage.isLoggedIn true AppStorage.appLaunchCount // 读取 if (AppStorage.isLoggedIn) { val token AppStorage.userToken // do something } // 监听登录状态变化 AppStorage.addLoginStateListener { isLoggedIn - updateUI(isLoggedIn) }4.4 封装中的注意事项与进阶技巧类型擦除与泛型处理上面的get/put方法使用了when类型判断由于类型擦除对泛型T的判断可能不够优雅。在生产代码中可以考虑使用inline reified来改进或者为每种基础类型定义扩展函数。线程安全MMKV本身操作是线程安全的但我们的监听器列表listeners的增删和遍历需要自己加锁如示例中的synchronized。监听器性能通知监听器时最好切换到主线程如果监听器需要更新UI可以使用Handler或协程的Dispatchers.Main。数据迁移从SharedPreferences迁移到MMKV是常见需求。可以在MMKVStorage初始化时添加一个migrateFromSharedPreferences的方法读取SP的所有条目并写入MMKV然后删除SP文件。异常处理在序列化/反序列化复杂对象时务必做好异常捕获返回默认值避免崩溃。单元测试由于封装层抽象了IKVStorage接口我们可以很容易地为其编写单元测试甚至创建一个基于内存的FakeKVStorage用于测试而不依赖真实的MMKV环境。通过这样一层封装我们不仅享受了MMKV的高性能与稳定性还获得了更好的代码组织、类型安全和可维护性为应用的长期演进打下了坚实基础。在实际项目中这个封装组件已经成为基础架构中不可或缺的一环彻底解决了本地轻量存储的痛点。