
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比如SharedPreferences的commit()或apply()是怎樣的流程當我們調用寫入方法時數據需要先從用戶空間的緩沖區拷貝到內核空間的緩沖區再由操作系統決定何時真正寫入磁盤。這個過程至少涉及兩次數據拷貝用戶態-內核態和一次系統調用在頻繁寫入小數據時上下文切換和拷貝的開銷就顯得非常可觀。mmap 則提供了一條“捷徑”。它通過系統調用將磁盤文件的一部分或全部直接映射到進程的虛擬內存地址空間。完成映射后應用程序讀寫這段內存區域就如同在操作一個巨大的字節數組。操作系統會在后臺透明地處理頁緩存、臟頁回寫將修改過的內存頁寫回磁盤等細節。對于 MMKV 來說這意味著寫入快調用putString()等接口后數據經過編碼直接寫入這塊映射內存。大部分情況下這只是一次內存拷貝操作無需立即發起系統調用。真正的磁盤寫入由操作系統異步完成對應用性能影響極小。讀取更快讀取數據時直接從那塊映射內存中讀取相當于內存訪問。首次訪問可能觸發缺頁中斷將數據從磁盤加載到內存之后的數據訪問幾乎就是內存速度。崩潰一致性由于映射關系由操作系統內核管理即使 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 實例對應一個文件。文件內部并不是簡單的鍵值對列表而是經過精心設計的結構以支持高效增刪改查和空間回收。文件頭包含魔數標識MMKV文件、版本號、文件大小等元信息。有效數據區順序存儲著經過 protobuf 編碼的鍵值對數據。空洞當某個鍵的值被更新或刪除時舊數據所占用的空間并不會被立即回收而是變成了“空洞”。隨著不斷更新和刪除文件中的“空洞”會越來越多導致文件體積膨脹空間利用率下降。為此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)) // 支持存儲 SetString 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 的遷移如果你有現存的項目使用SharedPreferencesMMKV 提供了極其方便的遷移工具可以一鍵無縫遷移。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 封裝設計思路一個好的封裝應該實現以下目標集中管理 Key避免硬編碼字符串散落各處。類型安全利用 Kotlin 的擴展函數或泛型提供類型安全的存取接口。默認值管理統一且方便地設置默認值。數據轉換封裝復雜對象如 JSON 對象、List的序列化與反序列化。可選增強集成加密、日志、遷移等高級功能。易于測試通過接口抽象便于在單元測試中替換實現。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: SetString) mmkv.encode(key, value) fun getStringSet(key: String, defaultValue: SetString emptySet()): SetString mmkv.decodeStringSet(key, defaultValue) ?: defaultValue // 刪除操作 fun remove(key: String) mmkv.removeValueForKey(key) fun clearAll() mmkv.clearAll() // 檢查是否存在 fun contains(key: String): Boolean mmkv.containsKey(key) }第三步為復雜對象提供序列化支持對于ListSomeModel或自定義對象我們需要將其轉換為 StringJSON或 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: ListT?) { if (list null) { remove(key) return } val type object : TypeTokenListT() {}.type val json gson.toJson(list, type) put(key, json) } // 獲取對象列表 inline fun reified T getObjectList(key: String, defaultValue: ListT emptyList()): ListT { val json getString(key, ) if (json.isEmpty()) return defaultValue return try { val type object : TypeTokenListT() {}.type gson.fromJson(json, type) ?: defaultValue } catch (e: Exception) { e.printStackTrace() defaultValue } } }第四步提供業務層便捷訪問現在我們可以創建一個PreferenceManager或Settings類對外提供業務語義明確的訪問接口。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) // 復雜對象存取示例搜索歷史ListString var searchHistory: ListString 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.searchHistory4.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 的字符串或字節數組時會帶來問題擴容抖動寫入大 Value 可能觸發文件重整和擴容這次寫入操作會變慢。內存壓力mmap 會將整個文件或大部分映射到內存。如果文件因為一個大 Value 變得很大會占用較多的虛擬內存。讀寫效率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.KeyT提供編譯時類型安全。數據一致性強一致性寫入后立即可讀。最終一致性寫入是異步的訂閱 Flow 可觀察變化。成熟度與生態非常成熟微信、QQ等億級應用驗證。較新屬于 Jetpack 官方組件未來主流。額外依賴需單獨引入庫。屬于 Android Jetpack 一部分。選型建議追求極致性能和多進程支持MMKV是當前不二之選。新項目且遵循最新 Jetpack 架構可以考慮DataStore特別是配合協程使用非常流暢。如果不需要多進程它是一個很好的現代化選擇。存量大型項目遷移如果飽受SharedPreferences性能或多進程問題困擾遷移到MMKV收益明顯且遷移成本較低。我個人在需要多進程共享配置、或對本地存儲性能有嚴苛要求的場景下依然首選 MMKV。而在一般的、單進程的偏好設置存儲上開始嘗試使用 DataStore享受其類型安全和響應式 API 的好處。5.7 一個真實的“踩坑”案例重復初始化導致的數據丟失有一次在排查線上問題時發現部分用戶的某些配置項莫名其妙恢復了默認值。日志顯示這些用戶都發生在一次熱更新或特定操作后。經過層層排查最終定位到原因代碼中某處存在重復的MMKV.initialize()調用并且傳入了一個不同的路徑。MMKV 的初始化如果被調用多次并且路徑不同后續獲取的默認 MMKV 實例可能會指向新的路徑導致 App 實際上在使用一個“新”的、空的數據文件從而“丟失”了舊數據。解決方案確保MMKV.initialize()只在Application.onCreate()中調用一次。如果確實需要多存儲位置使用MMKV.mmkvWithID(id, mode, rootPath)明確指定路徑并妥善管理這些實例的生命周期。在封裝類KVStorage中將mmkv實例的獲取也做單例化或靜態化處理避免重復創建。這個坑告訴我們對于存儲這類基礎組件初始化的管理必須嚴格且清晰。