微信MMKV原始碼分析(一) | 整體流程

ysbing發表於2018-09-27

初始化

在使用MMKV框架前,需呼叫以下方法進行初始化

MMKV.initialize(context);
複製程式碼

這裡的Java層主要是獲取到儲存檔案的路徑,傳入Native層,這裡預設的路徑是APP的內部儲存目錄下的mmkv路徑,這裡不支援修改,如需修改,需將原始碼clone下來手動修改編譯了。

public static String initialize(Context context) {
        String rootDir = context.getFilesDir().getAbsolutePath() + "/mmkv";
        initialize(rootDir);
        return rootDir;
    }
複製程式碼

到了Native層,通過Java_com_tencent_mmkv_MMKV_initialize方法跳轉到MMKV::initializeMMKV方法裡,啟動了一個執行緒做初始化,然後檢查內部路徑是否存在,不存在則建立之。

void MMKV::initializeMMKV(const std::string &rootDir) {
    static pthread_once_t once_control = PTHREAD_ONCE_INIT;
    pthread_once(&once_control, initialize);

    g_rootDir = rootDir;
    char *path = strdup(g_rootDir.c_str());
    mkPath(path);
    free(path);

    MMKVInfo("root dir: %s", g_rootDir.c_str());
}
複製程式碼

獲取MMKV物件

獲取MMKV物件的方法有以下幾個,最傻瓜式的defaultMMKV到最複雜的mmkvWithAshmemID方法,按需呼叫。

public MMKV defaultMMKV();

public MMKV defaultMMKV(int mode, String cryptKey);
    
public MMKV mmkvWithID(String mmapID);

public MMKV mmkvWithID(String mmapID, int mode);

public MMKV mmkvWithID(String mmapID, int mode, String cryptKey);
    
@Nullable
public MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, String cryptKey);
複製程式碼

上面的方法,基本都會來到getMMKVWithID方法,然後跳轉到MMKV::mmkvWithID裡

MMKV *MMKV::mmkvWithID(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey) {
    if (mmapID.empty()) {
        return nullptr;
    }
    SCOPEDLOCK(g_instanceLock);

    auto itr = g_instanceDic->find(mmapID);
    if (itr != g_instanceDic->end()) {
        MMKV *kv = itr->second;
        return kv;
    }
    auto kv = new MMKV(mmapID, size, mode, cryptKey);
    (*g_instanceDic)[mmapID] = kv;
    return kv;
}
複製程式碼

g_instanceDic是Map物件,先是根據mmapID在g_instanceDic進行查詢,有直接返回,沒就新建一個MMKV物件,然後再新增到g_instanceDic裡。

MMKV::MMKV(const std::string &mmapID, int size, MMKVMode mode, string *cryptKey)
    : m_mmapID(mmapID)
    , m_path(mappedKVPathWithID(m_mmapID, mode))
    , m_crcPath(crcPathWithID(m_mmapID, mode))
    , m_metaFile(m_crcPath, DEFAULT_MMAP_SIZE, (mode & MMKV_ASHMEM) ? MMAP_ASHMEM : MMAP_FILE)
    , m_crypter(nullptr)
    , m_fileLock(m_metaFile.getFd())
    , m_sharedProcessLock(&m_fileLock, SharedLockType)
    , m_exclusiveProcessLock(&m_fileLock, ExclusiveLockType)
    , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0)
    , m_isAshmem((mode & MMKV_ASHMEM) != 0) {
    m_fd = -1;
    m_ptr = nullptr;
    m_size = 0;
    m_actualSize = 0;
    m_output = nullptr;

    if (m_isAshmem) {
        m_ashmemFile = new MmapedFile(m_mmapID, static_cast<size_t>(size), MMAP_ASHMEM);
        m_fd = m_ashmemFile->getFd();
    } else {
        m_ashmemFile = nullptr;
    }

    if (cryptKey && cryptKey->length() > 0) {
        m_crypter = new AESCrypt((const unsigned char *) cryptKey->data(), cryptKey->length());
    }

    m_needLoadFromFile = true;

    m_crcDigest = 0;

    m_sharedProcessLock.m_enable = m_isInterProcess;
    m_exclusiveProcessLock.m_enable = m_isInterProcess;

    // sensitive zone
    {
        SCOPEDLOCK(m_sharedProcessLock);
        loadFromFile();
    }
}
複製程式碼

