视频参考:https://www.bilibili.com/video/BV1mbUBY7E24
关于游戏中文件输入输出(IO)操作的讨论。主要分为两类:
-
只读资产的加载
- 这部分主要涉及游戏中用于展示和运行的只读资源,例如音乐、音效、美术资源(如 3D 模型和纹理)等。
- 这些文件从磁盘加载到内存中供游戏使用,但不会被修改。
- 在现代游戏中,这些数据可能非常庞大(数千兆字节),通常通过后台流式加载的方式避免加载屏幕过长,从而提升用户体验。
-
游戏状态的保存和加载
- 这部分与游戏配置和进度相关,例如窗口模式设置、声音音量设置、解锁状态、存档文件等。
- 这些文件既需要写入磁盘,也需要在后续运行时从磁盘中读取。
- 通常这些数据量相对较小,因此可以通过简单的平面调用加载,不需要复杂的流式处理。
文件的读写过程因数据类型和用途的不同有不同的处理方式:
- 只读资源需要注重性能优化(如流式加载),以避免影响游戏运行。
- 状态数据则关注正确性和持久性,确保配置和进度能在多次运行中保持一致。
在过去的开发中,文件操作通常是通过以下步骤完成的:
打开文件:
使用文件名调用 openFile 函数,获得文件句柄(file handle)。
读取文件内容:
提供一个缓冲区(如 128 字节),通过 read 函数从文件中读取指定字节的数据。
根据返回值判断读取是否成功,如果失败,需要处理错误。
关闭文件:
在操作完成后,清理文件句柄。
// 定义文件名为 "test.bmp",指向字符串的指针
char *Filename = "test.bmp";
// 打开文件并获取文件句柄
file_handle *File = OpenFile(Filename);
// 定义一个大小为 128 字节的缓冲区
uint8 Buffer[128];
// 尝试从文件中读取缓冲区大小的数据
if(Read(File, sizeof(Buffer), Buffer)) {
// 如果读取成功,执行相应操作
} else {
// 如果读取失败,执行失败处理逻辑
}
// 关闭文件以释放资源
closeFile(File);
文件 I/O 操作的策略,特别是针对流式文件处理(streaming-based file I/O)是否适合某些目的进行了详细分析。以下是内容的要点总结和理解:
GetFileSize
是 Windows API 中用于获取文件大小的函数。
功能
返回指定文件的大小(以字节为单位)。
参数说明
-
hFile
- 输入参数,文件的句柄(
HANDLE
类型)。 - 句柄必须是由支持文件读取的函数(如
CreateFile
)返回的,且文件不能是管道。
- 输入参数,文件的句柄(
-
lpFileSizeHigh
- 可选参数,指向一个
DWORD
类型的变量,用于存储文件大小的高 32 位(适用于大于 4GB 的文件)。 - 如果为
NULL
,则忽略高 32 位。
- 可选参数,指向一个
返回值
-
成功
- 返回文件大小的低 32 位。
- 如果
lpFileSizeHigh
非空,则其指向的值存储文件大小的高 32 位。
-
失败
- 返回
INVALID_FILE_SIZE
(0xFFFFFFFF)。 - 此时需要调用
GetLastError()
检查是否确实发生错误(如文件大小正好等于INVALID_FILE_SIZE
,不会返回错误)。
- 返回
注意事项
- 对于超过 4GB 的文件,需要结合
lpFileSizeHigh
计算完整文件大小:uint64_t fullFileSize = ((uint64_t)lpFileSizeHigh << 32) | fileSizeLow;
- 如果文件句柄不可读,函数会失败。
示例代码
#include <windows.h>
#include <stdio.h>
int main() {
// 打开文件
HANDLE hFile = CreateFileA(
"example.txt", // 文件路径
GENERIC_READ, // 读取权限
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有文件
FILE_ATTRIBUTE_NORMAL, // 普通文件属性
NULL // 无模板文件
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open file. Error: %lu\n", GetLastError());
return 1;
}
// 获取文件大小
DWORD fileSizeLow;
DWORD fileSizeHigh;
fileSizeLow = GetFileSize(hFile, &fileSizeHigh);
if (fileSizeLow == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) {
printf("Failed to get file size. Error: %lu\n", GetLastError());
CloseHandle(hFile);
return 1;
}
// 计算完整文件大小
uint64_t fullFileSize = ((uint64_t)fileSizeHigh << 32) | fileSizeLow;
printf("File size: %llu bytes\n", fullFileSize);
// 关闭句柄
CloseHandle(hFile);
return 0;
}
CloseHandle
是 Windows API 中用于关闭对象句柄的函数。
功能
关闭一个打开的句柄,释放与之关联的系统资源。
参数说明
hObject
- 输入参数,要关闭的句柄(
HANDLE
类型)。 - 句柄可以是文件、线程、进程、同步对象(如互斥量、信号量)等。
- 输入参数,要关闭的句柄(
返回值
-
成功
返回TRUE
。 -
失败
返回FALSE
。可以调用GetLastError()
获取错误码以确定失败原因(例如句柄无效)。
注意事项
-
句柄无效时调用
如果传入的句柄已经被关闭或未初始化,会导致函数失败。 -
重复调用
不应多次关闭同一个句柄,否则可能导致程序异常。 -
文件句柄
文件操作完成后,必须调用CloseHandle
关闭文件句柄以避免资源泄漏。 -
句柄类型
确保关闭的是正确类型的句柄,错误的操作可能会影响其他系统资源。
示例代码
#include <windows.h>
#include <stdio.h>
int main() {
// 打开文件
HANDLE hFile = CreateFileA(
"example.txt", // 文件路径
GENERIC_READ, // 读取权限
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有文件
FILE_ATTRIBUTE_NORMAL, // 普通文件属性
NULL // 无模板文件
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open file. Error: %lu\n", GetLastError());
return 1;
}
printf("File opened successfully.\n");
// 关闭文件句柄
if (CloseHandle(hFile)) {
printf("Handle closed successfully.\n");
} else {
printf("Failed to close handle. Error: %lu\n", GetLastError());
}
return 0;
}
用途
- 关闭文件、进程、线程、同步对象等句柄。
- 释放资源,防止资源泄漏或句柄耗尽。
- 适用于清理已完成的操作,保持程序高效运行。
常见场景
- 文件操作结束后调用。
- 线程或进程执行完毕后释放句柄。
- 锁或事件对象不再使用时销毁句柄。
用途
- 获取文件大小用于内存分配或文件操作。
- 确保文件未超出特定大小限制。
- 用于处理大文件时计算高低 32 位文件大小。
核心内容:
-
流式文件 I/O 不适用当前目标:
- 对于作者的特定目标,流式文件 I/O 并不合适,因为他们的需求是针对文件块的明确读取,而不是从一个大型流中逐块拉取数据。
- 他们需要读取的文件数据通常是固定大小的、可以预知的,不需要使用流的逐步处理。
-
当前需求是高效读取:
- 他们的重点是加载完整的文件或资产(例如加载一个完整的位图文件)。
- 读取操作是“全或无”,即一次性加载整个数据块,而非逐步处理。
-
未来可能的优化:
- 在目前开发阶段,他们选择简单直接的 I/O 操作策略,而不会为复杂流式操作进行过多优化。
- 将来,当需要加载一个打包的资源文件时(例如包含多个资产的文件),可以使用类似流式读取的系统,但需要支持多线程。
-
日志文件写入的例外:
- 唯一可能需要流式处理的场景是调试日志的写入,但目前并没有计划实现这一功能。
对比分析:
-
流式文件 I/O 的优势:
- 适用于处理超大文件或实时数据流的场景,比如从网络中逐步拉取视频数据。
- 资源占用较少(按需读取),避免一次性加载整个文件而导致内存压力。
-
当前策略的选择:
- 出于效率和简单性,作者选择直接一次性读取所需的文件块,而不是处理流式数据。这种方法适合加载固定大小、结构明确的文件内容。
-
未来策略的演变:
- 随着需求升级,例如加载复杂的打包资源,可能需要采用更高级的策略,比如使用多线程支持的分块流式读取。
关键总结:
- 当前任务:简单高效地加载文件,无需使用流式文件处理。
- 未来可能:在需要复杂的打包文件读取时,会考虑实现多线程和流式文件处理。
- 优化方向:现阶段优先选择易于实现的直接文件读取,而不是为复杂功能增加开发成本。
CreateFileA
是 Windows API 中用于创建、打开文件、设备、管道或通信资源的函数。其作用和用途如下:
功能
- 打开一个现有文件以进行读取、写入或两者操作。
- 创建一个新文件。
- 打开设备(如磁盘驱动器、控制台等)。
- 创建/打开管道或通信端口。
参数说明
-
lpFileName
指向一个以空字符结尾的字符串,表示文件或设备的路径名。 -
dwDesiredAccess
指定所需的访问模式(如读取、写入或两者)。常见值:GENERIC_READ
: 读取访问权限GENERIC_WRITE
: 写入访问权限
-
dwShareMode
指定文件或设备共享模式。常见值:FILE_SHARE_READ
: 允许其他进程读取FILE_SHARE_WRITE
: 允许其他进程写入0
: 不共享
-
lpSecurityAttributes
可选参数,指定安全属性。如果为NULL
,使用默认设置。 -
dwCreationDisposition
指定文件的行为,如是否创建新文件或覆盖现有文件。常见值:CREATE_NEW
: 如果文件不存在,则创建;存在时失败CREATE_ALWAYS
: 始终创建新文件(覆盖现有文件)OPEN_EXISTING
: 打开现有文件,文件不存在时失败OPEN_ALWAYS
: 打开文件,文件不存在时创建TRUNCATE_EXISTING
: 打开文件并清空其内容
-
dwFlagsAndAttributes
指定文件或设备的标志和属性(如文件是否为隐藏或系统文件)。 -
hTemplateFile
可选参数,仅用于创建文件时,指定模板文件句柄。
返回值
- 成功:返回文件或设备的句柄(
HANDLE
类型)。 - 失败:返回
INVALID_HANDLE_VALUE
(可通过GetLastError()
获取具体错误码)。
常见用途
- 打开文件进行读取或写入。
- 创建新的日志文件。
- 打开串口通信设备(如
COM1
)。 - 操作管道或共享资源。
示例
HANDLE fileHandle = CreateFileA(
"example.txt", // 文件路径
GENERIC_READ | GENERIC_WRITE, // 读取和写入权限
0, // 不共享
NULL, // 默认安全属性
CREATE_ALWAYS, // 始终创建新文件
FILE_ATTRIBUTE_NORMAL, // 普通文件属性
NULL // 不使用模板文件
);
if (fileHandle == INVALID_HANDLE_VALUE) {
printf("Failed to create or open file. Error: %lu\n", GetLastError());
} else {
printf("File created/opened successfully.\n");
CloseHandle(fileHandle); // 关闭文件句柄
}
ReadFile
是 Windows API 中用于从文件或 I/O 设备读取数据的函数。
功能
从文件或输入/输出设备(如文件、管道、串口等)中读取指定数量的字节数据到缓冲区。
参数说明
-
hFile
- 输入参数,文件或设备的句柄(
HANDLE
类型)。 - 该句柄必须是由支持读取的函数(如
CreateFile
)打开的,并具有读取权限。
- 输入参数,文件或设备的句柄(
-
lpBuffer
- 输出参数,指向一个缓冲区,用于存储读取到的数据。
- 如果此参数为
NULL
,表示无效的缓冲区,函数将失败。
-
nNumberOfBytesToRead
- 输入参数,要读取的字节数。
- 指定读取操作期望完成的最大字节数。
-
lpNumberOfBytesRead
- 输出参数,指向一个变量,用于接收实际读取的字节数。
- 如果设置为
NULL
,则调用方必须使用重叠(OVERLAPPED
)结构处理字节计数。
-
lpOverlapped
- 输入/输出参数,指向一个
OVERLAPPED
结构,用于异步操作。 - 如果未使用异步操作,此参数必须为
NULL
。
- 输入/输出参数,指向一个
返回值
-
成功
- 返回
TRUE
。 *lpNumberOfBytesRead
包含实际读取的字节数。
- 返回
-
失败
- 返回
FALSE
。可以通过调用GetLastError()
获取具体错误码。 - 如果读取操作被挂起(如使用异步操作),则错误码可能为
ERROR_IO_PENDING
。
- 返回
注意事项
-
同步与异步操作
- 如果句柄未设置为异步模式(如未指定
FILE_FLAG_OVERLAPPED
),操作将以同步方式执行,函数在读取完成前不会返回。 - 如果句柄为异步模式,则必须提供
lpOverlapped
,以便跟踪操作状态。
- 如果句柄未设置为异步模式(如未指定
-
管道或设备的特殊性
- 对于管道或设备,
ReadFile
的行为可能会根据数据可用性发生变化(如阻塞或非阻塞模式)。
- 对于管道或设备,
-
缓冲区大小
- 确保
lpBuffer
有足够的大小存储nNumberOfBytesToRead
字节数据。
- 确保
示例代码
同步读取文件
#include <windows.h>
#include <stdio.h>
int main() {
// 打开文件
HANDLE hFile = CreateFileA(
"example.txt", // 文件路径
GENERIC_READ, // 读取权限
0, // 不共享
NULL, // 默认安全属性
OPEN_EXISTING, // 打开现有文件
FILE_ATTRIBUTE_NORMAL, // 普通文件属性
NULL // 无模板文件
);
if (hFile == INVALID_HANDLE_VALUE) {
printf("Failed to open file. Error: %lu\n", GetLastError());
return 1;
}
// 读取文件内容
char buffer[128] = {0}; // 缓冲区
DWORD bytesRead = 0; // 实际读取字节数
if (ReadFile(hFile, buffer, sizeof(buffer) - 1, &bytesRead, NULL)) {
printf("Read %lu bytes: %s\n", bytesRead, buffer);
} else {
printf("Failed to read file. Error: %lu\n", GetLastError());
}
// 关闭文件句柄
CloseHandle(hFile);
return 0;
}
用途
- 从文件中读取数据用于处理或存储。
- 从管道或串口读取输入数据。
- 结合异步 I/O 提高性能,处理多任务数据读取。
常见场景
- 文件数据解析。
- 读取串口通信内容。
- 处理日志或流数据。
VirtualAlloc
是 Windows API 中用于分配虚拟内存的函数。
功能
分配、保留或提交一个虚拟内存区域,并可设置该内存区域的访问权限。
参数说明
-
lpAddress
- 输入参数,指定内存区域的首地址(可选)。
- 如果为
NULL
,系统将自动选择合适的地址。 - 如果非
NULL
,则表示请求分配特定的地址,但需要符合内存对齐要求。
-
dwSize
- 输入参数,要分配的内存大小(以字节为单位)。
- 必须为系统页面大小(通常为 4KB)的倍数。
-
flAllocationType
- 输入参数,指定内存分配的类型。常见值:
MEM_COMMIT
: 提交内存,分配实际的物理内存或交换文件空间。MEM_RESERVE
: 保留内存地址空间,但不分配物理内存。MEM_RESET
: 将指定内存标记为已重置,但保留其保留状态。MEM_RESET_UNDO
: 撤销MEM_RESET
操作。
- 输入参数,指定内存分配的类型。常见值:
-
flProtect
- 输入参数,指定内存区域的访问保护类型。常见值:
PAGE_READONLY
: 只读访问权限。PAGE_READWRITE
: 可读可写访问权限。PAGE_EXECUTE
: 可执行但不可读写权限。PAGE_EXECUTE_READWRITE
: 可执行、可读、可写权限。
- 输入参数,指定内存区域的访问保护类型。常见值:
返回值
-
成功
返回分配的内存区域的起始地址(LPVOID
类型)。 -
失败
返回NULL
。可以调用GetLastError()
获取具体错误码。
注意事项
-
内存释放
- 使用
VirtualFree
释放通过VirtualAlloc
分配的内存,避免内存泄漏。
- 使用
-
内存类型
- 如果同时指定
MEM_COMMIT | MEM_RESERVE
,表示既保留地址空间又提交内存。
- 如果同时指定
-
页面大小对齐
- 分配的内存大小和地址必须符合页面大小对齐要求(通常为 4KB)。
-
分配失败
- 可能由内存不足、无效参数或地址冲突导致分配失败。
示例代码
分配和释放内存
#include <windows.h>
#include <stdio.h>
int main() {
// 分配 16KB 内存
SIZE_T size = 16 * 1024; // 16KB
LPVOID lpMemory = VirtualAlloc(
NULL, // 系统选择地址
size, // 分配大小
MEM_COMMIT | MEM_RESERVE, // 提交和保留内存
PAGE_READWRITE // 可读可写权限
);
if (lpMemory == NULL) {
printf("Memory allocation failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Memory allocated at address: %p\n", lpMemory);
// 使用分配的内存
char *data = (char *)lpMemory;
for (int i = 0; i < size; i++) {
data[i] = 'A';
}
printf("Memory written successfully.\n");
// 释放内存
if (!VirtualFree(lpMemory, 0, MEM_RELEASE)) {
printf("Memory release failed. Error: %lu\n", GetLastError());
return 1;
}
printf("Memory released successfully.\n");
return 0;
}
用途
- 创建大内存块以供动态使用。
- 管理内存保护以实现安全或性能优化(如只读、只执行内存)。
- 实现内存映射文件、动态加载器或自定义内存分配器。
常见场景
- 高性能计算时分配大内存块。
- 动态管理虚拟内存。
- 实现内存保护和访问权限控制。