MMKV 多進程K-V組件 MD

Markdown版本筆記 個人GitHub首頁 個人博客 個人微信 個人郵箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina.com

目錄

MMKV

MMKV
Android 安裝教程
Android 使用教程
Android 進階教程
Android 性能對比
MMKV 原理
MMKV for Android 多進程設計與實現java

MMKV is an efficient, small, easy-to-use mobile key-value storage framework used in the WeChat application. It's currently available on iOS, macOS, Android and Windows.android

MMKV 是基於 mmap 內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。c++

基本使用

implementation 'com.tencent:mmkv:1.0.19'

MMKV 默認以動態庫形式連接 libc++,會額外佔用 2MB 空間(解壓後)。若是你其餘庫沒有用到libc++_shared.so,或者你擔憂不一樣版本的libc++_shared.so會帶來潛在的問題,你可使用靜態連接 libc++ 的 MMKV:git

implementation 'com.tencent:mmkv-static:1.0.19'

在 App 啓動時初始化 MMKV,設定 MMKV 的根目錄:github

String rootDir = MMKV.initialize(this);

MMKV 提供一個全局的實例,能夠直接使用:算法

MMKV kv = MMKV.defaultMMKV();
kv.encode("bool", true); //添加、更新
boolean bValue = kv.decodeBool("bool"); //獲取
kv.removeValueForKey("bool"); //刪除
kv.containsKey("bool"); //判斷是否存在
String[] keys = kv.allKeys(); //獲取全部key的數組

若是不一樣業務須要區別存儲,也能夠單首創建本身的實例:數組

MMKV mmkv = MMKV.mmkvWithID("MyID");

支持的數據類型

支持如下 Java 語言基礎類型:boolean、int、long、float、double、byte[]
支持如下 Java 類和容器:緩存

  • String、Set<String>
  • 任何實現了Parcelable的類型

SharedPreferences 遷移

MMKV 提供了 importFromSharedPreferences() 函數,能夠比較方便地遷移數據過來。微信

MMKV 還額外實現了一遍 SharedPreferences、SharedPreferences.Editor 這兩個 interface,在遷移的時候只需兩三行代碼便可,其餘 CRUD 操做代碼都不用改。架構

MMKV mmkv = MMKV.mmkvWithID("myData");
// 遷移舊數據
SharedPreferences old_man = getSharedPreferences("myData", MODE_PRIVATE);
mmkv.importFromSharedPreferences(old_man); //臥槽,把SharedPreferences整個傳過去了,還不是想怎麼搞都行
old_man.edit().clear().commit();

// 跟之前用法同樣
SharedPreferences.Editor editor = mmkv.edit();
editor.putBoolean("bool", true);
//editor.commit(); // 無需調用 commit()

MMKV 組件如今開源了

原文:2018-09-21
MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件:2018-03-14

MMKV 是基於 mmap 內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf 實現,性能高,穩定性強。從 2015 年中至今,在 iOS 微信上使用已有近 3 年,其性能和穩定性通過了時間的驗證。近期已移植到 Android 平臺。在騰訊內部開源半年以後,獲得公司內部團隊的普遍應用和一致好評。如今一併對外開源,歡迎 Star、提 Issue 和 PR。

MMKV 源起

在微信客戶端的平常運營中,時不時就會爆發特殊文字引發系統的 crash,參考文章,文章裏面設計的技術方案是在關鍵代碼先後進行計數器的加減,經過檢查計數器的異常,來發現引發閃退的異常文字。在會話列表、會話界面等有大量 cell 的地方,但願新加的計時器不會影響滑動性能;另外這些計數器還要永久存儲下來——由於閃退隨時可能發生。這就須要一個性能很是高的通用 key-value 存儲組件,咱們考察了 SharedPreferences、NSUserDefaults、SQLite 等常見組件,發現都沒能知足如此苛刻的性能要求。考慮到這個防 crash 方案最主要的訴求仍是實時寫入,而 mmap 內存映射文件恰好知足這種需求,咱們嘗試經過它來實現一套 key-value 組件。

MMKV 原理

  • 內存準備:經過 mmap 內存映射文件,提供一段可供隨時寫入的內存塊,App 只管往裏面寫數據,由操做系統負責將內存回寫到文件,沒必要擔憂 crash 致使數據丟失。
  • 數據組織:數據序列化方面咱們選用 protobuf 協議,pb 在性能和空間佔用上都有不錯的表現。
  • 寫入優化:考慮到主要使用場景是頻繁地進行寫入更新,咱們須要有增量更新的能力。咱們考慮將增量 kv 對象序列化後,append 到內存末尾。
  • 空間增加:使用 append 實現增量更新帶來了一個新的問題,就是不斷 append 的話,文件大小會增加得不可控。咱們須要在性能和空間上作個折中。

