LZ4 的核心解压循环 按照 [Token][字面量溢出][原文][Offset][匹配溢出] 的顺序读取,并还原出原始数据。
这五个部分构成了 LZ4 压缩格式中每一个“序列”(Sequence)的完整二进制结构。解压器就是严格按照这个固定顺序,像流水线一样依次读取并还原数据的。
它们各自的作用如下:
1. Token(令牌)
- 是什么:每个序列的第一个字节,是整个序列的“控制头”。
- 干嘛用:它被劈成两半使用。高 4 位记录字面量长度,低 4 位记录匹配长度。
- 核心意义:它是解压器的“导航仪”,告诉解压器接下来该读多少字面量、该回溯多远复制多少匹配数据。
2. 字面量溢出(Literal Length Overflow)
- 是什么:0 个或多个额外字节,仅当 Token 高 4 位等于 15 时才存在。
- 干嘛用:因为 Token 只有 4 bit,最多只能表示 15。如果实际字面量长度 ≥ 15,就需要这些额外字节来“接力”表达超出的部分(遇到 0xFF 继续累加,直到读到 < 0xFF 的字节为止)。
- 核心意义:让 4 bit 的空间能表达任意大的字面量长度,短字面量零开销,长字面量按需扩展。
3. 原文(Literals / 字面量数据)
- 是什么:一段未经压缩的原始字节,长度由前两步确定。
- 干嘛用:直接拷贝到输出缓冲区。这些数据在历史窗口中找不到匹配,无法被压缩,只能原样保留。
- 核心意义:保证无损还原。压缩不是魔法,不能匹配的数据必须忠实记录。
4. Offset(偏移量 / 回溯距离)
- 是什么:固定的 2 字节小端序整数,紧跟在字面量数据之后。
- 干嘛用:告诉解压器“去已经解压出来的数据中,往回倒退多少个字节开始复制”。例如 Offset=9 表示从当前位置往前数 9 个字节处开始拷贝。
- 核心意义:这是 LZ4 实现压缩的核心机制——用 2 字节的地址引用代替一大段重复数据。注意:最后一个序列可能没有 Offset(只有字面量),解压完字面量后若已达目标长度就直接结束。
5. 匹配溢出(Match Length Overflow)
- 是什么:0 个或多个额外字节,仅当 Token 低 4 位等于 15 时才存在。
- 干嘛用:与字面量溢出原理相同。Token 低 4 位最多表示 15,加上最小匹配长度 4,即 Token 直接能表达的最大匹配长度为 19。超过 19 的部分通过这些额外字节编码。
- 核心意义:让短匹配(绝大多数情况)只需 Token 一个字节就能表达,超长匹配才付出额外字节代价。
📊 一张图看整体关系
┌─────────┬──────────────┬──────────┬─────────┬──────────────┐
│ Token │ 字面量溢出 │ 原文 │ Offset │ 匹配溢出 │
│ (1 byte) │ (0~N bytes) │ (L bytes)│(2 bytes)│ (0~N bytes) │
├─────────┼──────────────┴──────────┴─────────┴──────────────┤
│ 高4位:字面量长度 │
│ 低4位:匹配长度 │
└─────────────────────────────────────────────────────────────┘
↑ 决定第2部分是否存在及长度 ↑ 决定第5部分是否存在及长度
💡 为什么是这个顺序?
这个顺序是精心设计的,目的是让解压器能够单遍、流式、无需回溯地完成解压:
1. 先读 Token → 立刻知道接下来要处理什么
2. 再读字面量溢出 → 确定要拷贝多少原文
3. 拷贝原文 → 输出缓冲区有了新数据
4. 读 Offset → 知道去哪里找重复数据
5. 读匹配溢出 → 确定要复制多长的重复数据
6. 执行复制 → 完成本序列,回到步骤 1 处理下一个序列
每一步都只依赖前面已读取的信息,不需要预扫描或缓存整个块,这就是 LZ4 解压速度极快的根本原因。
- Offset:决定了复制的起点(从已解压数据中往回倒退多少个字节开始拷贝)。
- 匹配溢出:决定了复制的长度(当 Token 低4位不够用时,补充表达要拷贝多少字节)。
为了让你更直观地确认,我们把这两者在解压代码中的对应关系再对齐一下:
📍 Offset = 从哪开始复制
// 读取2字节小端序偏移量
final int matchDec = (compressed.readByte() & 0xFF) | ((compressed.readByte() & 0xFF) << 8);
// 计算复制起点:当前位置 - 偏移量
int ref = dOff - matchDec;
⚠️ 注意:Offset 是相对于当前写入位置的回溯距离,不是绝对地址。matchDec = 1 表示从上一个刚写入的字节开始复制,matchDec = 9 表示往前数9个字节。
📏 匹配溢出 = 拷贝多少(的补充部分)
// Token低4位提供基础长度(0~15 → 实际4~19)
int matchLen = token & 0x0F;
// 如果基础长度达到上限15,说明还有额外长度需要读取
if (matchLen == 0x0F) {
int len;
while ((len = compressed.readByte()) == (byte) 0xFF) {
matchLen += 0xFF; // 每读到0xFF累加255
}
matchLen += len & 0xFF; // 加上终止字节
}
// 最终复制长度 = 基础值 + 溢出值 + 最小匹配4
matchLen += MIN_MATCH;
🔗 两者如何协作完成一次复制
已解压数据: [A B C D E F G H I J K L M N O P]
↑ dOff(当前位置)
Offset = 6 → 起点 = dOff - 6 = 指向 'K'
matchLen = 5 → 从 'K' 开始复制 5 个字节: K L M N O
结果: [A B C D E F G H I J K L M N O P K L M N O]
💡 一个容易混淆的点
匹配溢出本身不是完整的拷贝长度,它只是完整长度的补充部分。完整的拷贝长度由三部分组成:
组成部分 来源 作用
基础值 Token 低4位 (0~15) 短匹配零开销表达
溢出值 匹配溢出字节 (0~N bytes) 仅在基础值=15时存在
MIN_MATCH 常量 4 因为小于4字节的匹配不值得压缩
所以准确地说:Token 低4位 + 匹配溢出 + 4 = 总共拷贝多少字节。
你的理解方向完全没问题,只是记住“匹配溢出”是增量而非总量就可以了。