LZ 系列(通用数据压缩)
LZ 系列都基于同一个核心思想:找重复的字节序列,用引用替代。
1 2 3 4 5 6 7 8 9
| 原始数据: "abcdefabcdef"
压缩后: "abcdef" + (往前6个字节,复制6个字节) ↑ 这就是一个"引用",比重复存储省空间
压缩结果: [literal: abcdef] [match: offset=6, len=6]
|
LZMA (Lempel-Ziv-Markov chain Algorithm)
7-Zip 使用的算法,压缩率极高但速度慢。
- 在 LZ 基础上加了马尔可夫链概率模型
- 用范围编码(Range Encoding)替代哈夫曼编码
- 搜索窗口极大(最大 4GB),能找到更远的重复
普通LZ: 只看前面 32KB 找重复
LZMA: 看前面最多 4GB 找重复 → 压缩率更高,但更慢更耗内存
- 压缩率:极高(最好)
- 压缩速度:很慢
- 解压速度:中等
- 适合:安装包、资源包(压缩一次,解压多次)
LZ4
专为速度设计,压缩率换速度。
- 搜索窗口只有 64KB,不追求最优匹配
- 输出格式极简,解压只需简单的内存拷贝操作
- 几乎不做概率统计,纯粹的字节匹配
1 2 3 4 5 6 7 8 9 10 11 12 13
| LZ4 数据块格式: [token 1字节] [extra literal长度?] [literal数据] [match偏移 2字节] [extra match长度?]
数据: "abcdeabcde"
序列1: literal="abcde"(5字节) + match(偏移5, 长度4) → 复制"abcd" 序列2: literal="e"(1字节,收尾无match)
但 LZ4 规范明确规定:最后一个序列必须以至少5字节的 literal 结尾,且没有 match 部分。 所以实际上 LZ4 编码器会放弃这个 match,直接把整个 "abcdeabcde" 当 literal 输出:
token = 0xA0 → 高4位=10(literal长度10), 低4位=0 literal = "abcdeabcde"
|
- 压缩率:一般
- 压缩速度:极快
- 解压速度:极快(接近内存带宽上限)
- 适合:运行时实时解压、游戏资源热加载
LZ4HC (LZ4 High Compression)
LZ4 的高压缩变体,压缩慢但解压和 LZ4 一样快。
1 2 3
| LZ4 → 快速找"够用"的匹配就行 LZ4HC → 穷举搜索"最优"匹配,压缩慢3-10倍 但输出格式完全兼容LZ4,解压速度相同
|
适合:离线打包资源(压缩慢没关系),运行时快速解压。
DXT Texture(GPU 纹理压缩)
和上面完全不同,DXT 是专门为 GPU 设计的,解压在 GPU 硬件上完成,不占 CPU。
块压缩(Block Compression)
1 2 3 4 5 6 7 8 9 10
| 把纹理分成 4×4 像素的小块: ┌──┬──┬──┬──┐ │ │ │ │ │ ├──┼──┼──┼──┤ │ │ │ │ │ 每个 4×4 块 = 16个像素 ├──┼──┼──┼──┤ │ │ │ │ │ 原始大小 = 16 × 4字节(RGBA) = 64字节 ├──┼──┼──┼──┤ │ │ │ │ │ └──┴──┴──┴──┘
|
DXT1 (BC1)
1 2 3 4 5 6 7 8 9 10
| 每个 4×4 块只存: - 2个"端点颜色" color0, color1(各16位) - 16个像素各2位的索引(选0%/33%/66%/100%插值)
存储大小 = 8字节(原来64字节)→ 压缩比 8:1
color0 ──────────────────── color1 | | | | 100% 66% 33% 0% ↑ 每个像素用2bit选择落在哪个位置
|
Unity 中的压缩格式
AssetBundle有三种压缩模式
1 2 3 4 5 6 7 8 9
| AssetBundle 文件: ┌─────────────────────────────┐ │ Header │ 版本、压缩类型、文件信息 ├─────────────────────────────┤ │ Catalog/Manifest │ 资源索引表(资源名→文件偏移) ├─────────────────────────────┤ │ Asset Data │ 实际资源数据(压缩块) │ [block0][block1][block2] │ └─────────────────────────────┘
|
打包时指定压缩方式1 2 3
| BuildAssetBundles("Assets/AB", BuildAssetBundleOptions.None); BuildAssetBundles("Assets/AB", BuildAssetBundleOptions.ChunkBasedCompression); BuildAssetBundles("Assets/AB", BuildAssetBundleOptions.UncompressedAssetBundle);
|
LZMA(整体压缩)
1 2 3 4 5 6 7
| 整个 AB 文件作为一个流压缩: ┌────────────────────────────────────┐ │ LZMA压缩流(Header+Catalog+Data) │ └────────────────────────────────────┘
加载时必须解压整个文件才能读取任何一个资源 → 体积最小,但加载慢,内存峰值高
|
LZ4(块压缩)
1 2 3 4 5 6 7
| 每个数据块独立压缩: ┌────────┬────────┬────────┬────────┐ │ block0 │ block1 │ block2 │ block3 │ 每块独立LZ4压缩 └────────┴────────┴────────┴────────┘
加载某个资源时只解压它所在的块 → 体积稍大,但可以随机访问,加载快
|
下载包 vs 运行时加载
这是两个不同阶段,关注点完全不同:
1 2 3 4 5 6 7 8 9
| 下载阶段: 用户等待下载 → 关注流量和时间 → 用 LZMA,体积最小,节省流量和下载时间 → 下载完存到本地磁盘
运行时加载阶段: 游戏运行中加载资源 → 关注速度和流畅度 → 用 LZ4,随机访问,解压快,不卡帧 → 从本地磁盘读取并解压到内存
|
实际工作流:
1 2 3 4 5 6 7
| 服务器存 LZMA 包 ↓ 用户下载 本地磁盘存 LZMA 包 ↓ 首次加载时 Unity 转换(RecompressAssetBundleAsync) 本地缓存存 LZ4 包 ↓ 之后每次启动 直接加载 LZ4 包,速度快
|
- Unity 不会自动将LZMA转换成LZ4(除非用 UnityWebRequest + 缓存版本号)
- 转换 = 解压 LZMA + 重压缩 LZ4,是两步操作
- 推荐在下载完成后立即转换,游戏运行时直接读 LZ4
Unity 提供的重压缩 API1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| IEnumerator DownloadAndRecompress(string url, string savePath, string cachePath) { using var req = UnityWebRequestAssetBundle.GetAssetBundle(url); yield return req.SendWebRequest();
File.WriteAllBytes(savePath, req.downloadHandler.data);
var op = AssetBundle.RecompressAssetBundleAsync( savePath, cachePath, BuildCompression.LZ4, 0, ThreadPriority.Low ); yield return op;
File.Delete(savePath);
}
|
哈夫曼编码
思路:高频字符用短编码,低频字符用长编码,减少总位数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 原始数据:"AAAAABBBCD" 统计频率:A=5, B=3, C=1, D=1
建哈夫曼树: 10 / \ A 5 (5) / \ B 2 (3) / \ C D (1) (1)
编码结果: A → 0 (1位) B → 10 (2位) C → 110 (3位) D → 111 (3位)
原始:10字符 × 8位 = 80位 压缩:5×1 + 3×2 + 1×3 + 1×3 = 17位 → 压缩率 79%
|
DEFLATE = LZ77 + 哈夫曼(zip/gzip/PNG 都用这个)
先用 LZ 消除重复,再用哈夫曼压缩剩余符号:
1 2 3 4 5 6 7 8 9 10 11 12 13
| 第一步 LZ77: 原始: "abcdeabcde" 输出: [literal:abcde] [match:5,5] ↓ 转成符号流 a b c d e <match:5,5>
第二步 哈夫曼: 对符号流统计频率,高频符号用短编码 a,b,c,d,e 各出现1次 <match> 出现1次 → 再压缩一遍
两步叠加,压缩率比单独用任何一种都高
|