乌啦呀哈呀哈乌啦!

欢迎光临,这里是喵pass的个人博客,希望有能帮到你的地方

0%

压缩格式

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);              // LZMA
BuildAssetBundles("Assets/AB", BuildAssetBundleOptions.ChunkBasedCompression); // LZ4
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 提供的重压缩 API
1
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)
{
// 下载 LZMA 包
using var req = UnityWebRequestAssetBundle.GetAssetBundle(url);
yield return req.SendWebRequest();

// 存到磁盘
File.WriteAllBytes(savePath, req.downloadHandler.data);

// 立即转换成 LZ4 存到缓存目录
var op = AssetBundle.RecompressAssetBundleAsync(
savePath, cachePath,
BuildCompression.LZ4,
0, ThreadPriority.Low
);
yield return op;

// 删掉原始 LZMA 包(可选)
File.Delete(savePath);

// 之后加载用 cachePath
}

哈夫曼编码

思路:高频字符用短编码,低频字符用长编码,减少总位数。

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次
→ 再压缩一遍

两步叠加,压缩率比单独用任何一种都高