三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Android本地存储优化:MMKV核心原理、集成封装与性能调优指南

Android本地存储优化:MMKV核心原理、集成封装与性能调优指南

1. 从 SharedPreferences 到 MMKV:为什么我们需要一个更好的本地存储方案?

如果你做过 Android 开发,对SharedPreferences一定不会陌生。这个由系统提供的轻量级键值对存储工具,几乎是每个 App 存储简单配置信息的首选。然而,用久了,它的“坑”也渐渐暴露出来:在主线程同步写入可能导致的 ANR(应用无响应)、多进程访问时的数据不一致、以及在大数据量下相对低效的读写性能。这些问题在追求极致用户体验和稳定性的今天,变得愈发不可接受。

正是在这样的背景下,微信团队开源了MMKV。它不是一个凭空创造的新概念,而是针对SharedPreferences的痛点,给出的一个“降维打击”式的解决方案。MMKV 的核心目标非常明确:更快、更稳、支持多进程。它基于内存映射(mmap)和 protobuf 编码,将性能提升到了一个新的层次。简单来说,你可以把它理解为一个超级加强版的SharedPreferences,但它的内部原理和实现方式,却与前者有着天壤之别。

这篇文章,我会从一个多年移动端开发者的角度,带你彻底搞懂 MMKV。我们不仅会深入它的核心原理,看看它是如何做到“快”的,还会手把手教你如何在项目中集成和使用它。更重要的是,我会分享在实际大型项目中,如何对 MMKV 进行符合自身业务需求的二次封装,让它用起来更顺手、更安全。无论你是刚刚听说 MMKV,还是已经用过但想深入了解,这篇文章都能给你带来实实在在的干货。

2. MMKV 核心原理深度拆解:快与稳的背后

要理解 MMKV 为什么强,我们必须深入到它的两个核心技术支柱:内存映射(Memory Mapping)Protocol Buffers 编码。这二者结合,共同构筑了 MMKV 高性能的基石。

2.1 内存映射(mmap):绕过内核的“高速通道”

传统文件 I/O(比如SharedPreferencescommit()apply())是怎样的流程?当我们调用写入方法时,数据需要先从用户空间的缓冲区,拷贝到内核空间的缓冲区,再由操作系统决定何时真正写入磁盘。这个过程至少涉及两次数据拷贝(用户态->内核态)和一次系统调用,在频繁写入小数据时,上下文切换和拷贝的开销就显得非常可观。

mmap 则提供了一条“捷径”。它通过系统调用,将磁盘文件的一部分或全部,直接映射到进程的虚拟内存地址空间。完成映射后,应用程序读写这段内存区域,就如同在操作一个巨大的字节数组。操作系统会在后台透明地处理页缓存、脏页回写(将修改过的内存页写回磁盘)等细节。

对于 MMKV 来说,这意味着:

  1. 写入快:调用putString()等接口后,数据经过编码直接写入这块映射内存。大部分情况下,这只是一次内存拷贝操作,无需立即发起系统调用。真正的磁盘写入由操作系统异步完成,对应用性能影响极小。
  2. 读取更快:读取数据时,直接从那块映射内存中读取,相当于内存访问。首次访问可能触发缺页中断将数据从磁盘加载到内存,之后的数据访问几乎就是内存速度。
  3. 崩溃一致性:由于映射关系由操作系统内核管理,即使 App 意外崩溃,只要数据成功写入了映射内存(即成为了“脏页”),内核最终会负责将其安全地同步到磁盘文件,这比SharedPreferences在崩溃时可能丢失apply()的数据要可靠得多。

注意:mmap 并不是银弹。它需要占用虚拟内存地址空间,且映射的文件大小会影响内存占用。MMKV 采用了动态扩容和文件重整机制来优化这一点,我们后面会讲到。

2.2 Protocol Buffers 编码:更小、更快的序列化

