MMKV主成分分析1.プロセス分析

序文

データの永続性は、Android開発。軽量のものはAndroidシステム自体によってサポートされ、SharedPreferences重量のものはサポートされSQLiteます。プロジェクトの要件によっては、データベースはほとんどのプロジェクトで使用されない場合がありますが、軽量のK-V永続化ツールはほとんどAndroidすべてのプロジェクトで使用されます。

SharedPreferences欠陥

SharedPreferencesこれは永続的なコンポーネントGoogleの公式セットであり、開発者として「生まれた」ときから使用しているツールのセットでもありますが、その欠陥も致命的です。K-VAndroid

  • 使用済み内部メモリ
  • getValue引き起こすかもしれないANR
  • マルチプロセッシングはサポートされていません
  • 部分的な更新はサポートされていません
  • commitまたはapply原因となる可能性がありますANR
  1. 使用済み内部メモリ

ContextImpl異なるファイルオブジェクトをすべてキャッシュするArrayMapオブジェクトますpackageNamesp

class ContextImpl extends Context {
    private final static String TAG = "ContextImpl";
    private final static boolean DEBUG = false;

    /**
     * Map from package name, to preference name, to cached preferences.
     */
    @GuardedBy("ContextImpl.class")
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
    
    ...
}
复制代码

そして、このマップには、リリース操作のみputが、リリース操作はありません

  1. ANR質問

sp初期化中、子スレッドを起動してローカルに保存されているファイルを読み取りますxml。この読み取り操作はロックされています。データ量が多い場合、または読み取り操作が完了していない場合は、sp読み取りと書き込みを行います。常にロックを待機します。リリースされる予定であり、ほとんどのプロジェクトでspは、操作が直接使用され、スレッドの実行が切り替えられないため、隠れた危険があります。

  1. マルチプロセッシングの問題

そういえば、SharedPreferencesクラス

Note: This class does not support use across multiple processes.
复制代码

因为没有使用跨进程的锁,官方也不建议使用sp来进行跨进程通信。跨进程场景下,当然数据库也是一个很可靠的方案,但是考虑到轻量级K-V的场景,我们还是需要三思一下。当然还有一些三方库基于ContentProvider实现了跨进程版的SharedPreferences参见XSharedPref,但是基于ContentProvider,也有启动慢,访问也慢的通病。

其实抛开sp也好MMKV也好,如果,如果有一种方案,能给人一种操作内存的速度+读写硬盘的效果该多好。

细数了sp的这些问题,接下来就开始引入MMKV了。 先看一下性能对比:

单进程读写性能 image.png 多进程读写性能

image.png

几乎是碾压式的优势了

MMKV

MMKV原本是腾讯基于mmap 内存映射文件用来iOS端记录日志使用的 K-V组件,后来延伸到Android端并拓展了多进程使用的场景,并开源的一个项目。

初始化入口

MMKV.initialize(context)
复制代码

java层的调用主要是获取保存文件地址传入到 Native层,默认是保存到App内部存储目录下:

public static String initialize(Context context) {
    String root = context.getFilesDir().getAbsolutePath() + "/mmkv";
    MMKVLogLevel logLevel = BuildConfig.DEBUG ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelInfo;
    return initialize(context, root, null, logLevel);
}
复制代码

当然这里的目录不需要调用侧去确保存在,Native曾会有这个判断,没有就会创建:

void MMKV::initializeMMKV(const MMKVPath_t &rootDir, MMKVLogLevel logLevel) {
    g_currentLogLevel = logLevel;

    ThreadLock::ThreadOnce(&once_control, initialize);

    g_rootDir = rootDir;
    mkPath(g_rootDir);

    MMKVInfo("root dir: " MMKV_PATH_FORMAT, g_rootDir.c_str());
}
复制代码

实例获取

获取MMKV进行操作,java层主要有以下几个方法:

