Markdown版本筆記 | 個人GitHub首頁 | 個人博客 | 個人微信 | 個人郵箱 |
---|---|---|---|---|
MyAndroidBlogs | baiqiantao | baiqiantao | bqt20094 | baiqiantao@sina.com |
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
的類型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()
原文:2018-09-21
MMKV--基於 mmap 的 iOS 高性能通用 key-value 組件:2018-03-14
MMKV 是基於 mmap
內存映射的移動端通用 key-value 組件,底層序列化/反序列化使用 protobuf
實現,性能高,穩定性強。從 2015 年中至今,在 iOS
微信上使用已有近 3 年,其性能和穩定性通過了時間的驗證。近期已移植到 Android
平臺。在騰訊內部開源半年以後,獲得公司內部團隊的普遍應用和一致好評。如今一併對外開源,歡迎 Star、提 Issue 和 PR。
在微信客戶端的平常運營中,時不時就會爆發特殊文字
引發系統的 crash,參考文章,文章裏面設計的技術方案是在關鍵代碼先後進行計數器
的加減,經過檢查計數器的異常,來發現引發閃退的異常文字。在會話列表、會話界面等有大量 cell 的地方,但願新加的計時器不會影響滑動性能;另外這些計數器還要永久存儲下來——由於閃退隨時可能發生。這就須要一個性能很是高的通用 key-value 存儲組件
,咱們考察了 SharedPreferences、NSUserDefaults、SQLite 等常見組件,發現都沒能知足如此苛刻的性能要求。考慮到這個防 crash 方案最主要的訴求仍是實時寫入
,而 mmap 內存映射文件恰好知足這種需求,咱們嘗試經過它來實現一套 key-value 組件。
內存映射文件
,提供一段可供隨時寫入的內存塊
,App 只管往裏面寫數據,由操做系統負責將內存回寫到文件
,沒必要擔憂 crash 致使數據丟失。頻繁地進行寫入更新
,咱們須要有增量更新
的能力。咱們考慮將增量 kv 對象序列化後,append 到內存末尾。更詳細的設計原理參考前文 《MMKV——iOS 下基於 mmap 的高性能通用 key-value 組件》。
咱們不是簡簡單單地照搬 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 都有加密功能。
iOS 的使用在前文已經陳述,這裏簡單介紹一下 Android 的用法。
MMKV 已託管到 bintray(JCenter),能夠直接使用。在 App 的 build.gradle 里加上依賴:
implementation 'com.tencent:mmkv:1.0.10'
MMKV 的使用很是簡單,全部變動立馬生效,無需調用 sync
、apply
。 在 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);
MMKV 提供了 importFromSharedPreferences()
函數,能夠比較方便地遷移數據過來。
MMKV 還額外實現了一遍 SharedPreferences
、SharedPreferences.Editor
這兩個 interface,在遷移的時候只需兩三行代碼便可,其餘 CRUD 操做代碼都不用改。
更詳細的用法能夠參看 GitHub 上的 wiki 文檔。
咱們將 MMKV 和 NSUserDefaults 進行對比,重複讀寫操做 1w 次。相關測試代碼在 iOS/MMKVDemo/MMKVDemo/
,結果見以下圖表。
測試機器是 iPhone X 256 G,iOS 12 beta 2,每組操做重複 1w 次,時間單位是 ms。
可見,MMKV 在寫入性能上遠遠超越 NSUserDefaults,在讀取性能上也有相近或超越的表現。
咱們將 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 遷移到 Android 平臺過程當中,不少同事反饋須要支持多進程訪問——這在以前是沒有考慮過的(由於 iOS 不支持多進程
),須要進行全盤的設計和仔細的實現。
說到 IPC,首要的問題就是架構選型,不一樣的架構效果截然不同。
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
)。
咱們有兩個選擇:
關於 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 失敗說明其餘進程持有了讀鎖,咱們須要先將本身的讀鎖釋放掉,再進行加寫鎖操做,以免死鎖的發生。
解寫鎖時,假如以前曾經持有讀鎖,那麼咱們不能直接釋放掉寫鎖,這樣會致使讀鎖也解了。咱們應該加一個讀鎖,將鎖降級。
寫了個簡單的測試,建立兩個 Service,測試 MMKV、MultiProcessSharedPreferences、SQLite 多進程讀寫的性能,具體代碼見 GitHub。
測試環境:Pixel 2 XL 64G, Android 8.1.0,單位:ms。每組測試分別循環 1000 次;MultiProcessSharedPreferences 使用 apply() 同步數據;SQLite 打開 WAL 選項。
2019-5-4