数据在存储和传输前需要序列化。SharedPreferences使用的是 XML 格式,虽然可读性好,但冗余信息多,解析效率低。MMKV 选择了 Google 的Protocol Buffers (protobuf)编码。

Protobuf 是一种二进制编码协议,它的核心优势在于:

  • 体积小:采用 Tag-Length-Value (TLV) 等紧凑的二进制格式,没有冗余的字段名、标签符号,相同内容比 XML 或 JSON 小很多。
  • 编解码快:二进制编码,解析时无需复杂的词法、语法分析,速度远超文本格式。
  • 向前/向后兼容:通过字段编号(field number)来标识数据,新增或删除字段不会破坏旧代码的解析,非常适合配置存储这类可能随版本演进的数据结构。

在 MMKV 中,每一个键值对都被编码为一个 protobuf 消息。键(Key)和值(Value)分别被编码并组合在一起。当你写入一个键值对时,MMKV 会先将其序列化成二进制数据,再写入 mmap 内存区域。读取时,则从对应位置读取二进制数据并反序列化。

这里有一个关键点:MMKV 并没有使用.proto文件来定义静态结构,而是采用了一种“自描述”的动态方式。它内部为每种数据类型(int, bool, string, bytes等)定义了固定的字段编号和编码格式。这种设计使得 MMKV 的 API 非常灵活,可以存储任意类型的键值对,而无需预定义模式(Schema)。

2.3 文件结构与扩容机制

一个 MMKV 实例对应一个文件。文件内部并不是简单的键值对列表,而是经过精心设计的结构,以支持高效增删改查和空间回收。

  1. 文件头:包含魔数(标识MMKV文件)、版本号、文件大小等元信息。
  2. 有效数据区:顺序存储着经过 protobuf 编码的键值对数据。
  3. 空洞:当某个键的值被更新或删除时,旧数据所占用的空间并不会被立即回收,而是变成了“空洞”。

随着不断更新和删除,文件中的“空洞”会越来越多,导致文件体积膨胀,空间利用率下降。为此,MMKV 引入了文件重整(Compaction)机制。

重整的触发时机通常包括:文件剩余空间不足需要扩容前、或者“空洞”总大小超过一定阈值时。重整的过程可以理解为“磁盘垃圾回收”:

  • MMKV 会遍历所有有效的键值对。
  • 将它们重新编码,并紧凑地写入到一个新的内存缓冲区或临时文件。
  • 最后用新的紧凑数据替换掉旧的文件内容。

这个机制保证了存储空间长期使用后依然高效。MMKV 默认的策略比较智能,会在空间不足时尝试重整来避免立即扩容。

2.4 多进程同步原理

支持多进程是 MMKV 相比SharedPreferences的一个巨大优势。其核心依赖于文件锁进程间通信(IPC)

  • 文件锁(fcntl 或 flock):任何进程在读写 MMKV 文件前,都必须先获取文件锁。这保证了同一时刻只有一个进程可以修改文件内容,避免了数据损坏。
  • 状态同步:当一个进程修改了数据后,其他进程如何感知?MMKV 使用了多种 IPC 机制来通知其他进程,在 Android 上主要依赖共享内存结合SystemV semaphore(信号量)POSIX 匿名共享内存 + pthread mutex(互斥锁)
    • 在内存中维护一个共享的“状态结构体”,记录文件的长度、内容 CRC 校验码等。
    • 当进程 A 写入数据后,会更新这个共享状态。
    • 进程 B 在每次读取操作前,会检查这个共享状态。如果发现状态已改变(说明文件被其他进程更新了),就会重新加载(reload)整个文件到自己的内存映射中,从而获取最新数据。

这个过程对开发者是透明的。你只需要在初始化时指定MMKV.MULTI_PROCESS_MODE,剩下的脏活累活 MMKV 都帮你处理好了。不过要注意,多进程模式下的性能损耗会比单进程稍大,因为涉及进程间同步开销。

3. 从零开始:MMKV 的集成与基础使用

理解了原理,我们来看看如何把它用起来。MMKV 的集成和使用非常 straightforward。

