Win32 C++离线安装器大型嵌入资源延迟加载方案咨询
嘿,这个问题我之前帮不少人解决过——完全可以通过延迟加载那个100MB的嵌入资源来让安装器秒启动!下面是两种基于微软工具链的可行方案,都是Win32原生支持的,你可以按需选择:
解决方案:两种延迟加载大资源的实现方式
方式一:将大资源附加到PE文件末尾(最简单易实现)
这种方法不需要修改编译/链接流程,只需要在构建脚本里把归档文件追加到安装器EXE后面,然后安装器运行时自己读取这部分数据。启动时系统完全不会加载这部分附加内容,因为它不在PE的标准加载范围内。
步骤1:构建时附加资源
用Windows自带的copy命令合并文件(注意/b参数表示二进制模式):
copy /b Installer.exe + archive.bin Installer_Final.exe
步骤2:安装器中按需读取附加的资源
在C++代码里,先获取自身的文件路径,打开文件,然后计算附加数据的起始位置(就是原EXE的大小),等UI就绪(比如进度条显示出来)后再读取这部分数据。可以分块读取来实时更新进度条:
#include <windows.h> #include <fstream> #include <vector> // 获取自身可执行文件的原始大小(即附加资源的起始偏移) DWORD GetSelfOriginalSize() { WCHAR szPath[MAX_PATH]; GetModuleFileNameW(NULL, szPath, MAX_PATH); WIN32_FILE_ATTRIBUTE_DATA fileAttrs; if (!GetFileAttributesExW(szPath, GetFileExInfoStandard, &fileAttrs)) { return 0; } // 这里假设你的EXE小于4GB,用nFileSizeLow足够 return fileAttrs.nFileSizeLow; } // 在UI就绪后调用,加载附加的归档资源 bool LoadAttachedArchive(std::vector<BYTE>& outArchiveData) { WCHAR szPath[MAX_PATH]; if (!GetModuleFileNameW(NULL, szPath, MAX_PATH)) { return false; } // 以二进制模式打开自身文件 std::ifstream selfFile(szPath, std::ios::binary | std::ios::ate); if (!selfFile.is_open()) { return false; } DWORD selfSize = GetSelfOriginalSize(); std::streampos totalFileSize = selfFile.tellg(); std::streampos archiveSize = totalFileSize - selfSize; if (archiveSize <= 0) { return false; } // 调整缓冲区大小并读取归档数据 outArchiveData.resize(static_cast<size_t>(archiveSize)); selfFile.seekg(selfSize); selfFile.read(reinterpret_cast<char*>(outArchiveData.data()), archiveSize); return selfFile.good(); }
方式二:使用自定义延迟加载节区(更符合PE规范)
如果你希望资源还是放在PE文件的标准节区里,可以创建一个自定义节区,设置节区属性为可丢弃、仅按需加载,这样系统在启动时不会把这个节区加载到内存,直到你手动访问它。
步骤1:修改资源嵌入方式
不要用标准的rc资源脚本嵌入大归档,而是把归档文件转换成C++头文件(比如用xxd -i archive.bin > archive.h,Windows下也有类似工具),然后在代码里指定把归档数据放到自定义节区:
#include "archive.h" // 里面包含const BYTE archive_data[]和size_t archive_size // 告诉链接器创建自定义节区".bigres",设置属性为可读、可丢弃 #pragma comment(linker, "/SECTION:.bigres,RD") // 把归档数据分配到这个自定义节区 __declspec(allocate(".bigres")) const BYTE g_ArchiveData[] = archive_data; const size_t g_ArchiveSize = archive_size;
步骤2:手动控制节区加载
默认情况下系统可能还是会加载节区,所以我们需要检查节区状态,未加载时手动映射:
#include <windows.h> #include <vector> #include <cstring> bool LoadArchiveSection(std::vector<BYTE>& outArchiveData) { MEMORY_BASIC_INFORMATION mbi; // 检查归档节区的内存状态 if (!VirtualQuery(g_ArchiveData, &mbi, sizeof(mbi))) { return false; } if (mbi.State == MEM_RESERVE) { // 节区未加载,手动映射文件中的对应部分 WCHAR szPath[MAX_PATH]; if (!GetModuleFileNameW(NULL, szPath, MAX_PATH)) { return false; } HANDLE hFile = CreateFileW(szPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) { return false; } HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (!hMapping) { CloseHandle(hFile); return false; } // 找到自定义节区在文件中的偏移和大小 HMODULE hModule = GetModuleHandle(NULL); PIMAGE_DOS_HEADER pDosHeader = reinterpret_cast<PIMAGE_DOS_HEADER>(hModule); PIMAGE_NT_HEADERS pNtHeaders = reinterpret_cast<PIMAGE_NT_HEADERS>(reinterpret_cast<BYTE*>(hModule) + pDosHeader->e_lfanew); PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pNtHeaders); for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; ++i) { if (strcmp(reinterpret_cast<char*>(pSectionHeader->Name), ".bigres") == 0) { const BYTE* pMappedData = reinterpret_cast<const BYTE*>(MapViewOfFile(hMapping, FILE_MAP_READ, 0, pSectionHeader->PointerToRawData, pSectionHeader->SizeOfRawData)); if (pMappedData) { outArchiveData.assign(pMappedData, pMappedData + g_ArchiveSize); UnmapViewOfFile(pMappedData); } break; } pSectionHeader++; } CloseHandle(hMapping); CloseHandle(hFile); } else { // 节区已加载,直接复制数据 outArchiveData.assign(g_ArchiveData, g_ArchiveData + g_ArchiveSize); } return !outArchiveData.empty(); }
额外优化小贴士
- 不管用哪种方式,都可以分块读取大资源,每读取一块就更新进度条,让用户看到安装在进行中;
- 用方式一的话,建议在构建时把原EXE的大小写入安装器的一个自定义小资源里,避免后续EXE修改后偏移计算错误;
- 测试时可以用
Process Explorer查看安装器的内存占用,确认启动时大资源没有被加载到内存。
内容的提问来源于stack exchange,提问作者Prasanna
相关产品推荐
相关产品推荐

