Android MMKV底层原理之流程分析
前言
数据持久化是 Android
开发过程中必然要面对的一个课题,轻量级的有 Android
系统 本身就支持的SharedPreferences
,重量级的有 SQLite
。根据项目需求的不同,数据库可能大部分项目都用不到,但是轻量化的 K-V
持久化工具几乎是个 Android
项目都要用到。
SharedPreferences
的缺陷
SharedPreferences
是Google
官方提供的一套K-V
持久化组件,也是从我们作为Android
开发“出生”起一直在用的一套工具,但是它的缺陷也是很致命的:
- 占用内存
-
getValue
时可能导致ANR
- 不支持多进程
- 不支持局部更新
-
commit
或apply
都可能导致ANR
- 占用内存
ContextImpl
中有一个 ArrayMap
对象来缓存不同packageName
下的所有sp
文件对象
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;
...
}
而且针对这个 map,只有 put
而没有释放的操作
-
ANR
问题
sp
初始化的时候,会去启动子线程读取保存在本地的xml
文件,这个读取操作是加了锁的,如果当数据量很大,或者这个读取操作未完成的时候,去对sp
进行读写,会一直等待锁释放,而大多数我们的项目中,对于sp
的操作都是在直接使用的,并未去切换线程执行,因此是个隐患。
- 多进程问题
讲到这里,放一个 SharedPreferences
类上面注释中的一句话
Note: This class does not support use across multiple processes.
因为没有使用跨进程的锁,官方也不建议使用sp
来进行跨进程通信。跨进程场景下,当然数据库也是一个很可靠的方案,但是考虑到轻量级K-V
的场景,我们还是需要三思一下。当然还有一些三方库基于ContentProvider
实现了跨进程版的SharedPreferences
参见XSharedPref,但是基于ContentProvider
,也有启动慢,访问也慢的通病。
其实抛开sp
也好MMKV
也好,如果,如果有一种方案,能给人一种操作内存的速度+读写硬盘的效果
该多好。
细数了sp
的这些问题,接下来就开始引入MMKV
了。 先看一下性能对比:
单进程读写性能
img多进程读写性能
img几乎是碾压式的优势了
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
基础概念
img
mmap
是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read
,write
等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
-
mmap
和常规文件操作的区别
常规文件操作:
- 进程发起读文件请求。
- 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的
inode
。 -
inode
在address_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()
函数,拿到映射区指针,供后续读写使用。
作者:Fourier
转载来源于:https://juejin.cn/post/7078640657807441934
如有侵权,请联系删除!