3.1 项目集成与初始化

首先是在项目中引入依赖。以 Gradle 为例:

dependencies { implementation 'com.tencent:mmkv:1.3.4' // 请使用最新版本 }

接下来,在 Application 的onCreate()方法中进行初始化。这是至关重要的一步,必须在使用任何 MMKV 实例前完成。

class MyApp : Application() { override fun onCreate() { super.onCreate() val rootDir = MMKV.initialize(this) Log.i("MMKV", "MMKV 根目录: $rootDir") // 通常你会得到类似 /data/user/0/your.package.name/files/mmkv/ 的路径 } }

MMKV.initialize(Context)方法会设置好 MMKV 的默认根存储路径。你也可以传入自定义的路径字符串。

初始化之后,你就可以获取全局的默认 MMKV 实例了:

val kv = MMKV.defaultMMKV()

如果你想创建不同 ID 的实例(用于隔离不同业务模块的数据),或者需要多进程支持,可以:

// 单进程,实例ID为 “myData” val kvSingle = MMKV.mmkvWithID("myData") // 多进程,实例ID为 “interProcessData” val kvMulti = MMKV.mmkvWithID("interProcessData", MMKV.MULTI_PROCESS_MODE) // 自定义存储路径的单进程实例 val customPath = "${filesDir.absolutePath}/my_custom_mmkv" val kvCustom = MMKV.mmkvWithID("custom", MMKV.SINGLE_PROCESS_MODE, customPath)

3.2 基础 API 使用详解

MMKV 的 API 设计几乎与SharedPreferences保持一致,学习成本极低。以下是一些核心操作:

写入数据:

kv.encode("bool", true) kv.encode("int", 1024) kv.encode("long", System.currentTimeMillis()) kv.encode("float", 3.14f) kv.encode("double", 3.1415926) kv.encode("string", "Hello from MMKV!") kv.encode("byteArray", byteArrayOf(1, 2, 3)) // 支持存储 Set<String> val stringSet = setOf("apple", "banana", "orange") kv.encode("stringSet", stringSet)

encode()方法会自动推断类型。所有写入操作都是同步但高效的,因为数据直接写入了 mmap 内存。

读取数据:

val boolValue = kv.decodeBool("bool", false) // 第二个参数是默认值 val intValue = kv.decodeInt("int", 0) val stringValue = kv.decodeString("string", "") val byteArrayValue = kv.decodeBytes("byteArray") val setValue = kv.decodeStringSet("stringSet", emptySet())

读取操作是内存级的,速度极快。

删除数据与清空:

kv.removeValueForKey("int") // 删除指定键 kv.removeValuesForKeys(arrayOf("bool", "float")) // 批量删除 kv.clearAll() // 清空所有数据(谨慎使用!)

其他实用操作:

// 检查键是否存在 val hasKey = kv.containsKey("string") // 获取所有键 val allKeys = kv.allKeys() // 获取某个键对应的 value 的 size(字节数) val valueSize = kv.getValueSize("string") // 获取文件总大小 val totalSize = kv.totalSize() // 获取实际数据大小(排除“空洞”) val actualSize = kv.actualSize()

3.3 与 SharedPreferences 的迁移

如果你有现存的项目使用SharedPreferences,MMKV 提供了极其方便的迁移工具,可以一键无缝迁移。

val oldSharedPrefs = getSharedPreferences("old_data", Context.MODE_PRIVATE) val mmkv = MMKV.mmkvWithID("migrated_data") // 一键迁移!数据会从 SharedPreferences 导入到 MMKV 中。 // 注意:这不会删除旧的 SharedPreferences 文件。 mmkv.importFromSharedPreferences(oldSharedPrefs) // 迁移完成后,可以(可选)删除旧文件 oldSharedPrefs.edit().clear().apply() // 或者直接删除 /data/data/your.package.name/shared_prefs/old_data.xml

实操心得:迁移操作最好在 App 首次安装或升级后的初始化阶段进行,并且只做一次。可以在SharedPreferences中存一个标记位,记录是否已迁移,避免重复迁移。

4. 进阶封装:打造业务友好的 MMKV 工具类

直接使用 MMKV 的 API 虽然简单,但在大型项目中,散落的encode/decode调用会带来一些问题:键名管理混乱、类型安全缺失、无法统一进行数据加密或格式转换、不利于单元测试等。因此,对 MMKV 进行一层符合自身业务逻辑的封装,是很有必要的。

4.1 封装设计思路

一个好的封装应该实现以下目标:

  1. 集中管理 Key:避免硬编码字符串散落各处。
  2. 类型安全:利用 Kotlin 的扩展函数或泛型,提供类型安全的存取接口。
  3. 默认值管理:统一且方便地设置默认值。
  4. 数据转换:封装复杂对象(如 JSON 对象、List)的序列化与反序列化。
  5. 可选增强:集成加密、日志、迁移等高级功能。
  6. 易于测试:通过接口抽象,便于在单元测试中替换实现。

4.2 基础封装实现示例

下面我们一步步实现一个基础的、类型安全的封装。

第一步:定义 Key 常量创建一个object类来集中管理所有存储键。

object StorageKeys { // 用户相关 const val KEY_USER_TOKEN = "user_token" const val KEY_USER_ID = "user_id" const val KEY_USER_NAME = "user_name" const val KEY_LAST_LOGIN_TIME = "last_login_time" // 应用配置 const val KEY_APP_THEME = "app_theme" // “light”, “dark”, “system” const val KEY_NOTIFICATION_ENABLED = "notification_enabled" const val KEY_FIRST_LAUNCH = "is_first_launch" // 业务数据 const val KEY_SEARCH_HISTORY = "search_history" // 需要序列化 }

第二步:创建核心的存储管理类我们创建一个KVStorage类,它内部持有 MMKV 实例,并提供类型安全的存取方法。

import com.tencent.mmkv.MMKV class KVStorage private constructor() { // 单例模式,确保全局只有一个存储管理器 companion object { val instance: KVStorage by lazy(mode = LazyThreadSafetyMode.SYNCHRONIZED) { KVStorage() } } private val mmkv: MMKV = MMKV.defaultMMKV() // 基础类型存取(使用扩展函数风格,更 Kotlin) fun put(key: String, value: String) = mmkv.encode(key, value) fun getString(key: String, defaultValue: String = ""): String = mmkv.decodeString(key, defaultValue) ?: defaultValue fun put(key: String, value: Int) = mmkv.encode(key, value) fun getInt(key: String, defaultValue: Int = 0): Int = mmkv.decodeInt(key, defaultValue) fun put(key: String, value: Boolean) = mmkv.encode(key, value) fun getBoolean(key: String, defaultValue: Boolean = false): Boolean = mmkv.decodeBool(key, defaultValue) fun put(key: String, value: Long) = mmkv.encode(key, value) fun getLong(key: String, defaultValue: Long = 0L): Long = mmkv.decodeLong(key, defaultValue) fun put(key: String, value: Float) = mmkv.encode(key, value) fun getFloat(key: String, defaultValue: Float = 0f): Float = mmkv.decodeFloat(key, defaultValue) fun put(key: String, value: Double) = mmkv.encode(key, value) fun getDouble(key: String, defaultValue: Double = 0.0): Double = mmkv.decodeDouble(key, defaultValue) fun put(key: String, value: Set<String>) = mmkv.encode(key, value) fun getStringSet(key: String, defaultValue: Set<String> = emptySet()): Set<String> = mmkv.decodeStringSet(key, defaultValue) ?: defaultValue // 删除操作 fun remove(key: String) = mmkv.removeValueForKey(key) fun clearAll() = mmkv.clearAll() // 检查是否存在 fun contains(key: String): Boolean = mmkv.containsKey(key) }

第三步:为复杂对象提供序列化支持对于List<SomeModel>或自定义对象,我们需要将其转换为 String(JSON)或 ByteArray 进行存储。

import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.lang.reflect.Type class KVStorage private constructor() { // ... 保留上述基础类型方法 ... private val gson = Gson() // 存储任意对象(转换为JSON字符串) fun <T> putObject(key: String, obj: T?) { if (obj == null) { remove(key) return } val json = gson.toJson(obj) put(key, json) } // 获取对象 inline fun <reified T> getObject(key: String, defaultValue: T? = null): T? { val json = getString(key, "") if (json.isEmpty()) return defaultValue return try { gson.fromJson(json, T::class.java) } catch (e: Exception) { e.printStackTrace() defaultValue } } // 存储对象列表 fun <T> putObjectList(key: String, list: List<T>?) { if (list == null) { remove(key) return } val type = object : TypeToken<List<T>>() {}.type val json = gson.toJson(list, type) put(key, json) } // 获取对象列表 inline fun <reified T> getObjectList(key: String, defaultValue: List<T> = emptyList()): List<T> { val json = getString(key, "") if (json.isEmpty()) return defaultValue return try { val type = object : TypeToken<List<T>>() {}.type gson.fromJson(json, type) ?: defaultValue } catch (e: Exception) { e.printStackTrace() defaultValue } } }

第四步:提供业务层便捷访问现在,我们可以创建一个PreferenceManagerSettings类,对外提供业务语义明确的访问接口。

object AppSettings { private val storage = KVStorage.instance var userToken: String get() = storage.getString(StorageKeys.KEY_USER_TOKEN) set(value) = storage.put(StorageKeys.KEY_USER_TOKEN, value) var userId: Long get() = storage.getLong(StorageKeys.KEY_USER_ID) set(value) = storage.put(StorageKeys.KEY_USER_ID, value) var appTheme: String get() = storage.getString(StorageKeys.KEY_APP_THEME, "system") set(value) = storage.put(StorageKeys.KEY_APP_THEME, value) var isNotificationEnabled: Boolean get() = storage.getBoolean(StorageKeys.KEY_NOTIFICATION_ENABLED, true) set(value) = storage.put(StorageKeys.KEY_NOTIFICATION_ENABLED, value) var isFirstLaunch: Boolean get() = storage.getBoolean(StorageKeys.KEY_FIRST_LAUNCH, true) set(value) = storage.put(StorageKeys.KEY_FIRST_LAUNCH, value) // 复杂对象存取示例:搜索历史(List<String>) var searchHistory: List<String> get() = storage.getObjectList(StorageKeys.KEY_SEARCH_HISTORY) set(value) = storage.putObjectList(StorageKeys.KEY_SEARCH_HISTORY, value) // 清除用户相关数据(退出登录时调用) fun clearUserData() { storage.remove(StorageKeys.KEY_USER_TOKEN) storage.remove(StorageKeys.KEY_USER_ID) storage.remove(StorageKeys.KEY_USER_NAME) // 注意:不清除主题、通知设置等全局配置 } }

现在,在业务代码中,你可以像访问属性一样使用存储功能,非常清晰和安全:

// 写入 AppSettings.userToken = "eyJhbGciOiJIUzI1NiIs..." AppSettings.searchHistory = listOf("Kotlin", "MMKV", "Android") // 读取 if (AppSettings.isFirstLaunch) { showGuide() AppSettings.isFirstLaunch = false } val history = AppSettings.searchHistory

4.3 封装的高级特性扩展

基础封装之上,我们可以根据需求添加更多功能。

1. 数据加密MMKV 本身支持 AES CFB-128 加密。你可以在创建 MMKV 实例时指定加密密钥。

val cryptKey = "My-Encryption-Key-16B".toByteArray() // 必须是16字节或以上 val encryptedKV = MMKV.mmkvWithID("encrypted_data", MMKV.SINGLE_PROCESS_MODE, null, cryptKey)

在封装层,可以为敏感数据(如 Token)专门创建一个加密的 MMKV 实例。

2. 备份与恢复虽然 MMKV 文件本身具有崩溃安全性,但重要的用户数据可以考虑定期备份到外部存储或云端。封装层可以提供exportToFile()importFromFile()的方法,将指定 Key 的数据打包导出。

3. 变化监听MMKV 提供了内容变化监听器。封装层可以将其包装成更易用的 RxJava Flow 或 Kotlin Flow,方便在 UI 层观察数据变化。

// 在 KVStorage 中添加 fun addOnValueChangedListener(listener: MMKV.OnContentChangeListener) { mmkv.addOnContentChangeListener(listener) } fun removeOnValueChangedListener(listener: MMKV.OnContentChangeListener) { mmkv.removeOnContentChangeListener(listener) }

4. 单元测试支持为了便于测试,可以将KVStorage抽象成一个接口IStorage,然后提供基于 MMKV 的实现MMKVStorage和基于内存的测试实现MockStorage。这样在单元测试中,可以轻松替换为MockStorage,不依赖真实的文件系统。

5. 实战避坑与性能调优指南

在实际项目中使用 MMKV,你可能会遇到一些特定场景下的问题。这里我总结了一些常见的“坑”和对应的解决方案。

5.1 多进程模式下的注意事项

虽然 MMKV 支持多进程,但并不意味着你可以像在单进程中一样随意读写。

  • 性能损耗:多进程同步有开销。如果某个进程写入极其频繁,可能会拖慢其他进程的读取速度。对于高频写入的数据,需要评估是否真的需要多进程共享,或者考虑使用其他 IPC 机制(如 AIDL、ContentProvider)来传递。
  • Reload 机制:其他进程写入后,当前进程的 MMKV 实例需要触发reload()才能读到最新数据。MMKV 内部会自动检查并触发,但这个检查有微小延迟。在对数据一致性要求极端严格的场景(如金融交易状态),你需要考虑在关键读取操作前,手动调用mmkv.reload()确保数据最新,或者采用更严格的同步机制。
  • 初始化时机:所有进程都必须在Application.onCreate()中初始化 MMKV,且使用相同的根路径和实例 ID,才能正确建立多进程同步。

5.2 存储超大 Value 的风险

MMKV 基于 mmap,文件大小是有限的(受系统内存和地址空间约束)。虽然 MMKV 会动态扩容,但单个 Value 特别大(比如超过几百 KB 的字符串或字节数组)时,会带来问题:

  1. 扩容抖动:写入大 Value 可能触发文件重整和扩容,这次写入操作会变慢。
  2. 内存压力:mmap 会将整个文件(或大部分)映射到内存。如果文件因为一个大 Value 变得很大,会占用较多的虚拟内存。
  3. 读写效率:protobuf 编码大二进制数据效率尚可,但整体不如专门的文件存储。

建议:对于超过 100KB 的单个数据(如图片缓存、大段文本),建议直接使用文件存储(FileOutputStream),而在 MMKV 中只存储其文件路径。

5.3 键的命名规范与清理

随着业务迭代,可能会产生大量废弃的 Key。这些 Key 对应的数据即使已无用,仍占据着存储空间(直到文件重整被回收)。

  • 命名规范:建议使用模块前缀,如user_profile:name,app_config:theme。这样在查看所有键或清理时更容易辨识。
  • 定期清理:在 App 升级时,可以编写一个“数据迁移”脚本,主动删除已知的、废弃的旧 Key。KVStorage封装类可以提供一个removeLegacyKeys()方法,在合适的时机调用。

5.4 版本升级与数据兼容性

当你的数据结构发生变化时(比如存储的对象模型增加了字段),需要考虑兼容性。

  • Protobuf 的天然兼容性:由于 MMKV 使用 protobuf 且是“自描述”的,新增字段在读取旧数据时会被忽略(取默认值),删除字段旧数据中多余的也会被忽略。这提供了基本的向前/向后兼容。
  • 复杂对象的显式处理:对于使用 Gson 序列化的复杂对象,情况不同。如果旧版本存储的 JSON 缺少新版本的字段,Gson 反序列化时该字段会是 null 或默认值。这通常可以接受。但如果字段类型发生变化(如String改为Int),则会导致反序列化失败。这时就需要在封装层的getObject方法中做更健壮的异常处理,或者实现显式的数据迁移逻辑。

5.5 性能监控与日志

MMKV 提供了日志回调接口MMKVHandler,可以监控错误和重要事件。

MMKV.registerHandler(object : MMKVHandler { override fun onError(mmkv: MMKV?, error: String): MMKVRecoverStrategic { Log.e("MMKV_ERROR", error) // 根据错误类型决定恢复策略 return MMKVRecoverStrategic.OnErrorDiscard } override fun onContentChanged(mmapID: String?) { Log.d("MMKV_CHANGE", "Content changed for: $mmapID") } })

在生产环境,建议至少监控onError,以便及时发现和上报存储层的损坏等问题。常见的错误包括“文件校验失败”、“空间不足”等。

5.6 与 DataStore 的选型考量

Jetpack DataStore 是 Android 官方推出的新一代数据存储解决方案,也旨在取代SharedPreferences。它提供了 Proto DataStore(类型安全,基于 protobuf)和 Preferences DataStore(键值对,类似 MMKV)两种。

MMKV 与 Preferences DataStore 的简单对比:

特性MMKVDataStore (Preferences)
性能极高,基于 mmap,读写为内存操作。高,但基于 Flow 和磁盘 I/O,写入是异步的。
多进程原生支持,通过文件锁和共享内存同步。不支持。官方明确说明不支持多进程。
API 风格同步 API,简单直接。异步 API(基于 Kotlin Coroutines Flow),更现代。
类型安全需自行封装实现。通过Preferences.Key<T>提供编译时类型安全。
数据一致性强一致性,写入后立即可读。最终一致性,写入是异步的,订阅 Flow 可观察变化。
成熟度与生态非常成熟,微信、QQ等亿级应用验证。较新,属于 Jetpack 官方组件,未来主流。
额外依赖需单独引入库。属于 Android Jetpack 一部分。

选型建议:

  • 追求极致性能和多进程支持MMKV是当前不二之选。
  • 新项目,且遵循最新 Jetpack 架构:可以考虑DataStore,特别是配合协程使用非常流畅。如果不需要多进程,它是一个很好的现代化选择。
  • 存量大型项目迁移:如果饱受SharedPreferences性能或多进程问题困扰,迁移到MMKV收益明显,且迁移成本较低。

我个人在需要多进程共享配置、或对本地存储性能有严苛要求的场景下,依然首选 MMKV。而在一般的、单进程的偏好设置存储上,开始尝试使用 DataStore,享受其类型安全和响应式 API 的好处。

5.7 一个真实的“踩坑”案例:重复初始化导致的数据丢失

有一次在排查线上问题时,发现部分用户的某些配置项莫名其妙恢复了默认值。日志显示,这些用户都发生在一次热更新或特定操作后。

经过层层排查,最终定位到原因:代码中某处存在重复的MMKV.initialize()调用,并且传入了一个不同的路径。MMKV 的初始化如果被调用多次,并且路径不同,后续获取的默认 MMKV 实例可能会指向新的路径,导致 App 实际上在使用一个“新”的、空的数据文件,从而“丢失”了旧数据。

解决方案

  1. 确保MMKV.initialize()只在Application.onCreate()中调用一次。
  2. 如果确实需要多存储位置,使用MMKV.mmkvWithID(id, mode, rootPath)明确指定路径,并妥善管理这些实例的生命周期。
  3. 在封装类KVStorage中,将mmkv实例的获取也做单例化或静态化处理,避免重复创建。

这个坑告诉我们,对于存储这类基础组件,初始化的管理必须严格且清晰。

← 返回列表