MMKV.initialize(this);
在MMKV
的整套流程中,MMKV
的初始化起着承上啓下的做用。java
public static String initialize(Context context) {
// 獲取根路徑
String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
MMKVLogLevel logLevel = MMKVLogLevel.LevelInfo;
return initialize(root, (MMKV.LibLoader)null, logLevel); // 進行加載 1 -->
}
// 1 -->
public static String initialize(String rootDir, MMKV.LibLoader loader, MMKVLogLevel logLevel) {
// 加載必要的so文件
// 。。。。。
// 經過JNI來對底層c的實現進行初始化調度
jniInitialize(MMKV.rootDir, logLevel2Int(logLevel));
return rootDir;
}
複製代碼
由於到這裏的話直接經過三方庫的導入已經不能知足查看了,因此直接去下載MMKV
的開源庫源碼查看比較合適。android
若是你並不太熟悉JNI
的方法調度,也不要緊,我會慢慢的經過方式來教你入門。git
JNI
方法,那如何定位呢? 摁兩下
Shift
的全局搜索,而後直接輸入
initializeMMKV
,就會獲得搜索結果了。
可以發現這裏存在兩個方法,進去看看就知道像C
寫的,那目標羣體就已經被你鎖定了。github
void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
// ThreadOnce說明初始化過程只會進行一次
ThreadLock::ThreadOnce(&once_control, initialize);
g_rootDir = rootDir;
// 對目標路徑進行設置,層級遞推若是不存在就建立。
mkPath(g_rootDir);
}
複製代碼
MMKV.defaultMMKV()
public static MMKV defaultMMKV() {
// 能夠設置爲多進程模式
// 重點所在
long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null); // *** -->
return new MMKV(handle); // 1 -->
}
// 1 -->
// 就是一個爲long類型的handle變量設置
private MMKV(long handle) {
nativeHandle = handle;
}
複製代碼
你能看到***
註釋位置是代碼中一個迷惑性行爲,經過數據類型定義可以知道最後獲得的數據是一個數據類型爲long
的數據,咱們能夠猜想這個數據的用處對應着最後可以用於尋找到對應的MMKV
,經過深層次調用後能夠發現他調用了一個mmkvWithID()
的方法,其中DEFAULT_MMAP_ID
爲mmkv.default
。安全
MMKV *MMKV::mmkvWithID(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath) {
// 。。。。。。
auto mmapKey = mmapedKVKey(mmapID, relativePath); // 1 -->
auto itr = g_instanceDic->find(mmapKey);
if (itr != g_instanceDic->end()) {
MMKV *kv = itr->second; // 2 -->
return kv;
}
if (relativePath) {
if (!isFileExist(*relativePath)) {
if (!mkPath(*relativePath)) {
return nullptr;
}
}
}
auto kv = new MMKV(mmapID, size, mode, cryptKey, relativePath); // 3 -->
(*g_instanceDic)[mmapKey] = kv;
return kv;
}
複製代碼
在這個代碼中總共有兩個核心部分:app
mmapID
和relativePath
兩個值進行必定的運算操做,具體關係就是mmapID
和relativePath
的重合關係,具體仍是要見於代碼實現。註釋2
和註釋3
,就是經過一個Map
的形式來對數據進行存儲,若是在g_instanceDic
這個變量中進行數據查詢。MMKV::MMKV(const string &mmapID, int size, MMKVMode mode, string *cryptKey, string *relativePath)
: m_mmapID(mmapedKVKey(mmapID, relativePath)) // historically Android mistakenly use mmapKey as mmapID
, m_path(mappedKVPathWithID(m_mmapID, mode, relativePath)) // 1 -->
, m_crcPath(crcPathWithID(m_mmapID, mode, relativePath))
, m_dic(nullptr)
, m_dicCrypt(nullptr)
, m_file(new MemoryFile(m_path, size, (mode & MMKV_ASHMEM) ? MMFILE_TYPE_ASHMEM : MMFILE_TYPE_FILE))
, m_metaFile(new MemoryFile(m_crcPath, DEFAULT_MMAP_SIZE, m_file->m_fileType))
, m_metaInfo(new MMKVMetaInfo())
, m_crypter(nullptr)
, m_lock(new ThreadLock())
, m_fileLock(new FileLock(m_metaFile->getFd(), (mode & MMKV_ASHMEM)))
, m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
, m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
, m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0 || (mode & CONTEXT_MODE_MULTI_PROCESS) != 0) {
m_actualSize = 0;
m_output = nullptr;
if (cryptKey && cryptKey->length() > 0) {
m_dicCrypt = new MMKVMapCrypt();
m_crypter = new AESCrypt(cryptKey->data(), cryptKey->length());
} else {
m_dic = new MMKVMap();
}
// 。。。。。。一些賦值操做
// sensitive zone
{
SCOPED_LOCK(m_sharedProcessLock);
loadFromFile();
}
}
複製代碼
和SharedPreferences
相同最後仍是須要經歷一場和文件讀寫的殊死搏鬥,那問題就來了,一樣是文件讀寫,爲何MMKV
可以以百倍的速度碾壓各種已成熟的產品呢?從個人思路出發能夠分爲這樣的幾種狀況:源碼分析
FastJson
就可以發現,數據的處理速度基本上可以有很是高的提高。可是這對於相對成熟的產品而言通常不會有這種方案。MMKV
的實現方案基本都是依靠JNI
來調度完成,而C
的處理速度和Java
相比想來咱們也是有目共睹的。SharedPreferences
和MMKV
二者都是咱們有目共睹須要對數據進行讀寫操做的,而數據的最後來源就是本地的文件,一個更易於讀寫的文件方案勢必是一個最關鍵的突破點。迴歸正題:loadFromFile();
post
在剛剛的猜測中,我說起了關於文件讀寫的問題,由於對MMKV
而言,文件讀寫這一關確定是躲不過去的,可是如何更高效就是咱們應該去思考的點了。性能
void MMKV::loadFromFile() {
// 文件不合法就從新加載
if (!m_file->isFileValid()) {
m_file->reloadFromFile();
}
// 文件依舊不合法就報錯
if (!m_file->isFileValid()) {
MMKVError("file [%s] not valid", m_path.c_str());
} else {
// 進入這一步至少說明文件是合法的,可是須要進行數據的校驗
// error checking
bool loadFromFile = false, needFullWriteback = false;
checkDataValid(loadFromFile, needFullWriteback);
auto ptr = (uint8_t *) m_file->getMemory();
// loading
if (loadFromFile && m_actualSize > 0) {
MMBuffer inputBuffer(ptr + Fixed32Size, m_actualSize, MMBufferNoCopy);
if (m_crypter) {
clearDictionary(m_dicCrypt);
} else {
clearDictionary(m_dic);
}
// 1 -->
if (needFullWriteback) {
if (m_crypter) {
MiniPBCoder::greedyDecodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::greedyDecodeMap(*m_dic, inputBuffer); // 2 -->
}
} else {
// 1 -->
if (m_crypter) {
MiniPBCoder::decodeMap(*m_dicCrypt, inputBuffer, m_crypter); // 2 -->
} else {
MiniPBCoder::decodeMap(*m_dic, inputBuffer); // 2 -->
}
}
m_output = new CodedOutputData(ptr + Fixed32Size, m_file->getFileSize() - Fixed32Size);
m_output->seek(m_actualSize); // 計算出數據量的實際大小
if (needFullWriteback) {
fullWriteback();
}
} else {
// 若是數據是不合法或者空的就直接丟棄。
// 。。。。。。
}
}
m_needLoadFromFile = false;
}
複製代碼
在代碼段中我標註出了註釋1
和註釋2
,也是我認爲相當重要的代碼了,分別作了兩大操做:優化
MMKV
的綜合性能可以強過SharedPreferences
。protobuf
做爲MMKV
最後的選擇方案在性能和空間佔用上都有不錯的表現。kv.encodeXXX("string", XXX);
這裏的代碼分析只拿一個做爲樣例便可
MMKV_JNI jboolean encodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean value) {
MMKV *kv = reinterpret_cast<MMKV *>(handle); // 1-->
if (kv && oKey) {
string key = jstring2string(env, oKey); // 將key再進行特殊的加工處理
return (jboolean) kv->set((bool) value, key); // 2 -->
}
return (jboolean) false;
}
複製代碼
關注幾個註釋點:
Java
這一層中進行的操做只是一個數據類型爲long
的handle
變量進行賦值操做,而這個handle
中在後期能夠被解析轉化爲已經初始化完成的MMKV
對象。bool MMKV::set(bool value, MMKVKey_t key) {
// 1. 進行數據的測量,並建立相同大小的區間
size_t size = pbBoolSize();
MMBuffer data(size);
// 2. 轉化爲CodedOutputData對象用於寫入
CodedOutputData output(data.getPtr(), size);
output.writeBool(value); // 3 -->
// 從名字就能知道這實際上是一個正式的數據替換操做
// 追溯後能夠發現會出現一個文件的寫入。
return setDataForKey(move(data), key);
}
// 3-->
void CodedOutputData::writeBool(bool value) {
// 用0和1來表示最後的數值
this->writeRawByte(static_cast<uint8_t>(value ? 1 : 0));
}
複製代碼
可是經過官方的文檔中可以知道,關於這個文件格式下的數據是存在問題的,那就是他並不支持增量更新 ,這也就意味着複雜的操做會更加多了,那騰訊的解決方案是什麼呢?
標準
protobuf
不提供增量更新的能力,每次寫入都必須全量寫入。考慮到主要使用場景是頻繁地進行寫入更新,咱們須要有增量更新的能力:將增量kv
對象序列化後,直接append
到內存末尾;這樣同一個key
會有新舊若干份數據,最新的數據在最後;那麼只需在程序啓動第一次打開mmkv
時,不斷用後讀入的value
替換以前的值,就能夠保證數據是最新有效的。
一句話講來就是,新的或更改過的就最後新增後面插入。
而新舊數據累加勢必會形成文件的龐大,那這方面MMKV
給出的解決方案又是怎麼樣的呢?
之內存
pagesize
爲單位申請空間,在空間用盡以前都是append
模式;當append
到文件末尾時,進行文件重整、key
排重,嘗試序列化保存排重結果;排重後空間仍是不夠用的話,將文件擴大一倍,直到空間足夠。
一樣的換成一句話來進行描述,有上限目標的文件重寫。
這一段的代碼實現就不貼出了,具體位置就在MMKV_IO
中的ensureMemorySize()
方法,經過已存在數據大小的總量來進行整理,由於不少時候數據量很大是由於大容量的數據的重複添加形成的。
kv.decodeXXX("string");
MMKV_JNI jboolean decodeBool(JNIEnv *env, jobject, jlong handle, jstring oKey, jboolean defaultValue) {
MMKV *kv = reinterpret_cast<MMKV *>(handle);
if (kv && oKey) {
string key = jstring2string(env, oKey);
return (jboolean) kv->getBool(key, defaultValue);
}
return defaultValue;
}
複製代碼
其實基本邏輯和寫文件的差很少了,這個時候仍是首先要獲取一個對應的MMKV
對象,而後完成數據的獲取。
bool MMKV::getBool(MMKVKey_t key, bool defaultValue) {
auto data = getDataForKey(key);
if (data.length() > 0) {
CodedInputData input(data.getPtr(), data.length());
return input.readBool();
}
return defaultValue;
}
複製代碼
轉化爲CodedInputData
的對象來完成數據的讀取,若是數據不存在,那就直接默認值返回。
kv.removeValueForKey("string")
在看代碼以前作一個思考,在已知的數據基礎上,換成你會怎麼作這樣的操做呢?
咱們要關注的點有如下幾個:
protobuf
是一個不支持增量更新的文件格式,相對應MMKV
給出的解決方案就是經過尾部增長,出現新舊數據疊加問題1
的引伸,新舊數據疊加的一個查詢和刪除問題,由於新舊數據,那麼作查詢的時候勢必要屢次的查,若是每次的數據都有1G
,那你的查詢每次都要疊加到1G
的程度,而不是查到便可開始刪除。對於以上問題思考清楚了的話,咱們就能夠給出MMKV
的解決方案了。
auto itr = m_dic->find(key);
if (itr != m_dic->end()) {
m_hasFullWriteback = false;
static MMBuffer nan; // ******
auto ret = appendDataWithKey(nan, itr->second); // ******
if (ret.first) {
#ifdef MMKV_APPLE
[itr->first release];
#endif
m_dic->erase(itr);
}
return ret.first;
}
複製代碼
將關注點所有放置於註釋帶*
的代碼段上,一個沒有賦值的MMBuffer
說明數據爲空,而後直接調用appendDataWithKey()
文件寫入的方案,說明最後出如今protobuf
的數據樣式會是這樣的。
message empty{
}
複製代碼
其實就是往裏面加一個新的空數據做爲新的數據。
從源碼分析完以後,和SharedPreferences
相比,從新整理後能夠總結爲如下幾點的突破:
mmap
的使用: 內存映射的技術的使用,減小了 SharedPreferences
的拷貝和提交的時間消耗。SharedPreferences
同樣的直接文件重構。一樣要注意這樣的方式會形成冗餘數據的增長。mmap
做爲突破口,來完成對其餘進程對當前文件的操做的一個狀態感知,主要就是分爲三方面:寫指針增加、內存重整、內存增加 。