游戏会将一些资源文件压缩后放在服务器,客户端在需要的时候拉取,然后解压使用。用的是 C# 的 lz4 进行的压缩和解压缩,导致申请的内存没办法及时释放(mono 虚拟机申请的内存是不会归还给系统的),所以如果手机上如果经历了边玩边下载的话,峰值内存会有大概100M+的上浮,这对于一些内存比较受限的机器来说是不友好的。
问题说明
- C# mono 虚拟机申请的内存是不会归还给系统的。
- 原生语言(C/C++)可以自由控制内存的释放。
- 如果想内存峰值降低,则需要将原来由 C# 开发的 lz4 模块,改用 C/C++ 实现,然后由 C# 调用,从而达到降低内存峰值的目的。
实现方案
Github 上有 C 语言实现的 lz4 可以直接用:https://github.com/lz4/lz4。参考根目录下 examples 里的例子即可实现对一个文件进行 lz4 压缩和解压缩,里面的文档和例子都很清楚,这里就不做赘述了。
Github 上这个 lz4 的包只能针对单个二进制文件进行压缩,无法处理文件夹。如果需要对文件夹进行压缩时,就需要先将文件夹合并成一个二进制文件,解压后再分割还原成文件夹。在 Linux 中将文件夹合并成单个文件通常使用 tar 命令,在 iOS 平台尝试了下,是无法调用 Shell 命令的,所以需要用代码实现一下 tar 文件解析。
整个过程简述如下:
- 在 Mac/Linux 上用 tar 命令打包一个文件夹为一个文件。
- 用 lz4 库对第一步打包后的文件压缩,然后放到服务器。
- 客户端拉取到压缩的文件后,调用 lz4 库的 Api 解压缩。
- 调用自己实现的 tar 解析包,将解压缩后的单个文件分割还原成文件夹。
第四步中需要自己实现一个 tar 的解析包,GNU Linux tar 包的源码有近 30w 行的代码,我们的业务场景并不需要如此完善的实现,只要能正常分割还原文件就可以了,下面看下如何实现一个具备基础功能的 tar 解析包。
Tar 格式
tar 是 Unix 和类 Unix 系统上的归档打包工具,可以将多个文件合并为一个文件,打包后的文件名亦为 “tar”。目前,tar 文件格式已经成为 POSIX 标准,最初是 POSIX.1-1988,目前是 POSIX.1-2001。本程序最初的设计目的是将文件备份到磁带上(tape archive),因而得名 tar。
tar 会为每个文件生成一个 512 字节的 header,记录它的名称、权限、大小等信息,然后将文件内容按 512 个字节分割成多个块,按顺序放在 header 后面。最后将所有的这些块写进一个二进制文件中。
C++ 实现解析
根据 tar 标准 定义这个 tar header 的结构体:
typedef struct tar_header
{ /* byte offset */
char name[100]; /* 0 */
char mode[8]; /* 100 */
char uid[8]; /* 108 */
char gid[8]; /* 116 */
char size[12]; /* 124 */
char mtime[12]; /* 136 */
char chksum[8]; /* 148 */
char typeflag; /* 156 */
char linkname[100]; /* 157 */
char magic[6]; /* 257 */
char version[2]; /* 263 */
char uname[32]; /* 265 */
char gname[32]; /* 297 */
char devmajor[8]; /* 329 */
char devminor[8]; /* 337 */
char prefix[155]; /* 345 */
/* 500 */
} tar_header;
再定义下文件的类型:
/* Values used in typeflag field. */
#define REGTYPE '0' /* regular file */
#define AREGTYPE '\0' /* regular file */
#define LNKTYPE '1' /* link */
#define SYMTYPE '2' /* reserved */
#define CHRTYPE '3' /* character special */
#define BLKTYPE '4' /* block special */
#define DIRTYPE '5' /* directory */
#define FIFOTYPE '6' /* FIFO special */
#define CONTTYPE '7' /* reserved */
获取各个文件的位置、名称和大小:
std::vector<std::string> file_names;
std::vector<size_t> file_sizes;
std::vector<size_t> file_data_start_addrs;
const int block_size{ 512 };
file = fopen("tar_name", "rb");
if (!file) return false;
unsigned char buf[block_size];
tar_header* header = (tar_header*)buf;
memset(buf, 0, block_size);
size_t pos{ 0 };
while (1) {
size_t read_size = fread(buf, block_size, 1, file);
if (read_size != 1) break;
pos += block_size;
size_t file_size{0};
sscanf(header->size, "%lo", &file_size);
size_t file_block_count = (file_size + block_size - 1) / block_size;
switch (header->typeflag) {
case '0':
case '\0':
// normal file
file_sizes.push_back(file_size);
file_names.push_back(std::string(header->name));
file_data_start_addrs.push_back(pos);
break;
case '1':
// hard link
break;
case '2':
// symbolic link
break;
case '3':
// device file/special file
break;
case '4':
// block device
break;
case '5':
// directory
break;
case '6':
// named pipe
break;
default:
break;
}
pos += file_block_count * block_size;
fseek(file, pos, SEEK_SET);
}
fseek(file, 0, SEEK_SET);
return true;
更完善的实现可以参考 GNU Linux Tar 包的源码:https://www.gnu.org/software/tar/
/ C/C++中文件操作总结
/
使用 C/C++ 来做一些压缩、加密等操作,其中经常要用到一些文件操作的函数
打开文件
fopen 是 C 标准库用来操作文件的函数,拥有良好的移植性,而 open 是 UNIX 的系统调用,移植性有限,我基本都使用 fopen 函数:
FILE *fopen(char *filename, char *mode);
在这里,filename 是字符串,用来命名文件,访问模式 mode 的值可以是下列值中的一个:
模式 | 描述 |
r | 打开一个已有的文本文件,允许读取文件。 |
w | 打开一个文本文件,允许写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会从文件的开头写入内容。如果文件存在,则会被截断为零长度,重新写入。 |
a | 打开一个文本文件,以追加模式写入文件。如果文件不存在,则会创建一个新文件。在这里,您的程序会在已有的文件内容中追加内容。 |
r+ | 打开一个文本文件,允许读写文件。 |
w+ | 打开一个文本文件,允许读写文件。如果文件已存在,则文件会被截断为零长度,如果文件不存在,则会创建一个新文件。 |
a+ | 打开一个文本文件,允许读写文件。如果文件不存在,则会创建一个新文件。读取会从文件的开头开始,写入则只能是追加模式。 |
如果处理的是二进制文件,则需使用下面的访问模式来取代上面的访问模式:
"rb", "wb", "ab", "rb+", "r+b", "wb+", "w+b", "ab+", "a+b"
返回值FILE *
是指向这个文件的指针,后续用它来做文件读写等操作。
读写文件
下面两个函数用于二进制文件输入和输出:
size_t fread(void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
size_t fwrite(const void *ptr, size_t size_of_elements,
size_t number_of_elements, FILE *a_file);
需要注意的是返回值是成功读取的对象个数,即number_of_elements
,而不是读取内容的size
。若出现错误或到达文件末尾,则可能小于number_of_elements
。
关闭文件
文件操作完成后,请使用fclose()
函数关闭文件。函数的原型如下:
int fclose(FILE *fp);
如果成功关闭文件,fclose()
函数返回零,如果关闭文件时发生错误,函数返回 -1。
文件权限
文件权限标志可以使用加权数字表示,这组数字被称为 umask 变量,它的类型是 mode_t,是一个无符号八进制数。umask 变量的定义方法如下表所示。umask 变量由 3 位数字组成,数字的每一位代表一类权限。用户所获得的权限是加权数值的总和。例如 764 表示所有者拥有读、写和执行权限,群组拥有读和写权限,其他用户拥有读权限。
加权数值 | 第1位 | 第2位 | 第3位 |
4 | 所有者拥有读权限 | 群组拥有读权限 | 其他用户拥有读权限 |
2 | 所有者拥有写权限 | 群组拥有写权限 | 其他用户拥有写权限 |
1 | 所有者拥有执行权限 | 群组拥有执行权限 | 其他用户拥有执行权限 |
文件类型
Linux 中一切皆文件,有以下 7 种文件类型。
普通文件类型:Linux 中最多的一种文件类型, 包括 纯文本文件(ASCII)、二进制文件 (binary)、数据格式的文件 (data) 和各种压缩文件。使用ls -l
命令查看时第一个属性为 [-]。
目录文件:就是目录, 能用 cd 命令进入的。第一个属性为 [d],例如 [drwxrwxrwx]。
块设备文件: 就是存储数据以供系统存取的接口设备,简单而言就是硬盘。例如一号硬盘的代码是 /dev/hda1 等文件。第一个属性为 [b]。
字符设备文件:即串行端口的接口设备,例如键盘、鼠标等等。第一个属性为 [c]。
套接字文件:这类文件通常用在网络数据连接。可以启动一个程序来监听客户端的要求,客户端就可以通过套接字来进行数据通信。第一个属性为 [s],最常在 /var/run 目录中看到这种文件类型。
管道文件:FIFO也是一种特殊的文件类型,它主要的目的是,解决多个程序同时存取一个文件所造成的错误。FIFO 是 first-in-first-out (先进先出) 的缩写。第一个属性为 [p]。
链接文件:类似 Windows 下面的快捷方式。第一个属性为 [l],例如 [lrwxrwxrwx]。
文件信息
access()
函数用于检查文件是否存在和访问权限,原型如下:
// mode 可选值
// R_OK 测试读许可权
// W_OK 测试写许可权
// X_OK 测试执行许可权
// F_OK 测试文件是否存在
int access(const char * pathname, int mode)
成功执行时,返回0,失败返回-1。
stat
函数用于获取文件的详细信息,保存在标准库的struct stat
中,原型如下:
int stat(const char * pathname, stat *buf);
struct stat
定义如下:
struct stat {
dev_t st_dev; //文件的设备编号
ino_t st_ino; //节点
mode_t st_mode; //文件的类型和存取的权限
nlink_t st_nlink; //连到该文件的硬连接数目,刚建立的文件值为1
uid_t st_uid; //用户ID
gid_t st_gid; //组ID
dev_t st_rdev; //(设备类型)若此文件为设备文件,则为其设备编号
off_t st_size; //文件字节数(文件大小)
unsigned long st_blksize; //块大小(文件系统的I/O 缓冲区大小)
unsigned long st_blocks; //块数
time_t st_atime; //最后一次访问时间
time_t st_mtime; //最后一次修改时间
time_t st_ctime; //最后一次改变时间(指属性)
};
便利函数
获取文件内容和大小
static FILE *OpenFileWithPath(const char *path)
{
const char *fileMode = "rb";
return fopen (path, fileMode);
}
static char *ReadStringFromFile(const char *pathName, int *size)
{
FILE *file = OpenFileWithPath (pathName);
if (file == NULL)
{
return 0;
}
fseek (file, 0, SEEK_END);
int length = ftell(file);
fseek (file, 0, SEEK_SET);
if (length < 0)
{
fclose (file);
return 0;
}
*size = length;
char *outData = malloc (length);
int readLength = fread (outData, 1, length, file); fclose(file);
if (readLength != length)
{
free (outData);
return 0;
}
return outData;
}
从绝对路径中提取文件名
char* FileName(char *absolutePath)
{
if (absolutePath[0] != '/') {
return 0;
}
char *s = strrchr(absolutePath, '/');
size_t size = strlen(s);
char *des = malloc(size);
memcpy(des, s + 1, size);
return des;
}
获取绝对路径的文件所在的目录
char* DirName(char *absolutePath)
{
if (absolutePath[0] != '/') {
return 0;
}
const size_t len = strlen(absolutePath);
char *path = malloc(len + 1);
strncpy(path, absolutePath, len);
if (path[len - 1] == '/') {
path[len - 1] = 0;
}
for(char * p = path + strlen(path) - 1; *p; p--){
if (*p == '/'){
*p = '\0';
break;
}
}
return path;
}
递归创建一个目录
#define DEFAULT_DIR_MODE S_IRWXU | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH // 0755
int RecursiveMkdir(const char * dir, const unsigned int mode){
const size_t len = strlen(dir);
if (!len){
return 0;
}
char * path = calloc(len + 1, sizeof(char));
strncpy(path, dir, len);
if (path[len - 1] == '/'){
path[len - 1] = 0;
}
for(char * p = path + 1; *p; p++){
if (*p == '/'){
*p = '\0';
if (access(path, 0) < 0) {
mkdir(path, mode?mode:DEFAULT_DIR_MODE);
}
*p = '/';
}
}
free(path);
return 0;
}
/