/**
 * Create an MMKV instance with an unique ID (in single-process mode).
 * @param mmapID The unique ID of the MMKV instance.
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV mmkvWithID(String mmapID) throws RuntimeException {
    ...
    long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, null);
    return checkProcessMode(handle, mmapID, SINGLE_PROCESS_MODE);
}

/**
 * Create an MMKV instance in single-process or multi-process mode.
 * @param mmapID The unique ID of the MMKV instance.
 * @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV mmkvWithID(String mmapID, int mode) throws RuntimeException {
    ...
    long handle = getMMKVWithID(mmapID, mode, null, null);
    return checkProcessMode(handle, mmapID, mode);
}

/**
 * Create an MMKV instance in customize process mode, with an encryption key.
 * @param mmapID The unique ID of the MMKV instance.
 * @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
 * @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey) throws RuntimeException {
    ...
    long handle = getMMKVWithID(mmapID, mode, cryptKey, null);
    return checkProcessMode(handle, mmapID, mode);
}

/**
 * Create an MMKV instance in customize folder.
 * @param mmapID The unique ID of the MMKV instance.
 * @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV mmkvWithID(String mmapID, String rootPath) throws RuntimeException {
    ...
    long handle = getMMKVWithID(mmapID, SINGLE_PROCESS_MODE, null, rootPath);
    return checkProcessMode(handle, mmapID, SINGLE_PROCESS_MODE);
}

/**
 * Create an MMKV instance with customize settings all in one.
 * @param mmapID The unique ID of the MMKV instance.
 * @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
 * @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
 * @param rootPath The folder of the MMKV instance, defaults to $(FilesDir)/mmkv.
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV mmkvWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath)
    throws RuntimeException {
    ...
    long handle = getMMKVWithID(mmapID, mode, cryptKey, rootPath);
    return checkProcessMode(handle, mmapID, mode);
}

/**
 * Get an backed-up MMKV instance with customize settings all in one.
 * @param mmapID The unique ID of the MMKV instance.
 * @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
 * @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
 * @param rootPath The backup folder of the MMKV instance.
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV backedUpMMKVWithID(String mmapID, int mode, @Nullable String cryptKey, String rootPath)
        throws RuntimeException {
    ...
    long handle = getMMKVWithID(mmapID, mode, cryptKey, rootPath);
    return checkProcessMode(handle, mmapID, mode);
}

/**
 * Create an MMKV instance base on Anonymous Shared Memory, aka not synced to any disk files.
 * @param context The context of Android App, usually from Application.
 * @param mmapID The unique ID of the MMKV instance.
 * @param size The maximum size of the underlying Anonymous Shared Memory.
 *            Anonymous Shared Memory on Android can't grow dynamically, must set an appropriate size on creation.
 * @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
 * @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV mmkvWithAshmemID(Context context, String mmapID, int size, int mode, @Nullable String cryptKey)
    throws RuntimeException {
    ...
    long handle = getMMKVWithIDAndSize(mmapID, size, mode, cryptKey);
    if (handle != 0) {
        return new MMKV(handle);
    }
    throw new IllegalStateException("Fail to create an Ashmem MMKV instance [" + mmapID + "]");
}

/**
 * Create the default MMKV instance in single-process mode.
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV defaultMMKV() throws RuntimeException {
    ...
    long handle = getDefaultMMKV(SINGLE_PROCESS_MODE, null);
    return checkProcessMode(handle, "DefaultMMKV", SINGLE_PROCESS_MODE);
}

/**
 * Create the default MMKV instance in customize process mode, with an encryption key.
 * @param mode The process mode of the MMKV instance, defaults to {@link #SINGLE_PROCESS_MODE}.
 * @param cryptKey The encryption key of the MMKV instance (no more than 16 bytes).
 * @throws RuntimeException if there's an runtime error.
 */
public static MMKV defaultMMKV(int mode, @Nullable String cryptKey) throws RuntimeException {
    ...
    long handle = getDefaultMMKV(mode, cryptKey);
    return checkProcessMode(handle, "DefaultMMKV", mode);
}
复制代码

最终都会来到Native层的getMMKVWithID函数上来

MMKV_JNI jlong getMMKVWithID(JNIEnv *env, jobject, jstring mmapID, jint mode, jstring cryptKey, jstring rootPath) {
    MMKV *kv = nullptr;
    if (!mmapID) {
        return (jlong) kv;
    }
    string str = jstring2string(env, mmapID);

    bool done = false;
    if (cryptKey) {
        string crypt = jstring2string(env, cryptKey);
        if (crypt.length() > 0) {
            if (rootPath) {
                string path = jstring2string(env, rootPath);
                kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, &path);
            } else {
                kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, &crypt, nullptr);
            }
            done = true;
        }
    }
    if (!done) {
        if (rootPath) {
            string path = jstring2string(env, rootPath);
            kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, &path);
        } else {
            kv = MMKV::mmkvWithID(str, DEFAULT_MMAP_SIZE, (MMKVMode) mode, nullptr, nullptr);
        }
    }

    return (jlong) kv;
}
复制代码

