錦囊篇|一文摸懂SharedPreferences和MMKV(二)

目錄

MMKV源碼分析

初始化 / 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_IDmmkv.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

  1. mmapKey的值的計算: 經過mmapIDrelativePath兩個值進行必定的運算操做,具體關係就是mmapIDrelativePath的重合關係,具體仍是要見於代碼實現。
  2. MMKV的生成: 這裏的解釋對應註釋2註釋3,就是經過一個Map的形式來對數據進行存儲,若是在g_instanceDic這個變量中進行數據查詢。

MMKV的內部結構

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可以以百倍的速度碾壓各種已成熟的產品呢?從個人思路出發能夠分爲這樣的幾種狀況:源碼分析

  1. 不夠健壯的錯誤數據處理。 這若是你作一個簡易版的FastJson就可以發現,數據的處理速度基本上可以有很是高的提高。可是這對於相對成熟的產品而言通常不會有這種方案。
  2. 底層進行數據處理。 這個方案的推行在必定程度上也是對應如今的二者對比有必定的道理,由於可以發現MMKV的實現方案基本都是依靠JNI來調度完成,而C的處理速度和Java相比想來咱們也是有目共睹的。
  3. 更優化的文件讀取方案。 這就是對當前方案的分析了,由於尚未看到後面的代碼,因此這裏是一種方案的猜想。由於SharedPreferencesMMKV二者都是咱們有目共睹須要對數據進行讀寫操做的,而數據的最後來源就是本地的文件,一個更易於讀寫的文件方案勢必是一個最關鍵的突破點。
  4. 。。。。。接下來由你開始進行更多的思考。

迴歸正題: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,也是我認爲相當重要的代碼了,分別作了兩大操做:優化

  1. 數據的寫回方案製做: 這是要一個很是有特點的地方,爲何這麼說呢?其實你可以從一個判斷的變量名可以看出會對數據的寫回方式有一個選擇,也就是部分寫回和所有寫回的策略之選,那這就是第一個緣由爲何MMKV的綜合性能可以強過SharedPreferences
  2. 文件格式的選擇: 其實這是解析時候的事情了,這一段的論證來源於 MMKV 原理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;
}
複製代碼

關注幾個註釋點:

  1. 註釋1: 這就是以前在上面的時候已經提到過的在Java這一層中進行的操做只是一個數據類型爲longhandle變量進行賦值操做,而這個handle中在後期能夠被解析轉化爲已經初始化完成的MMKV對象。
  2. 註釋2: 完成相對應的數據放置操做,那這裏就要觀察代碼的深層調度是一個怎麼樣的過程了。
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")

在看代碼以前作一個思考,在已知的數據基礎上,換成你會怎麼作這樣的操做呢?

咱們要關注的點有如下幾個:

  1. protobuf是一個不支持增量更新的文件格式,相對應MMKV給出的解決方案就是經過尾部增長,出現新舊數據疊加
  2. 問題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相比,從新整理後能夠總結爲如下幾點的突破:

  1. mmap的使用: 內存映射的技術的使用,減小了 SharedPreferences 的拷貝和提交的時間消耗。
  2. 數據的更新方式: 局部更新的數據,經過尾部追加來進行完成,而不是像SharedPreferences同樣的直接文件重構。一樣要注意這樣的方式會形成冗餘數據的增長。
  3. 多進程訪問安全的設計: 詳細見於MMKV for Android 多進程設計與實現,主要仍是以mmap做爲突破口,來完成對其餘進程對當前文件的操做的一個狀態感知,主要就是分爲三方面:寫指針增加、內存重整、內存增加

參考資料

  1. MMKV官方文檔

相關文章
相關標籤/搜索