热更新:最简单的解释就是不关软件直接更新,更新期间软件直接用。
应用程序使用热更新具有以下好处:
1.增强应用程序的可扩展性和灵活性
2.减少应用程序的停机时间,提高用户体验。
3.更快地部署新功能和修复程序错
4.节省资源,降低维护成本
本文将介绍一种PC端常见的热更新手法——动态加载链接库
其实现热更新的基本流程如下:
1.将应用程序分成两部分:静态部分和动态部分。静态部分是应用程序的核心代码和资源,动态部分是应用程序的插件或扩展。
2.将动态部分打包成DLL文件,同时在应用程序中添加加载和卸载DLL文件的代码。
3.在应用程序启动时加载DLL文件,并将其加载到内存中。这时,应用程序可以动态调用DLL中的函数或方法,执行相关的功能。
4.在应用程序运行时,如果需要更新DLL文件,可以将新版本的DLL文件替换旧版本的DLL文件,并重新加载DLL文件。这样,应用程序就能够在不停机的情况下实现热更新。
以下是PC热更新代码实现:
动态加载内存:
MmLoadDll.h
点击查看代码
#ifndef _MM_LOAD_DLL_H_ #define _MM_LOAD_DLL_H_ #include <Windows.h> typedef BOOL(__stdcall *typedef_DllMain)(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved); void ShowError(CHAR *lpszText); // 模拟LoadLibrary加载内存DLL文件到进程中 // lpData: 内存DLL文件数据的基址 // dwSize: 内存DLL文件的内存大小 // 返回值: 内存DLL加载到进程的加载基址 LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize); // 根据PE结构,获取PE文件加载到内存后的镜像大小 // lpData: 内存DLL文件数据的基址 // 返回值: 返回PE文件结构中IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage值的大小 DWORD GetSizeOfImage(LPVOID lpData); // 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中 // lpData: 内存DLL文件数据的基址 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL MmMapFile(LPVOID lpData, LPVOID lpBaseAddress); // 对齐SectionAlignment // dwSize: 表示未对齐前内存的大小 // dwAlignment: 对齐大小值 // 返回值: 返回内存对齐之后的值 DWORD Align(DWORD dwSize, DWORD dwAlignment); // 修改PE文件重定位表信息 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL DoRelocationTable(LPVOID lpBaseAddress); // 填写PE文件导入表信息 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL DoImportTable(LPVOID lpBaseAddress); // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL SetImageBase(LPVOID lpBaseAddress); // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL CallDllMain(LPVOID lpBaseAddress); // 模拟GetProcAddress获取内存DLL的导出函数 // lpBaseAddress: 内存DLL文件加载到进程中的加载基址 // lpszFuncName: 导出函数的名字 // 返回值: 返回导出函数的的地址 LPVOID MmGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName); // 释放从内存加载的DLL到进程内存的空间 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL MmFreeLibrary(LPVOID lpBaseAddress); #endif
MmLoadDll.cpp
点击查看代码
#include "MmLoadDll.h" #include<stdio.h> #include<tchar.h> void ShowError(CHAR* lpszText) { CHAR szErr[MAX_PATH] = { 0 }; sprintf_s(szErr, MAX_PATH,"%s Error!\r\nError Code Is:%d\r\n", lpszText, GetLastError()); MessageBoxA(NULL, szErr, "ERROR", MB_OK | MB_ICONERROR); } // 模拟LoadLibrary加载内存DLL文件到进程中 // lpData: 内存DLL文件数据的基址 // dwSize: 内存DLL文件的内存大小 // 返回值: 内存DLL加载到进程的加载基址 LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize) { LPVOID lpBaseAddress = NULL; // 获取镜像大小 DWORD dwSizeOfImage = GetSizeOfImage(lpData); // 在进程中开辟一个可读、可写、可执行的内存块 lpBaseAddress = VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (NULL == lpBaseAddress) { ShowError((CHAR*)"VirtualAlloc"); return NULL; } RtlZeroMemory(lpBaseAddress, dwSizeOfImage); // 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中 if (FALSE == MmMapFile(lpData, lpBaseAddress)) { ShowError((CHAR*)"MmMapFile"); return NULL; } // 修改PE文件重定位表信息 if(FALSE == DoRelocationTable(lpBaseAddress)) { ShowError((CHAR*)"DoRelocationTable"); return NULL; } // 填写PE文件导入表信息 if (FALSE == DoImportTable(lpBaseAddress)) { ShowError((CHAR*)"DoImportTable"); return NULL; } //修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。 //统一设置成一个属性PAGE_EXECUTE_READWRITE DWORD dwOldProtect = 0; if (FALSE == VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect)) { ShowError((CHAR*)"VirtualProtect"); return NULL; } // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase if (FALSE == SetImageBase(lpBaseAddress)) { ShowError((CHAR*)"SetImageBase"); return NULL; } // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint if (FALSE == CallDllMain(lpBaseAddress)) { ShowError((CHAR*)"CallDllMain"); return NULL; } return lpBaseAddress; } // 根据PE结构,获取PE文件加载到内存后的镜像大小 // lpData: 内存DLL文件数据的基址 // 返回值: 返回PE文件结构中IMAGE_NT_HEADERS.OptionalHeader.SizeOfImage值的大小 DWORD GetSizeOfImage(LPVOID lpData) { DWORD dwSizeOfImage = 0; PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); dwSizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage; return dwSizeOfImage; } // 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中 // lpData: 内存DLL文件数据的基址 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL MmMapFile(LPVOID lpData, LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpData; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); // 获取SizeOfHeaders的值: 所有头+节表头的大小 DWORD dwSizeOfHeaders = pNtHeaders->OptionalHeader.SizeOfHeaders; // 获取节表的数量 WORD wNumberOfSections = pNtHeaders->FileHeader.NumberOfSections; // 获取第一个节表头的地址 PIMAGE_SECTION_HEADER pSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD)pNtHeaders + sizeof(IMAGE_NT_HEADERS)); // 加载 所有头+节表头的大小 RtlCopyMemory(lpBaseAddress, lpData, dwSizeOfHeaders); // 对齐SectionAlignment循环加载节表 WORD i = 0; LPVOID lpSrcMem = NULL; LPVOID lpDestMem = NULL; DWORD dwSizeOfRawData = 0; for (i = 0; i < wNumberOfSections; i++) { if ((0 == pSectionHeader->VirtualAddress) || (0 == pSectionHeader->SizeOfRawData)) { pSectionHeader++; continue; } lpSrcMem = (LPVOID)((DWORD)lpData + pSectionHeader->PointerToRawData); lpDestMem = (LPVOID)((DWORD)lpBaseAddress + pSectionHeader->VirtualAddress); dwSizeOfRawData = pSectionHeader->SizeOfRawData; RtlCopyMemory(lpDestMem, lpSrcMem, dwSizeOfRawData); pSectionHeader++; } return TRUE; } // 对齐SectionAlignment // dwSize: 表示未对齐前内存的大小 // dwAlignment: 对齐大小值 // 返回值: 返回内存对齐之后的值 DWORD Align(DWORD dwSize, DWORD dwAlignment) { DWORD dwRet = 0; DWORD i = 0, j = 0; i = dwSize / dwAlignment; j = dwSize % dwAlignment; if (0 != j) { i++; } dwRet = i * dwAlignment; return dwRet; } // 修改PE文件重定位表信息 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL DoRelocationTable(LPVOID lpBaseAddress) { /* 重定位表的结构: // DWORD sectionAddress, DWORD size (包括本节需要重定位的数据) // 例如 1000节需要修正5个重定位数据的话,重定位表的数据是 // 00 10 00 00 14 00 00 00 xxxx xxxx xxxx xxxx xxxx 0000 // ----------- ----------- ---- // 给出节的偏移 总尺寸=8+6*2 需要修正的地址 用于对齐4字节 // 重定位表是若干个相连,如果address 和 size都是0 表示结束 // 需要修正的地址是12位的,高4位是形态字,intel cpu下是3 */ //假设NewBase是0x600000,而文件中设置的缺省ImageBase是0x400000,则修正偏移量就是0x200000 //注意重定位表的位置可能和硬盘文件中的偏移地址不同,应该使用加载后的地址 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_BASE_RELOCATION pLoc = (PIMAGE_BASE_RELOCATION)((unsigned long)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress); // 判断是否有 重定位表 if ((PVOID)pLoc == (PVOID)pDosHeader) { // 重定位表 为空 return TRUE; } while ((pLoc->VirtualAddress + pLoc->SizeOfBlock) != 0) //开始扫描重定位表 { WORD *pLocData = (WORD *)((PBYTE)pLoc + sizeof(IMAGE_BASE_RELOCATION)); //计算本节需要修正的重定位项(地址)的数目 int nNumberOfReloc = (pLoc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD); for (int i = 0; i < nNumberOfReloc; i++) { // 每个WORD由两部分组成。高4位指出了重定位的类型,WINNT.H中的一系列IMAGE_REL_BASED_xxx定义了重定位类型的取值。 // 低12位是相对于VirtualAddress域的偏移,指出了必须进行重定位的位置。 /* #ifdef _WIN64 if ((DWORD)(pLocData[i] & 0x0000F000) == 0x0000A000) { // 64位dll重定位,IMAGE_REL_BASED_DIR64 // 对于IA-64的可执行文件,重定位似乎总是IMAGE_REL_BASED_DIR64类型的。 ULONGLONG* pAddress = (ULONGLONG *)((PBYTE)pNewBase + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF)); ULONGLONG ullDelta = (ULONGLONG)pNewBase - m_pNTHeader->OptionalHeader.ImageBase; *pAddress += ullDelta; } #endif */ if ((DWORD)(pLocData[i] & 0x0000F000) == 0x00003000) //这是一个需要修正的地址 { // 32位dll重定位,IMAGE_REL_BASED_HIGHLOW // 对于x86的可执行文件,所有的基址重定位都是IMAGE_REL_BASED_HIGHLOW类型的。 DWORD* pAddress = (DWORD *)((PBYTE)pDosHeader + pLoc->VirtualAddress + (pLocData[i] & 0x0FFF)); DWORD dwDelta = (DWORD)pDosHeader - pNtHeaders->OptionalHeader.ImageBase; *pAddress += dwDelta; } } //转移到下一个节进行处理 pLoc = (PIMAGE_BASE_RELOCATION)((PBYTE)pLoc + pLoc->SizeOfBlock); } return TRUE; } // 填写PE文件导入表信息 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL DoImportTable(LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress); // 循环遍历DLL导入表中的DLL及获取导入表中的函数地址 char *lpDllName = NULL; HMODULE hDll = NULL; PIMAGE_THUNK_DATA lpImportNameArray = NULL; PIMAGE_IMPORT_BY_NAME lpImportByName = NULL; PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL; FARPROC lpFuncAddress = NULL; DWORD i = 0; while (TRUE) { if (0 == pImportTable->OriginalFirstThunk) { break; } // 获取导入表中DLL的名称并加载DLL lpDllName = (char *)((DWORD)pDosHeader + pImportTable->Name); hDll = GetModuleHandleA(lpDllName); if (NULL == hDll) { hDll = LoadLibraryA(lpDllName); if (NULL == hDll) { pImportTable++; continue; } } i = 0; // 获取OriginalFirstThunk以及对应的导入函数名称表首地址 lpImportNameArray = (PIMAGE_THUNK_DATA)((DWORD)pDosHeader + pImportTable->OriginalFirstThunk); // 获取FirstThunk以及对应的导入函数地址表首地址 lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((DWORD)pDosHeader + pImportTable->FirstThunk); while (TRUE) { if (0 == lpImportNameArray[i].u1.AddressOfData) { break; } // 获取IMAGE_IMPORT_BY_NAME结构 lpImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)pDosHeader + lpImportNameArray[i].u1.AddressOfData); // 判断导出函数是序号导出还是函数名称导出 if (0x80000000 & lpImportNameArray[i].u1.Ordinal) { // 序号导出 // 当IMAGE_THUNK_DATA值的最高位为1时,表示函数以序号方式输入,这时,低位被看做是一个函数序号 lpFuncAddress = GetProcAddress(hDll, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF)); } else { // 名称导出 lpFuncAddress = GetProcAddress(hDll, (LPCSTR)lpImportByName->Name); } // 注意此处的函数地址表的赋值,要对照PE格式进行装载,不要理解错了!!! lpImportFuncAddrArray[i].u1.Function = (DWORD)lpFuncAddress; i++; } pImportTable++; } return TRUE; } // 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL SetImageBase(LPVOID lpBaseAddress) { PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); pNtHeaders->OptionalHeader.ImageBase = (ULONG32)lpBaseAddress; return TRUE; } // 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL CallDllMain(LPVOID lpBaseAddress) { typedef_DllMain DllMain = NULL; PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); DllMain = (typedef_DllMain)((ULONG32)pDosHeader + pNtHeaders->OptionalHeader.AddressOfEntryPoint); // 调用入口函数,附加进程DLL_PROCESS_ATTACH BOOL bRet = DllMain((HINSTANCE)lpBaseAddress, DLL_PROCESS_ATTACH, NULL); if (FALSE == bRet) { ShowError((CHAR*)"DllMain"); } return bRet; } // 模拟GetProcAddress获取内存DLL的导出函数 // lpBaseAddress: 内存DLL文件加载到进程中的加载基址 // lpszFuncName: 导出函数的名字 // 返回值: 返回导出函数的的地址 LPVOID MmGetProcAddress(LPVOID lpBaseAddress, PCHAR lpszFuncName) { LPVOID lpFunc = NULL; // 获取导出表 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBaseAddress; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((ULONG32)pDosHeader + pDosHeader->e_lfanew); PIMAGE_EXPORT_DIRECTORY pExportTable = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); // 获取导出表的数据 PDWORD lpAddressOfNamesArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfNames); PCHAR lpFuncName = NULL; PWORD lpAddressOfNameOrdinalsArray = (PWORD)((DWORD)pDosHeader + pExportTable->AddressOfNameOrdinals); WORD wHint = 0; PDWORD lpAddressOfFunctionsArray = (PDWORD)((DWORD)pDosHeader + pExportTable->AddressOfFunctions); DWORD dwNumberOfNames = pExportTable->NumberOfNames; DWORD i = 0; // 遍历导出表的导出函数的名称, 并进行匹配 for (i = 0; i < dwNumberOfNames; i++) { lpFuncName = (PCHAR)((DWORD)pDosHeader + lpAddressOfNamesArray[i]); if (0 == lstrcmpi((LPCSTR)lpFuncName, (LPCSTR)lpszFuncName))/*****************这里要注意名称单双字问题**************************/ { // 获取导出函数地址 wHint = lpAddressOfNameOrdinalsArray[i]; lpFunc = (LPVOID)((DWORD)pDosHeader + lpAddressOfFunctionsArray[wHint]); break; } } return lpFunc; } // 释放从内存加载的DLL到进程内存的空间 // lpBaseAddress: 内存DLL数据按SectionAlignment大小对齐映射到进程内存中的内存基址 // 返回值: 成功返回TRUE,否则返回FALSE BOOL MmFreeLibrary(LPVOID lpBaseAddress) { BOOL bRet = FALSE; if (NULL == lpBaseAddress) { return bRet; } bRet = VirtualFree(lpBaseAddress, 0, MEM_RELEASE); lpBaseAddress = NULL; return bRet; }
热更新:
HMR.h
点击查看代码
#pragma once #include"MmLoadDll.h" //热更信息:热更模块路径,版本文件路径 struct HMRInfo { char moudlePath[MAX_PATH]; char versionFile[MAX_PATH]; HMRInfo(){} HMRInfo(char* _modulePath,char* _versionFile) { memcpy(moudlePath,_modulePath, MAX_PATH); memcpy(versionFile, _versionFile, MAX_PATH); } }; void HMR(int initVersion, char* modulePath, char* versionFile); DWORD checkVersion(LPVOID Param); int LoadDll(char* dllPath);
HMR.cpp
点击查看代码
#include"HMR.h" #include<iostream> #include<Windows.h> using namespace std; LPVOID g_lpBaseAddress = NULL; //动态加载的模块及地址 int g_curVersion = 1; HMRInfo hmrInfo; //动态加载Dll int LoadDll(char* dllPath) { HANDLE hFile = CreateFileA(dllPath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_ARCHIVE, NULL); if (INVALID_HANDLE_VALUE == hFile) { ShowError((CHAR*)"CreateFile"); return 1; } DWORD dwFileSize = GetFileSize(hFile, NULL); // 申请动态内存并读取DLL到内存中 BYTE* lpData = new BYTE[dwFileSize]; if (NULL == lpData) { ShowError((CHAR*)"new"); return 2; } DWORD dwRet = 0; ReadFile(hFile, lpData, dwFileSize, &dwRet, NULL); // 将内存DLL加载到程序中 g_lpBaseAddress = MmLoadLibrary(lpData, dwFileSize); if (NULL == g_lpBaseAddress) { ShowError((CHAR*)"MmLoadLibrary"); return 3; } CloseHandle(hFile); delete[] lpData; } //版本检测线程回调函数 DWORD checkVersionProc(LPVOID Param) { while (1) { //获取dll版本 FILE* fp = NULL; fopen_s(&fp,hmrInfo.versionFile, "r"); char tips[20]; int version; fscanf(fp, "%s %d", tips, &version); if (version > g_curVersion)//如果版本有更新 { printf("find new version. version code:%d\n", version); g_curVersion = version; /*卸载原有dll*/ if (g_lpBaseAddress) { FARPROC UnLoadDLL = (FARPROC)MmGetProcAddress(g_lpBaseAddress, (PCHAR)"UnloadDLL"); UnLoadDLL(); //释放dll中所占用的资源 MmFreeLibrary(g_lpBaseAddress); //释放dll在进程空间中使用的内存 g_lpBaseAddress = NULL; } /*加载新版本的dll*/ LoadDll(hmrInfo.moudlePath); } else { printf("%s %d\n", tips, version); } fclose(fp); Sleep(10000); } } //热更新接口 void HMR(int initVersion, char* modulePath, char* versionFile) { g_curVersion = initVersion; strcpy(hmrInfo.moudlePath, modulePath); strcpy(hmrInfo.versionFile, versionFile); LoadDll(modulePath); HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)checkVersionProc, &hmrInfo, 0, NULL); Sleep(1000);//确保线程被完整创建并运行 }
注意事项:
1.动态加载的dll要提供一个导出函数UnloadDLL,用来处理dll内部资源的回收,以及线程的关闭,其最后要Sleep一定时间,确保资源完全回收以及线程完全关闭
2.版本文件中的格式为: version: 版本号
冒号和版本号之间存在一个空格,版本号为整数(可根据自己需求修改代码及版本文件格式)
分类:
C/C++开发
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构