公司的游戏会将一些资源文件压缩后放在服务器,客户端在需要的时候拉取,然后解压使用。用的是 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;
通过file_names
、file_sizes
和 file_data_start_addrs
就可以去取各个文件的内容了:
bool GetFileContents(const char* file_name, char* contents)
{
bool flag = false;
for (int i = 0; i < file_names.size(); i++) {
std::string name_(file_name);
if (file_names[i].compare(name_) == 0) {
int file_size = file_sizes[i];
flag = true;
fseek(file, file_data_start_addrs[i], SEEK_SET);
fread(contents, file_size, 1, file);
fseek(file, 0, SEEK_SET);
break;
}
}
return flag;
}
更完善的实现可以参考 GNU Linux Tar 包的源码:https://www.gnu.org/software/tar/
文档信息
- 本文作者:Ning Zhang
- 本文链接:https://sunsetroads.github.io/2020/06/12/lz4/
- 版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)