Qt内存映射到文件,解决打开大文件占用内存高的问题
前言
在开发桌面项目中经常会遇到打开大文件进行解析后对数据进行处理,如果文件太大,解析后的数据全部放到内存中会占用大量的内存,如果同时打开多个文件就会导致内存暴增,最终可能就会出现内存占满而程序崩溃。
解决这个问题,一个比较常见的方法就是实用内存映射到文件。
内存映射文件是一种将文件映射到内存中的技术,通过将文件的内容映射到内存中的一块地址空间,可以直接在内存中对文件进行读写操作,而不需要通过传统的文件IO操作,这一过程涉及创建文件句柄、建立内存映射对象、映射文件到进程空间以及解除映射等步骤。这种方式可以大大提高文件的读写效率,并且方便进行随机访问。
C++的开源库Boost中,可以使用boost::iostreams::mapped_file
类来实现内存映射文件的读写操作。该类提供了简单易用的接口,可以方便地进行文件的读写操作。
本文主要介绍Qt中的QFile来实现内存映射操作。使用也非常简单,但是过程中有些步骤需要注意。
正文
QFileDevice中提供了一个函数 map
用于现内存映射操作,函数定义如下:
uchar *QFileDevice::map(qint64 offset, qint64 size, QFileDevice::MemoryMapFlags flags = NoOptions)
使用map进行内存映射,unmap取消内存映射。
应用场景
示例场景一: 音频文件编辑
举一个实际的应用场景。
音频文件编辑功能,解析音频的pcm数据后,如果将数据全部放到内存中,然后对内存数据进行操作,理论上是没问题的,但是如果打开的文件比较大,几百兆甚至几个G,将解析的数据放到内存中就会导致内存暴增,如果同时要编辑多个大文件音频,对电脑内存无疑是个巨大的挑战。这种情况下,使用内存映射到文件的方式就是一种很好的选择。
首先将音频解析(如使用FFmpeg)出PCM数据,将PCM数据直接存储到本地文件中,然后将内存映射到该文件,通过指针去操作文件内容、如读取数据、更新数据等。
用法其实很简单:
QFile m_file;
uchar *m_dataPtr;m_file.setFileName(fileName);
if(m_file.open(QIODevice::ReadWrite)){m_dataPtr = m_file.map(0,m_file.size());//to do ...
}
注意,通过map映射后,m_file一定要是打开的状态,如果关闭文件,再操作m_dataPtr 指针将会报错。而且打开文件的方式必须是QIODevice::ReadWrite
类型,否则使用指针去更新内存数据时会导致崩溃。
如果使用完毕,要取消映射也很简单,通过unmap,然后关闭文件即可。
if(m_dataPtr){m_file.unmap(m_dataPtr);m_dataPtr = nullptr;
}
if(m_file.isOpen()){m_file.close();
}
注意,如果是在一个类中需要多次操作,可以将指针和文件对象定义成类的成员变量,这样就可以只打开一次文件,然后整个类都可以使用一个指针来操作。
另外,在映射的时候是指定了映射范围的,如 m_file.map(0,m_file.size());
是指定了整个文件的大小空间,当然也可以选择某个区间进行映射,这个自己决定就好。
需要注意的是,如果文件内容有新增或删除,通过映射的指针就无法完成,需要取消映射然后关闭文件后,通过QFile去写入新的文件内容,然后重新做映射。
示例场景二:拷贝文件
文件拷贝,大家都知道可以使用QFile::copy的方式来实现,但是对于大文件的拷贝,在实测中,使用内存映射后,直接进行内存拷贝的方式速度更快一些。
上代码:
bool fastCopyFile(const QString &sourceFilePath, const QString &destFilePath)
{//先做内存映射,再做内存拷贝,这种方式比直接使用 QFile::copy效率更高QFile sourceFile(sourceFilePath);QFile destFile(destFilePath);if (!sourceFile.open(QIODevice::ReadOnly)) return false;if (!destFile.open(QIODevice::ReadWrite)) { // 需要 ReadWrite 权限用于映射sourceFile.close();return false;}// 预分配目标文件大小qint64 fileSize = sourceFile.size();destFile.resize(fileSize);// 映射源文件uchar *srcMap = sourceFile.map(0, fileSize);if (!srcMap) {sourceFile.close();destFile.close();return false;}// 映射目标文件uchar *destMap = destFile.map(0, fileSize);if (!destMap) {sourceFile.unmap(srcMap);sourceFile.close();destFile.close();return false;}// 直接内存拷贝memcpy(destMap, srcMap, fileSize);// 解除映射并关闭文件sourceFile.unmap(srcMap);destFile.unmap(destMap);sourceFile.close();destFile.close();return true;
}
这里其实就是用了内存映射到文件的功能,直接操作指针,进行内存拷贝。