更詳細的設計原理參考前文 《MMKV——iOS 下基於 mmap 的高性能通用 key-value 組件》

MMKV for Android 特有功能

咱們不是簡簡單單地照搬 iOS 的實現,在遷移到 Android 的過程當中,深刻分析了 Android 平臺現有 kv 組件的痛點,在原有功能基礎上,開發了 Android 特有的功能。

多進程訪問
經過與 Android 開發同窗的溝通,瞭解到系統自帶的 SharedPreferences 對多進程的支持很差。現有基於 ContentProvider 封裝的實現,雖然多進程是支持了,可是性能低下,常常致使 ANR。考慮到 mmap 共享內存本質上的多進程共享的,咱們在這個基礎上,深刻挖掘了 Android 系統的能力,提供了多是業界最高效的多進程數據共享組件。具體實現原理咱們中秋節後分享,心急的同窗能夠前往 GitHub 查看源碼和 wiki 文檔。

匿名內存
在多進程共享的基礎上,考慮到某些敏感數據(例如密碼)須要進程間共享,可是不方便落地存儲到文件上,直接用 mmap 不合適。咱們瞭解到 Android 系統提供了 Ashmem 匿名共享內存的能力,發現它在進程退出後就會消失,不會落地到文件上,很是適合這個場景。咱們很愉快地提供了 Ashmem MMKV 的功能。

數據加密
不像 iOS 提供了硬件層級的加密機制,在 Android 環境裏,數據加密是很是必須的。MMKV 使用了 AES CFB-128 算法來加密/解密。咱們選擇 CFB 而不是常見的 CBC 算法,主要是由於 MMKV 使用 append-only 實現插入/更新操做,流式加密算法更加合適。事實上這個功能也回饋到了 iOS 版,因此如今兩個系統的 MMKV 都有加密功能。

MMKV 使用

iOS 的使用在前文已經陳述,這裏簡單介紹一下 Android 的用法。

Android 快速上手

MMKV 已託管到 bintray(JCenter),能夠直接使用。在 App 的 build.gradle 里加上依賴:

implementation 'com.tencent:mmkv:1.0.10'

MMKV 的使用很是簡單,全部變動立馬生效,無需調用 syncapply。 在 App 啓動時初始化 MMKV,設定 MMKV 的根目錄(files/mmkv/),例如在 MainActivity 裏:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    String rootDir = MMKV.initialize(this);
    Log.d("mmkv", "root: " + rootDir);
    //……
}

MMKV 提供一個全局的實例,能夠直接使用:

MMKV kv = MMKV.defaultMMKV();

kv.encode("bool", true);
kv.encode("int", Integer.MIN_VALUE);
kv.encode("string", "Hello from mmkv");

boolean bValue = kv.decodeBool("bool");
int iValue = kv.decodeInt("int");
String str = kv.decodeString("string");

若是不一樣業務須要區別存儲,也能夠單首創建本身的實例:

MMKV mmkv = MMKV.mmkvWithID("MyID");
mmkv.encode("bool", true);

SharedPreferences 遷移

MMKV 提供了 importFromSharedPreferences() 函數,能夠比較方便地遷移數據過來。

MMKV 還額外實現了一遍 SharedPreferencesSharedPreferences.Editor 這兩個 interface,在遷移的時候只需兩三行代碼便可,其餘 CRUD 操做代碼都不用改。

更詳細的用法能夠參看 GitHub 上的 wiki 文檔。

MMKV 性能

iOS 性能對比

咱們將 MMKV 和 NSUserDefaults 進行對比,重複讀寫操做 1w 次。相關測試代碼在 iOS/MMKVDemo/MMKVDemo/,結果見以下圖表。

測試機器是 iPhone X 256 G,iOS 12 beta 2,每組操做重複 1w 次,時間單位是 ms。

可見,MMKV 在寫入性能上遠遠超越 NSUserDefaults,在讀取性能上也有相近或超越的表現。

Android 性能對比

咱們將 MMKV 和 SharedPreferences、SQLite 進行對比, 重複讀寫操做 1k 次。相關測試代碼在 Android/MMKV/mmkvdemo/。結果以下圖表。

單進程性能

測試機器是 Pixel 2 XL 64G,Android 8.1,每組操做重複 1k 次,時間單位是 ms。