再来到MMKV::mmkvWithID函数上:

MMKV *MMKV::mmkvWithID(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath) {

    if (mmapID.empty()) {
        return nullptr;
    }
    SCOPED_LOCK(g_instanceLock);

    auto mmapKey = mmapedKVKey(mmapID, rootPath);
    auto itr = g_instanceDic->find(mmapKey);
    if (itr != g_instanceDic->end()) {
        MMKV *kv = itr->second;
        return kv;
    }

    if (rootPath) {
        MMKVPath_t specialPath = (*rootPath) + MMKV_PATH_SLASH + SPECIAL_CHARACTER_DIRECTORY_NAME;
        if (!isFileExist(specialPath)) {
            mkPath(specialPath);
        }
        MMKVInfo("prepare to load %s (id %s) from rootPath %s", mmapID.c_str(), mmapKey.c_str(), rootPath->c_str());
    }

    auto kv = new MMKV(mmapID, mode, cryptKey, rootPath);
    kv->m_mmapKey = mmapKey;
    (*g_instanceDic)[mmapKey] = kv;
    return kv;
}
复制代码

可以看出这里先从g_instanceDic中查找是否有满足条件的MMKV实例,有就返回,没有就创建

new MMKV(mmapID, mode, cryptKey, rootPath)
复制代码

然后添加到g_instanceDic中。 获取到 Native层的对象指针地址后,java 层在MMKV类中会保存下来,提供给后续的读写操作实际使用

long handle = getMMKVWithID(mmapID, mode, cryptKey, rootPath);
复制代码

数据写入

@Override
public Editor putString(String key, @Nullable String value) {
    encodeString(nativeHandle, key, value);
    return this;
}
复制代码

Native

MMKV_JNI jboolean encodeString(JNIEnv *env, jobject, jlong handle, jstring oKey, jstring oValue) {
    MMKV *kv = reinterpret_cast<MMKV *>(handle);
    if (kv && oKey) {
        string key = jstring2string(env, oKey);
        if (oValue) {
            string value = jstring2string(env, oValue);
            return (jboolean) kv->set(value, key);
        } else {
            kv->removeValueForKey(key);
            return (jboolean) true;
        }
    }
    return (jboolean) false;
}
复制代码

来到MMKV:set函数里:

bool MMKV::set(const string &value, MMKVKey_t key) {
    if (isKeyEmpty(key)) {
        return false;
    }
    return setDataForKey(MMBuffer((void *) value.data(), value.length(), MMBufferNoCopy), key, true);
}
复制代码

setDataForKey函数

bool MMKV::setDataForKey(MMBuffer &&data, MMKVKey_t key, bool isDataHolder) {
    if ((!isDataHolder && data.length() == 0) || isKeyEmpty(key)) {
        return false;
    }
    ...

#ifndef MMKV_DISABLE_CRYPT
    if (m_crypter) {
        if (isDataHolder) {
            auto sizeNeededForData = pbRawVarint32Size((uint32_t) data.length()) + data.length();
            if (!KeyValueHolderCrypt::isValueStoredAsOffset(sizeNeededForData)) {
                data = MiniPBCoder::encodeDataWithObject(data);
                isDataHolder = false;
            }
        }
        auto itr = m_dicCrypt->find(key);
        if (itr != m_dicCrypt->end()) {
            ...
            if (!ret.first) {
                return false;
            }
            if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
                KeyValueHolderCrypt kvHolder(ret.second.keySize, ret.second.valueSize, ret.second.offset);
                memcpy(&kvHolder.cryptStatus, &t_status, sizeof(t_status));
                itr->second = move(kvHolder);
            } else {
                itr->second = KeyValueHolderCrypt(move(data));
            }
        } else {
            auto ret = appendDataWithKey(data, key, isDataHolder);
            if (!ret.first) {
                return false;
            }
            if (KeyValueHolderCrypt::isValueStoredAsOffset(ret.second.valueSize)) {
                auto r = m_dicCrypt->emplace(
                    key, KeyValueHolderCrypt(ret.second.keySize, ret.second.valueSize, ret.second.offset));
                if (r.second) {
                    memcpy(&(r.first->second.cryptStatus), &t_status, sizeof(t_status));
                }
            } else {
                m_dicCrypt->emplace(key, KeyValueHolderCrypt(move(data)));
            }
        }
    } else