MMKV的建構函式裡,做了一系列引數的構造,分別有:

  • m_mmapID:檔名
  • m_path:存放路徑
  • m_crcPath:校驗檔案存放路徑
  • m_metaFile:記憶體對映的管理物件
  • m_crypter:AES加密金鑰
  • m_lock:執行緒鎖
  • m_fileLock:檔案鎖
  • m_sharedProcessLock:對映檔案到記憶體的鎖
  • m_exclusiveProcessLock:在記憶體讀寫資料時的鎖
  • m_isInterProcess:是否主程式
  • m_isAshmem:是否匿名記憶體
  • m_ptr:檔案對映到記憶體後的地址
  • m_size:檔案大小
  • m_actualSize:記憶體大小,這個會因為寫資料動態變化
  • m_output:Protobuf物件,用於寫檔案,效率之所高,這裡也很關鍵
  • m_ashmemFile:匿名記憶體的檔案物件
  • m_needLoadFromFile:一個標識物件,用於是否載入過檔案,載入過就將它置為false
  • m_crcDigest:資料校驗

MMKV物件構造完畢後,會將該物件的指標地址返回給Java層,Java層的MMKV類會儲存住該地址,用於接下來的讀寫操作。

public static MMKV mmkvWithID(String mmapID, int mode, String cryptKey) {
    long handle = getMMKVWithID(mmapID, mode, cryptKey);
    return new MMKV(handle);
}
複製程式碼

寫資料

以寫入String物件為例,看看寫入步驟

public boolean encode(String key, String value) {
    return encodeString(nativeHandle, key, value);
}
複製程式碼

來到MMKV::setStringForKey方法

bool MMKV::setStringForKey(const std::string &value, const std::string &key) {
    if (key.empty()) {
        return false;
    }
    auto data = MiniPBCoder::encodeDataWithObject(value);
    return setDataForKey(std::move(data), key);
}
複製程式碼

MiniPBCoder::encodeDataWithObject方法將value構造出一個Protobuf資料物件(本章不對此詳細分析),然後將構造出來的資料物件通過std::move方法傳到setDataForKey裡

bool MMKV::setDataForKey(MMBuffer &&data, const std::string &key) {
    if (data.length() == 0 || key.empty()) {
        return false;
    }
    SCOPEDLOCK(m_lock);
    SCOPEDLOCK(m_exclusiveProcessLock);
    checkLoadData();

    // m_dic[key] = std::move(data);
    auto itr = m_dic.find(key);
    if (itr == m_dic.end()) {
        itr = m_dic.emplace(key, std::move(data)).first;
    } else {
        itr->second = std::move(data);
    }

    return appendDataWithKey(itr->second, key);
}
複製程式碼
  • checkLoadData()用來檢查檔案有效性(本章不對此詳細分析)
  • m_dic是一個Map物件,在這裡判斷是否已經存在該Key,有就替換,沒就新增
  • appendDataWithKey()是將該物件新增到記憶體裡(本章不對此詳細分析)

讀資料

public String decodeString(String key, String defaultValue) {
    return decodeString(nativeHandle, key, defaultValue);
}
複製程式碼

來到MMKV::getDataForKey方法

const MMBuffer &MMKV::getDataForKey(const std::string &key) {
    SCOPEDLOCK(m_lock);
    checkLoadData();
    auto itr = m_dic.find(key);
    if (itr != m_dic.end()) {
        return itr->second;
    }
    static MMBuffer nan(0);
    return nan;
}
複製程式碼

通過key在m_dic物件裡進行查詢,如果查詢到,就返回,沒則返回一個0長度的物件

到此,整體流程講完了,還有很多原理性的東西,在後面的章節再講,其中mmap對映的操作,還有protobuf的讀寫原理等。
如果本文對您有用的話,記得點一個贊哦!

相關文章