2、资源压缩入门:资源压缩的定义、常见压缩算法简介(Zlib、LZ4、Snappy)

好,咱们正式开始聊压缩。

你可能觉得压缩不就是把文件变小吗?没错,但放在双缓冲刷新这个场景里,压缩的意义远不止省硬盘空间。它直接影响数据从磁盘搬到内存、再从内存搬到显存的速度。说白了,压缩是在用 CPU 时间换 I/O 带宽。这个 trade-off 值不值,取决于你的瓶颈在哪。

2.1 资源压缩到底在干什么?

定义其实很简单:资源压缩是通过编码技术,用更少的比特数表示原始数据的过程。压缩后的数据不能直接使用,必须先解压还原。

我习惯把压缩分成两类:

  • 无损压缩:解压后和原始数据一模一样。游戏资源、配置文件、代码必须用无损。
  • 有损压缩:丢掉一些人眼/人耳不敏感的信息,换来更高压缩比。图片、音频、视频常用。

咱们这门课主要关注无损压缩。为什么?因为双缓冲刷新涉及的是纹理、网格、着色器这些二进制资源,丢一个字节都可能崩掉。

核心观点:压缩比 ≠ 加载速度。高压缩比可能解压慢,低压缩比可能 I/O 快。你要找的是那个平衡点。

2.2 三个主流算法:Zlib、LZ4、Snappy

市面上压缩算法很多,但做实时资源加载,你绕不开这三个。我一个个说。

2.2.1 Zlib(Deflate)

Zlib 是 Deflate 算法的封装库。Deflate 是 LZ77 + Huffman 编码的组合拳。压缩率很高,但解压速度一般。

我在项目中遇到过一个问题:用 Zlib 压缩的关卡包,加载时 CPU 飙到 80%,帧率直接掉到个位数。后来发现是解压太慢,把渲染线程给拖垮了。

指标 Zlib(默认级别)
压缩比 高(2:1 ~ 5:1)
压缩速度 慢(约 50 MB/s)
解压速度 中等(约 200 MB/s)
内存占用 较高(需要 256KB 窗口)
适用场景 安装包、存档、不频繁加载的资源

注意:Zlib 的压缩级别(1-9)对压缩比影响大,但对解压速度影响很小。我建议你固定用级别 6,别在加载路径上调级别,收益不大。

2.2.2 LZ4

LZ4 是 LZ77 的变种,设计目标就是极致的解压速度。它放弃了高压缩比,换来了接近内存拷贝的解压性能。

你想想看,LZ4 的解压速度能到 4 GB/s 以上。这意味着什么?意味着解压不再是瓶颈,瓶颈回到了磁盘 I/O 上。

指标 LZ4(默认)
压缩比 低(1.5:1 ~ 2.5:1)
压缩速度 快(约 500 MB/s)
解压速度 极快(4 GB/s+)
内存占用 低(64KB 窗口)
适用场景 实时加载、纹理流送、双缓冲预解压

我的习惯:在双缓冲的预加载线程里,我优先用 LZ4。解压快,不阻塞主线程。压缩比低一点没关系,反正磁盘带宽现在也够用。

2.2.3 Snappy

Snappy 是 Google 开发的,目标跟 LZ4 类似——快。但它更注重压缩速度,而不是压缩比。

Snappy 的压缩和解压速度都很均衡,没有明显的短板。不过压缩比比 LZ4 还略低一点。

指标 Snappy(默认)
压缩比 低(1.3:1 ~ 2.0:1)
压缩速度 极快(600 MB/s+)
解压速度 快(3 GB/s+)
内存占用 低(32KB 窗口)
适用场景 日志、网络传输、对压缩比不敏感的场景

2.3 算法对比:一张图看懂

下面这张 SVG 图把三个算法的核心差异画出来了。你可以看到它们在压缩比、压缩速度、解压速度三个维度上的相对位置。

常见压缩算法对比(相对位置) 压缩比 → 速度 → Zlib 高压缩比 · 慢 LZ4 低压缩比 · 极快 Snappy 更低压缩比 · 更快 选型建议 • 实时加载 → LZ4 • 安装包 → Zlib • 日志/网络 → Snappy

2.4 怎么选?我的经验

没有银弹。选哪个算法取决于你的具体场景。我列几个常见情况:

  • 游戏安装包/更新包:用 Zlib。压缩比高,包体小,用户下载快。解压慢一点没关系,反正安装是一次性的。
  • 运行时资源加载(双缓冲预解压):用 LZ4。解压速度是王道,不能让玩家等。
  • 网络传输/日志记录:用 Snappy。压缩和解压都快,CPU 开销低。

一个小技巧:你可以混合使用。比如安装包用 Zlib 压缩,安装时解压成 LZ4 格式存到本地。这样既省了下载流量,又保证了运行时的加载速度。我在一个手游项目里就是这么干的,效果很好。

2.5 代码示例:快速上手

光说不练假把式。下面给三个算法的基本用法,用 Python 演示。你可以在自己的环境里跑跑看。

import zlib
import lz4.frame
import snappy

data = b"Hello, this is a test data block for compression comparison." * 1000

# Zlib
compressed_zlib = zlib.compress(data, level=6)
decompressed_zlib = zlib.decompress(compressed_zlib)
print(f"Zlib 压缩比: {len(data)/len(compressed_zlib):.2f}")

# LZ4
compressed_lz4 = lz4.frame.compress(data)
decompressed_lz4 = lz4.frame.decompress(compressed_lz4)
print(f"LZ4 压缩比: {len(data)/len(compressed_lz4):.2f}")

# Snappy
compressed_snappy = snappy.compress(data)
decompressed_snappy = snappy.decompress(compressed_snappy)
print(f"Snappy 压缩比: {len(data)/len(compressed_snappy):.2f}")

跑完你会发现,Zlib 的压缩比最高,但压缩时间也最长。LZ4 和 Snappy 的压缩比接近,但速度差了一个数量级。

2.6 避坑指南

最后说几个我踩过的坑:

  • 别在渲染线程里解压。我曾经这么干过,结果帧率直接腰斩。解压一定要放到后台线程。
  • 注意内存碎片。频繁解压小资源会导致堆碎片。我建议用对象池或者预分配缓冲区。
  • 压缩级别不是越高越好。Zlib 级别 9 比级别 6 压缩比高不了多少,但压缩时间翻倍。不值。
  • 测试要用真实数据。用随机数据测压缩比没有意义。拿你实际的纹理、模型、配置文件来测。

曾经我犯过一个错:在移动端用 Zlib 解压 10MB 的纹理,解压花了 200ms,导致掉帧。后来换成 LZ4,解压只用了 15ms,问题解决。选对算法,收益立竿见影。