You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 06:57:29