可見,MMKV 在寫入性能上遠遠超越 SharedPreferences & SQLite,在讀取性能上也有相近或超越的表現。

多進程性能

測試機器是 Pixel 2 XL 64G,Android 8.1,每組操做重複 1k 次,時間單位是 ms。

可見,MMKV 不管是在寫入性能仍是在讀取性能,都遠遠超越 MultiProcessSharedPreferences & SQLite & SQLite, MMKV 在 Android 多進程 key-value 存儲組件上是不二之選

MMKV for Android 多進程設計與實現

原文:2018-09-25

原本這是此框架最核心的部分,是最須要區學習的,可是看完後只能說:一臉懵逼!

將 MMKV 遷移到 Android 平臺過程當中,不少同事反饋須要支持多進程訪問——這在以前是沒有考慮過的(由於 iOS 不支持多進程),須要進行全盤的設計和仔細的實現。

IPC 選型

說到 IPC,首要的問題就是架構選型,不一樣的架構效果截然不同。

CS 架構 vs 去中心化架構

Android 平臺第一個想到的就是 ContentProvider:一個單獨進程管理數據,數據同步不易出錯,簡單好用易上手。然而它的問題也很明顯,就是一個字:啓動慢,訪問也慢。這個能夠說是 Android 下基於 Binder 的 CS 架構組件的通用痛點。

至於其餘的 CS 架構,例如經典的 socket、PIPE、message queue,由於要至少 2 次的內存拷貝,就更加慢了。

MMKV 追求的是極致的訪問速度,咱們要儘量地避免進程間通訊,CS 架構是不可取的。再考慮到 MMKV 底層使用 mmap 實現,採用去中心化的架構是很天然的選擇。咱們只須要將文件 mmap 到每一個訪問進程的內存空間,加上合適的進程鎖,再處理好數據的同步,就可以實現多進程併發訪問。

挑選進程鎖

然而去中心化的架構實現起來並不簡單,Android 是個閹割版的 Linux,IPC 組件的支持比較殘缺。例如,說到進程鎖第一個想到的就是 pthread 庫的 pthread_mutex,建立於共享內存的 pthread_mutex 是能夠用做進程鎖的,然而 Android 版的 pthread_mutex 並不保證robust,亦即對 pthread_mutex 加了鎖的進程被 kill,系統不會進行清理工做,這個鎖會一直存在下去,那麼其餘等鎖的進程就會永遠餓死。

其餘的 IPC 組件,例如信號量、條件變量,也有一樣問題,Android 爲了可以儘快關閉進程,真是無所不用其極。

找了一圈,可以保證 robust 的,只有已打開的文件描述符,以及基於文件描述符的文件鎖和 Binder 組件的死亡通知(是的,Binder 也是依賴這個清理機制運做,打開的文件是 /dev/binder)。

咱們有兩個選擇:

  • 文件鎖,優勢是自然 robust,缺點是不支持遞歸加鎖,也不支持讀寫鎖升級/降級,須要自行實現。
  • pthread_mutex,優勢是 pthread 庫支持遞歸加鎖,也支持讀寫鎖升級/降級,缺點是不 robust,須要自行清理。

關於 mutex 清理,有個可能的方案是基於 Binder 死亡通知進行清理:A、B進程相互註冊對方的死亡通知,在對方死亡的時候進行清理。但有個比較棘手的場景:只有 A 進程存在,那麼他的死亡通知就沒人處理,留下一個永遠加鎖的 mutex。Binder 規定死亡通知不能本進程自行處理,必須由其餘進程處理,因此這個問題很差解決。

綜合各類考慮,咱們先將文件鎖做爲一個簡單的互斥鎖,進行 MMKV 的多進程開發,稍後再回頭解決遞歸鎖和讀寫鎖升級/降級的問題。

多進程實現細節

首先咱們簡單回顧一下 MMKV 原來的邏輯。MMKV 本質上是將文件 mmap 到內存塊中,將新增的 key-value 通通 append 到內存中;到達邊界後,進行重整回寫以騰出空間,空間仍是不夠的話,就 double 內存空間;對於內存文件中可能存在的重複鍵值,MMKV 只選用最後寫入的做爲有效鍵值。那麼其餘進程爲了保持數據一致,就須要處理這三種狀況:寫指針增加、內存重整、內存增加。但首先還得解決一個問題:怎麼讓其餘進程感知這三種狀況?

