LZ4 的核心解压循环 按照 [Token][字面量溢出][原文][Offset][匹配溢出] 的顺序读取,并还原出原始数据。
2026/7/3 6:20:15 网站建设 项目流程

这五个部分构成了 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 = 总共拷贝多少字节。

你的理解方向完全没问题,只是记住“匹配溢出”是增量而非总量就可以了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询