#endif // MMKV_DISABLE_CRYPT
    {
        auto itr = m_dic->find(key);
        if (itr != m_dic->end()) {
            auto ret = appendDataWithKey(data, itr->second, isDataHolder);
            if (!ret.first) {
                return false;
            }
            itr->second = std::move(ret.second);
        } else {
            auto ret = appendDataWithKey(data, key, isDataHolder);
            if (!ret.first) {
                return false;
            }
            m_dic->emplace(key, std::move(ret.second));
        }
    }
    m_hasFullWriteback = false;
#ifdef MMKV_APPLE
    [key retain];
#endif
    return true;
}
复制代码

data = MiniPBCoder::encodeDataWithObject(data);data转换为一个protobuf对象然后通过appendDataWithKey()调用doAppendDataWithKey()最终再通过writeRawData写入到内存中:

void CodedOutputData::writeRawData(const MMBuffer &data) {
    size_t numberOfBytes = data.length();
    if (m_position + numberOfBytes > m_size) {
        auto msg = "m_position: " + to_string(m_position) + ", numberOfBytes: " + to_string(numberOfBytes) +
                   ", m_size: " + to_string(m_size);
        throw out_of_range(msg);
    }
    memcpy(m_ptr + m_position, data.getPtr(), numberOfBytes);
    m_position += numberOfBytes;
}
复制代码

可以看出实际上最终是通过库函数memcpy内存拷贝来将数据写入到目标内存中,那么这个目标内存怎么来的呢,接着来看。 这是写数据,读数据getDataForKey大同小异不再赘述。

mmap

MMKV的核心基于 mmap,之所以他比sp要快很多,也是mmap的特性使然

mmap基础概念

mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用readwrite等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。

image.png

  1. mmap和常规文件操作的区别

常规文件操作:

  • 进程发起读文件请求。
  • 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的inode
  • inodeaddress_space上查找要请求的文件页是否已经缓存在页缓存中。如果存在,则直接返回这片文件页的内容。
  • 如果不存在,则通过inode定位到文件磁盘地址,将数据从磁盘复制到页缓存。之后再次发起读页面过程,进而将页缓存中的数据发给用户进程。

总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

mmap 内存映射原理

  • 进程启动映射过程,并在虚拟地址空间中为映射创建虚拟映射区域
  • 调用内核空间的系统调用函数mmap(不同于用户空间函数),实现文件物理地址和进程虚拟地址的一一映射关系

注:前两个阶段仅在于创建虚拟区间并完成地址映射,但是并没有将任何文件数据的拷贝至主存。真正的文件读取是当进程发起读或写操作时。

  • 进程发起对这片映射空间的访问,引发缺页异常,实现文件内容到物理内存(主存)的拷贝

注:修改过的脏页面并不会立即更新回文件中,而是有一段时间的延迟,可以调用msync()来强制同步, 这样所写的内容就能立即保存到文件里了。

总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。 说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此mmap效率更高。

mmap优点总结

  • 对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

  • 实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。

  • 提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。

     同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。

  • 可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。

mmap的函数原型:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
复制代码
  • start:映射区的开始地址。设置null即可。

  • length:映射区的长度。传入文件对齐后的大小m_size

  • prot:期望的内存保护标志,不能与文件的打开模式冲突。设置可读可写。

  • flags:指定映射对象的类型,映射选项和映射页是否可以共享。设置MAP_SHARED表示可进程共享,MMKV之所以可以实现跨进程使用,这里是关键。

  • fd:有效的文件描述词。用上面所打开的m_fd

  • off_toffset:被映射对象内容的起点。从头开始,比较好理解。

再次来到 MMKV初始化函数中