狀態同步

  • 寫指針的同步
    咱們能夠在每一個進程內部緩存本身的寫指針,而後在寫入鍵值的同時,還要把最新的寫指針位置也寫到 mmap 內存中;這樣每一個進程只須要對比一下緩存的指針與 mmap 內存的寫指針,若是不同,就說明其餘進程進行了寫操做。事實上 MMKV 本來就在文件頭部保存了有效內存的大小,這個數值恰好就是寫指針的內存偏移量,咱們能夠重用這個數值來校對寫指針。

  • 內存重整的感知
    考慮使用一個單調遞增的序列號,每次發生內存重整,就將序列號遞增。將這個序列號也放到 mmap 內存中,每一個進程內部也緩存一份,只須要對比序列號是否一致,就可以知道其餘進程是否觸發了內存重整。

  • 內存增加的感知
    事實上 MMKV 在內存增加以前,會先嚐試經過內存重整來騰出空間,重整後還不夠空間才申請新的內存。因此內存增加能夠跟內存重整同樣處理。至於新的內存大小,能夠經過查詢文件大小來得到,無需在 mmap 內存另外存放。

狀態同步邏輯用僞碼錶達大概是這個樣子:

寫指針增加

當一個進程發現 mmap 寫指針增加,就意味着其餘進程寫入了新鍵值。這些新的鍵值都 append 在原有寫指針後面,可能跟前面的 key 重複,也多是全新的 key,而原寫指針前面的鍵值都是有效的。那麼咱們就要把這些新鍵值都讀出來,插入或替換原有鍵值,並將寫指針同步到最新位置。

內存重整

當一個進程發現內存被重整了,就意味着原寫指針前面的鍵值所有失效,那麼最簡單的作法是所有拋棄掉,從頭開始從新加載一遍。

內存增加

正如前文所述,發生內存增加的時候,必然已經先發生了內存重整,那麼原寫指針前面的鍵值也是通通失效,處理邏輯跟內存重整同樣。

文件鎖

到這裏咱們已經完成了數據的多進程同步工做,是時候回頭處理鎖事了,亦即前面提到的遞歸鎖和鎖升級/降級。

  • 遞歸鎖
    意思是若是一個進程/線程已經擁有了鎖,那麼後續的加鎖操做不會致使卡死,而且解鎖也不會致使外層的鎖被解掉。對於文件鎖來講,前者是知足的,後者則否則。由於文件鎖是狀態鎖,沒有計數器,不管加了多少次鎖,一個解鎖操做就全解掉。只要用到子函數,就很是須要遞歸鎖。

  • 鎖升級/降級
    鎖升級是指將已經持有的共享鎖,升級爲互斥鎖,亦即將讀鎖升級爲寫鎖;鎖降級則是反過來。文件鎖支持鎖升級,可是容易死鎖:假如 A、B 進程都持有了讀鎖,如今都想升級到寫鎖,就會陷入相互等待的困境,發生死鎖。另外,因爲文件鎖不支持遞歸鎖,也致使了鎖降級沒法進行,一降就降到沒有鎖。

爲了解決這兩個難題,須要對文件鎖進行封裝,增長讀鎖、寫鎖計數器。處理邏輯以下表:

讀鎖計數器 寫鎖計數器 加讀鎖 加寫鎖 解讀鎖 解寫鎖
0 0 加讀鎖 加寫鎖 - -
0 1 +1 +1 - 解寫鎖
0 N +1 +1 - -1
1 0 +1 解讀鎖再加寫鎖 解讀鎖 -
1 1 +1 +1 -1 加讀鎖
1 N +1 +1 -1 -1
N 0 +1 解讀鎖再加寫鎖 -1 -
N 1 +1 +1 -1 加讀鎖
N N +1 +1 -1 -1

須要注意的地方有兩點:

  • 加寫鎖時,若是當前已經持有讀鎖,那麼先嚐試加寫鎖,try_lock 失敗說明其餘進程持有了讀鎖,咱們須要先將本身的讀鎖釋放掉,再進行加寫鎖操做,以免死鎖的發生。

  • 解寫鎖時,假如以前曾經持有讀鎖,那麼咱們不能直接釋放掉寫鎖,這樣會致使讀鎖也解了。咱們應該加一個讀鎖,將鎖降級

MMKV 多進程性能

寫了個簡單的測試,建立兩個 Service,測試 MMKV、MultiProcessSharedPreferences、SQLite 多進程讀寫的性能,具體代碼見 GitHub。

測試環境:Pixel 2 XL 64G, Android 8.1.0,單位:ms。每組測試分別循環 1000 次;MultiProcessSharedPreferences 使用 apply() 同步數據;SQLite 打開 WAL 選項。

2019-5-4

相關文章
相關標籤/搜索