MMKV::MMKV(const string &mmapID, MMKVMode mode, string *cryptKey, MMKVPath_t *rootPath)
    : m_mmapID(mmapID)
    , m_path(mappedKVPathWithID(m_mmapID, mode, rootPath))
    , m_crcPath(crcPathWithID(m_mmapID, mode, rootPath))
    , m_dic(nullptr)
    , m_dicCrypt(nullptr)
    , m_file(new MemoryFile(m_path))
    , m_metaFile(new MemoryFile(m_crcPath))
    , m_metaInfo(new MMKVMetaInfo())
    , m_crypter(nullptr)
    , m_lock(new ThreadLock())
    , m_fileLock(new FileLock(m_metaFile->getFd()))
    , m_sharedProcessLock(new InterProcessLock(m_fileLock, SharedLockType))
    , m_exclusiveProcessLock(new InterProcessLock(m_fileLock, ExclusiveLockType))
    , m_isInterProcess((mode & MMKV_MULTI_PROCESS) != 0)
复制代码

来看MemoryFile构造:

MemoryFile::MemoryFile(string path, size_t size, FileType fileType)
    : m_diskFile(std::move(path), OpenFlag::ReadWrite | OpenFlag::Create, size, fileType), m_ptr(nullptr), m_size(0), m_fileType(fileType) {
    if (m_fileType == MMFILE_TYPE_FILE) {
        reloadFromFile();
    } else {
        if (m_diskFile.isFileValid()) {
            m_size = m_diskFile.m_size;
            auto ret = mmap();
            if (!ret) {
                doCleanMemoryCache(true);
            }
        }
    }
}
复制代码

如果是普通文件,就回去执行 reloadFromFile函数:

void MemoryFile::reloadFromFile() {
    ...
    if (!m_diskFile.open()) {
        MMKVError("fail to open:%s, %s", m_diskFile.m_path.c_str(), strerror(errno));
    } else {
        FileLock fileLock(m_diskFile.m_fd);
        InterProcessLock lock(&fileLock, ExclusiveLockType);
        SCOPED_LOCK(&lock);

        mmkv::getFileSize(m_diskFile.m_fd, m_size);
        // round up to (n * pagesize)
        if (m_size < DEFAULT_MMAP_SIZE || (m_size % DEFAULT_MMAP_SIZE != 0)) {
            size_t roundSize = ((m_size / DEFAULT_MMAP_SIZE) + 1) * DEFAULT_MMAP_SIZE;
            truncate(roundSize);
        } else {
            auto ret = mmap();
            if (!ret) {
                doCleanMemoryCache(true);
            }
        }
#    ifdef MMKV_IOS
        tryResetFileProtection(m_diskFile.m_path);
#    endif
    }
}
复制代码

初始化时已经知道回去创建文件并打开了,会先判断文件大小,如果不是DEFAULT_MMAP_SIZE的倍数,就会去调用truncate去进行扩容,有效减少内存碎片:

bool MemoryFile::truncate(size_t size) {
        ...
        if (::ftruncate(m_diskFile.m_fd, static_cast<off_t>(m_size)) != 0) {
            MMKVError("fail to truncate [%s] to size %zu, %s", m_diskFile.m_path.c_str(), m_size, strerror(errno));
            m_size = oldSize;
            return false;
        }
        if (m_size > oldSize) {
            if (!zeroFillFile(m_diskFile.m_fd, oldSize, m_size - oldSize)) {
                MMKVError("fail to zeroFile [%s] to size %zu, %s", m_diskFile.m_path.c_str(), m_size, strerror(errno));
                m_size = oldSize;
                return false;
            }
        }

    if (m_ptr) {
        if (munmap(m_ptr, oldSize) != 0) {
            MMKVError("fail to munmap [%s], %s", m_diskFile.m_path.c_str(), strerror(errno));
        }
    }
    auto ret = mmap();
    if (!ret) {
        doCleanMemoryCache(true);
    }
    return ret;
}
复制代码

如果是DEFAULT_MMAP_SIZE的倍数,就会正常走 mmap()函数,拿到映射区指针,供后续读写使用。

参考文章

Tencent/MMKV/C++ Core

认真分析mmap:是什么 为什么 怎么用

微信MMKV源码分析(二) | mmap映射

存储性能优化 MMKV源码解析

おすすめ

転載: juejin.im/post/7078640657807441934
おすすめ