目录
  1. 1. 前言
  2. 2. 一、GIF 文件格式全景:从 GIF87a 到 GIF89a
    1. 2.1. 1.1 历史沿革
    2. 2.2. 1.2 文件整体结构
  3. 3. 二、Header 与 Logical Screen Descriptor
    1. 3.1. 2.1 文件头(6 字节)
    2. 3.2. 2.2 逻辑屏幕描述符(7 字节)
      1. 3.2.1. packed_field 逐位解析
  4. 4. 三、Global Color Table(全局颜色表)
    1. 4.1. 3.1 结构
    2. 4.2. 3.2 全局 vs 局部颜色表
  5. 5. 四、Image Descriptor 与图像数据块
    1. 5.1. 4.1 Image Descriptor 结构(10 字节)
    2. 5.2. 4.2 交错(Interlace)与 Adam7 四遍扫描
    3. 5.3. 4.3 LZW Minimum Code Size
  6. 6. 五、LZW 压缩算法深度解析
    1. 6.1. 5.1 字典初始化
    2. 6.2. 5.2 可变长度编码
    3. 6.3. 5.3 解码算法
    4. 6.4. 5.4 LZW 数据中的子块(Sub-block)结构
  7. 7. 六、Graphic Control Extension(图形控制扩展)
    1. 7.1. 6.1 字节结构
    2. 7.2. 6.2 Packed Field 位结构
    3. 7.3. 6.3 Disposal Method(处置方式)
    4. 7.4. 6.4 Delay Time
  8. 8. 七、Application Extension(NETSCAPE 2.0 循环控制)
    1. 8.1. 7.1 结构
    2. 8.2. 7.2 解析实现
  9. 9. 八、Plain Text Extension 与 Comment Extension
    1. 9.1. 8.1 Plain Text Extension(纯文本扩展)
    2. 9.2. 8.2 Comment Extension(注释扩展)
    3. 9.3. 8.3 通用的扩展跳过逻辑
  10. 10. 九、完整 C 语言 GIF 解码器实现
    1. 10.1. 9.1 数据结构汇总
    2. 10.2. 9.2 主解析循环
    3. 10.3. 9.3 渲染到 Canvas 与动画控制
  11. 11. 十、JNI 桥接与 Kotlin/Java 调用
    1. 11.1. 10.1 JNI 接口设计
    2. 11.2. 10.2 Kotlin 封装层
    3. 11.3. 10.3 动画播放器
  12. 12. 十一、Android 系统方案对比
    1. 12.1. 11.1 Movie 类(已废弃)
    2. 12.2. 11.2 ImageDecoder(API 28+)
    3. 12.3. 11.3 三种方案对比
  13. 13. 十二、结语
GIF解析

前言

GIF(Graphics Interchange Format)诞生于 1987 年,由 CompuServe 推出,是互联网早期最重要的图像格式之一。不同于 JPEG 的有损压缩和 PNG 的静态单帧画面,GIF 凭借无损 LZW 压缩256 色调色板索引以及多帧动画能力,在表情包、简单动画、图标等场景中至今无可替代。

对于 Android 开发者而言,系统自带的 ImageDecoder(API 28+)和已废弃的 Movie 类提供了 GIF 播放能力,但理解其二进制格式并实现一个原生 C 语言解码器不仅能深入掌握 LZW 压缩、调色板索引、交错渲染等底层技术,还能在需要自定义动画行为(如帧缓存策略、位图复用池、内存映射解码)的场景中提供系统 API 无法给予的灵活性。

参考规范: GIF89a Specification (W3C), Covering the GIF87a and GIF89a formats.
源码地址: GitHub - leocheung/gif-decoder(示例代码均可在 NDK 环境下编译运行)


一、GIF 文件格式全景:从 GIF87a 到 GIF89a

1.1 历史沿革

GIF 经历了两个主要版本:

版本 发布年份 关键特性
GIF87a 1987 静态帧、LZW 压缩、交错(Interlace)、Global Color Table
GIF89a 1989 新增 Graphic Control Extension(动画控制/透明色)、Application Extension(循环计数)、Comment/Plain Text Extension

两者最核心的差异在于 Extension Block 体系:GIF87a 没有扩展块,每个文件仅包含单张图像数据;GIF89a 引入了以 0x21 为标记的扩展块,使得多帧动画透明色支持循环播放控制成为可能。

1.2 文件整体结构

一个完整的 GIF89a 动画文件遵循以下层级结构:

┌────────────────────────────────────────┐
│ Header (6 bytes) │ "GIF89a" 或 "GIF87a"
├────────────────────────────────────────┤
│ Logical Screen Descriptor (7 bytes) │ 画布尺寸 + 全局调色板参数
├────────────────────────────────────────┤ ← 如果全局颜色表标志 = 1
│ Global Color Table (3 × N bytes) │ 全局调色板
├────────────────────────────────────────┤
│ [Extension Block] │ 可选:循环控制(GCE)
│ [Extension Block] │ 可选:注释(Comment)
├────────────────────────────────────────┤
│ Image Descriptor (10 bytes) │ 第一帧描述
│ [Local Color Table] │ 可选:局部调色板
│ Image Data (LZW compressed) │ LZW 压缩的图像索引数据
├────────────────────────────────────────┤
│ [Graphic Control Extension] │ 下一帧的延迟/透明/处置方式
│ Image Descriptor │ 第二帧描述
│ ... │
├────────────────────────────────────────┤
│ ... 重复帧数据 ... │
├────────────────────────────────────────┤
│ Trailer (1 byte: 0x3B) │ 文件结束标记
└────────────────────────────────────────┘

每个数据块都由一个引入符(Introducer)标识其类型:

引入符 十六进制 含义
, 0x2C Image Descriptor(图像描述符)
! 0x21 Extension Introducer(扩展引入符)
; 0x3B Trailer(文件尾)

二、Header 与 Logical Screen Descriptor

2.1 文件头(6 字节)

typedef struct __attribute__((packed)) {
char signature[3]; // 固定 "GIF"
char version[3]; // "87a" 或 "89a"
} GifHeader;

解析时首先校验前 6 字节:

int validate_header(const uint8_t *data, size_t len) {
if (len < 6) return -1;
if (memcmp(data, "GIF", 3) != 0) return -2;
if (memcmp(data + 3, "87a", 3) != 0 &&
memcmp(data + 3, "89a", 3) != 0) return -3;
return (data[4] == '8' && data[5] == '9') ? 89 : 87;
}

2.2 逻辑屏幕描述符(7 字节)

紧随 Header 之后的是 Logical Screen Descriptor(LSD),定义了整个 GIF 动画的逻辑画布尺寸全局颜色表参数

typedef struct __attribute__((packed)) {
uint16_t canvas_width; // 逻辑画布宽度(小端序)
uint16_t canvas_height; // 逻辑画布高度(小端序)
uint8_t packed_field; // 打包字段(位域)
uint8_t background_color; // 背景色索引(指向全局颜色表)
uint8_t pixel_aspect_ratio; // 像素宽高比(通常为 0)
} GifLogicalScreenDescriptor;

packed_field 逐位解析

  Bit 7        Bit 6-4           Bit 3        Bit 2-0
┌───────┐ ┌──────────────┐ ┌──────────┐ ┌──────────────┐
│ M │ │ Color Res │ │ Sort Flg │ │ GCT Size │
│ (GCT │ │ (cr+1 bits) │ │(是否排序)│ │ (2^(N+1)) │
│ flag)│ │ │ │ │ │ │
└───────┘ └──────────────┘ └──────────┘ └──────────────┘
  • Bit 7 (M): 全局颜色表标志。1 = 存在全局颜色表,0 = 不存在(每帧必须有局部颜色表)。
  • Bits 4-6 (cr): 颜色分辨率,值为 原始颜色位数 - 1。例如 cr=7 表示源图像使用了 8 bits/primary color(共 256 级)。
  • Bit 3 (S): 排序标志。若 1,全局颜色表按使用频率递减排列(GIF87a 中指示解码器可据此优化显示;GIF89a 中此位含义有所弱化)。
  • Bits 0-2 (size): 全局颜色表大小。实际条目数 = 2^(size + 1)

提取字段示例

#define GIF_HAS_GCT(packed)       ((packed) >> 7) & 0x01)
#define GIF_COLOR_RESOLUTION(p) (((packed) >> 4) & 0x07)
#define GIF_SORT_FLAG(p) (((packed) >> 3) & 0x01)
#define GIF_GCT_SIZE(p) (2 << ((packed) & 0x07)) // = 2^(value+1)

Pixel Aspect Ratio:若值为 0,表示正方形像素(1:1)。非零时,实际宽高比 = (value + 15) / 64。现代显示设备几乎总是正方形像素,因此该字段在实践中通常为 0。


三、Global Color Table(全局颜色表)

3.1 结构

packed_field 的 bit 7 为 1,则 LSD 之后紧跟着全局颜色表。每个条目 3 字节,按 R-G-B 顺序存储:

typedef struct __attribute__((packed)) {
uint8_t r;
uint8_t g;
uint8_t b;
} GifColor; // 3 bytes per entry

// 颜色表总量
// GifColor global_table[gct_size]; 其中 gct_size = 2^(size_field + 1)

例如 size_field = 6 时,GCT 包含 2^(6+1) = 128 个条目,占用 128 × 3 = 384 字节。

3.2 全局 vs 局部颜色表

  • 全局颜色表(GCT):应用于所有未指定局部颜色表的帧。
  • 局部颜色表(LCT):单帧专属,可覆盖 GCT。LCT 的存在性由 Image Descriptor 的 packed_field bit 7 控制。

解码时的颜色表选择逻辑:

GifColor* get_color_table(GifImageDesc *img, GifColor *global) {
if (img->has_local_ct) return img->local_color_table;
return global;
}

四、Image Descriptor 与图像数据块

4.1 Image Descriptor 结构(10 字节)

每个图像块以 0x2C 标记开始,紧接着是 9 字节的 Image Descriptor:

typedef struct __attribute__((packed)) {
uint8_t separator; // 固定 0x2C
uint16_t left; // 图像左上角 X(相对逻辑画布)
uint16_t top; // 图像左上角 Y(相对逻辑画布)
uint16_t width; // 图像宽度(像素)
uint16_t height; // 图像高度(像素)
uint8_t packed_field; // 打包字段
} GifImageDescriptor;

packed_field 位结构

  Bit 7        Bit 6        Bit 5     Bit 4     Bit 3-0
┌───────┐ ┌──────────┐ ┌───────┐ ┌───────┐ ┌──────────┐
│ M │ │ Interlace│ │ Sort │ │ Resv'd│ │ LCT Size │
│(局部色│ │ 交错标志 │ │ 排序 │ │ 保留 │ │ 2^(N+1) │
│ 表标志)│ │ │ │ │ │ │ │ │
└───────┘ └──────────┘ └───────┘ └───────┘ └──────────┘
  • Bit 6 (Interlace): 若为 1,图像采用 4-pass 交错方式编码(详见 4.2)。
  • Bit 7 (M): 若为 1,此帧拥有自己的局部颜色表。

4.2 交错(Interlace)与 Adam7 四遍扫描

交错显示的设计初衷是在网络带宽极低的年代(调制解调器拨号),让用户快速看到图像的大致轮廓。GIF 使用 4-pass 交错与 PNG 的 Adam7(7-pass)有所不同。

解码器重建图像时,行按以下顺序写入输出缓冲区:

Pass 1 (每 8 行):  row 0, 8, 16, 24, ...  (1/8 of image rows)
Pass 2 (每 8 行): row 4, 12, 20, 28, ... (1/8 of image rows)
Pass 3 (每 4 行): row 2, 6, 10, 14, ... (1/4 of image rows)
Pass 4 (每 2 行): row 1, 3, 5, 7, ... (1/2 of image rows)

交错重建的 C 实现:

// 交错行的遍历顺序表
static const int INTERLACE_PASS_START[] = { 0, 4, 2, 1 };
static const int INTERLACE_PASS_STEP[] = { 8, 8, 4, 2 };

void deinterlace_pixels(uint8_t *dest, const uint8_t *src,
uint16_t width, uint16_t height) {
int src_idx = 0;
for (int pass = 0; pass < 4; pass++) {
int start_row = INTERLACE_PASS_START[pass];
int step = INTERLACE_PASS_STEP[pass];
for (int y = start_row; y < height; y += step) {
memcpy(dest + y * width, src + src_idx * width, width);
src_idx++;
}
}
}

4.3 LZW Minimum Code Size

Image Descriptor 之后紧跟着一个字节的 LZW Minimum Code Size(简称 min_code_sizelzw_min)。这是 LZW 压缩的初始编码位数。

对于 8 位索引颜色(256 色),min_code_size 通常为 8。实际编码时,第一个可用码从 min_code_size + 1 位开始。


五、LZW 压缩算法深度解析

LZW(Lempel-Ziv-Welch)是 GIF 的核心压缩算法,是一种基于字典的无损压缩方法。

5.1 字典初始化

GIF 的 LZW 字典预置了以下条目:

编码值 含义
0 ~ 255 单字节字面量(对应 0x00 ~ 0xFF)
256 Clear Code:重置字典(清空所有动态条目,码长恢复到初始值)
257 **End of Information (EOI)**:图像数据结束标记
258+ 动态字典条目(运行时构建的多字节序列)

5.2 可变长度编码

GIF LZW 的编码位数动态增长

  • 初始位数 = min_code_size + 1(例如 min=8 时,初始 9 位)
  • 每当下一个要插入的编码值达到 2^current_code_size 时,码长 +1
  • 最大码长 = 12 位(GIF 规范限制,对应字典条目上限 4096)
// 确定当前是否需要增加码长
int code_size = min_code_size + 1;
int clear_code = 1 << min_code_size;
int eoi_code = clear_code + 1;
int next_code = eoi_code + 1; // 下一个可分配的编码
int max_code = (1 << code_size);

// 当 next_code 达到 max_code 且 code_size < 12 时增大位数
if (next_code >= max_code && code_size < 12) {
code_size++;
max_code = (1 << code_size);
}

5.3 解码算法

解码器需要维护一个与编码器一致的动态字典。每个字典条目是一个 (prefix_code, suffix_byte) 二元组。

解码流程

1. 读取第一个编码 → 直接输出对应的单字节(必然是 0-255 的字面量或 Clear Code)
2. 循环读取后续编码 code:
a) 若 code == Clear Code (256):
- 重置字典(保留 0~257,清空 258+)
- 重置 code_size = min_code_size + 1
- 读取下一个 code 作为新的"前一个码",转到步骤 2
b) 若 code == EOI Code (257): 结束解码
c) 若 code 在字典中:
- 输出 code 对应的字节序列
- 将 (prev_code, first_byte_of_code) 添加入字典
d) 若 code **不在**字典中(KωKωK 特殊情况):
- 输出 prev_code 的序列 + 其首字节
- 将 (prev_code, first_byte_of_prev_code) 添加入字典
e) 更新 prev_code = code

完整的 C 语言 LZW 解码器

#define MAX_DICT_SIZE  4096
#define MAX_CODE_BITS 12

typedef struct {
uint16_t prefix; // 前缀编码
uint8_t suffix; // 后缀字节
uint8_t length; // 序列长度(可选,便于输出)
uint8_t first; // 此序列的首字节(编码时缓存)
} LzwDictEntry;

typedef struct {
uint8_t *data; // 输入的 LZW 码流
size_t size; // 数据总字节数
uint32_t bit_pos; // 当前读取位位置
uint32_t bit_buf; // 位缓冲区
int bit_count; // 缓冲区中剩余的位数
} BitReader;

// --- Bit Reader 实现 ---
static uint32_t read_bits(BitReader *br, int n) {
while (br->bit_count < n) {
if (br->bit_pos >= br->size) break;
br->bit_buf |= (uint32_t)br->data[br->bit_pos++] << br->bit_count;
br->bit_count += 8;
}
uint32_t result = br->bit_buf & ((1 << n) - 1);
br->bit_buf >>= n;
br->bit_count -= n;
return result;
}

// --- LZW 解码主函数 ---
int lzw_decode(const uint8_t *lzw_data, size_t data_len,
uint8_t *output, size_t output_max,
int min_code_size) {
LzwDictEntry dict[MAX_DICT_SIZE];
BitReader br = { .data = (uint8_t *)lzw_data, .size = data_len };
size_t out_pos = 0;

// 初始化字典 0~255
for (int i = 0; i < 256; i++) {
dict[i].prefix = 0xFFFF; // 无效
dict[i].suffix = (uint8_t)i;
dict[i].first = (uint8_t)i;
dict[i].length = 1;
}

int clear_code = 1 << min_code_size;
int eoi_code = clear_code + 1;
int code_size = min_code_size + 1;
int next_code = eoi_code + 1;
int max_code = 1 << code_size;

// 第一个码必然是 clear_code 或字面量
uint16_t code = read_bits(&br, code_size);
if (code == eoi_code) return out_pos;

// 输出第一个码对应的字节
uint8_t *first_out = output;
uint16_t prev_code = code;
uint8_t first_char = (code < 256) ? (uint8_t)code : dict[code].first;
output[out_pos++] = first_char;

// 主解码循环
while (out_pos < output_max) {
code = read_bits(&br, code_size);
if (code == eoi_code) break;

if (code == clear_code) {
// 重置字典
next_code = eoi_code + 1;
code_size = min_code_size + 1;
max_code = 1 << code_size;

code = read_bits(&br, code_size);
if (code == eoi_code) break;
prev_code = code;
output[out_pos++] = (code < 256) ? (uint8_t)code : dict[code].first;
continue;
}

uint8_t *entry_start;
int entry_len;

if (code < next_code) {
// code 在字典中 → 输出其展开序列
// 同时添加 (prev_code, first_of_code) 到字典
uint8_t stack[MAX_DICT_SIZE];
int stack_pos = 0;
uint16_t c = code;
while (c >= 256 && c < next_code) {
stack[stack_pos++] = dict[c].suffix;
c = dict[c].prefix;
}
stack[stack_pos++] = (uint8_t)c; // 根字符(0-255)
first_char = (uint8_t)c;

// 倒序输出
for (int i = stack_pos - 1; i >= 0; i--) {
if (out_pos >= output_max) break;
output[out_pos++] = stack[i];
}
} else {
// KωKωK 特例:code == next_code
uint8_t stack[MAX_DICT_SIZE];
int stack_pos = 0;
uint16_t c = prev_code;
while (c >= 256 && c < next_code) {
stack[stack_pos++] = dict[c].suffix;
c = dict[c].prefix;
}
uint8_t root = (uint8_t)c;
first_char = root;
stack[stack_pos++] = root;

for (int i = stack_pos - 1; i >= 0; i--) {
if (out_pos >= output_max) break;
output[out_pos++] = stack[i];
}
}

// 添加新条目到字典 (prev_code, first_char)
if (next_code < MAX_DICT_SIZE) {
dict[next_code].prefix = prev_code;
dict[next_code].suffix = first_char;
dict[next_code].first = dict[prev_code].first;
dict[next_code].length = dict[prev_code].length + 1;
next_code++;
}

// 检查是否需要扩大码长
if (next_code >= max_code && code_size < MAX_CODE_BITS) {
code_size++;
max_code = 1 << code_size;
}

prev_code = code;
}

return out_pos;
}

5.4 LZW 数据中的子块(Sub-block)结构

LZW 压缩后的数据并非连续字节流,而是被分割为子块(Sub-blocks),每个子块结构为:

+-------+-------+-------+     +-------+
| Size | data | Size | ... | 0x00 |
|(1byte)|(N字节)|(1byte)| |(结束) |
+-------+-------+-------+ +-------+
  • 每个子块最大 255 字节
  • 子块大小为 0 表示整个图像数据的 LZW 子块结束
// 收集所有 LZW 子块为连续字节流
size_t read_lzw_sub_blocks(uint8_t *src, size_t src_len,
uint8_t *dst, size_t dst_max) {
size_t total = 0;
size_t offset = 0;
while (offset < src_len) {
uint8_t block_size = src[offset++];
if (block_size == 0) break; // 终止子块
if (total + block_size > dst_max) break;
memcpy(dst + total, src + offset, block_size);
total += block_size;
offset += block_size;
}
return total;
}

六、Graphic Control Extension(图形控制扩展)

GCE 是 GIF89a 最重要的扩展之一,控制每一帧的延迟时间、透明色和处置方式

6.1 字节结构

偏移  大小  含义
+0 1 扩展引入符 (0x21)
+1 1 扩展标签 (0xF9 = Graphic Control)
+2 1 块大小 (固定 0x04)
+3 1 Packed Field
+4 2 Delay Time (1/100 秒,小端序)
+6 1 Transparent Color Index
+7 1 块结束符 (0x00)

6.2 Packed Field 位结构

  Bit 7-5       Bit 4-2             Bit 1        Bit 0
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
│ Reserved │ │ Disposal │ │ User │ │ Transp. │
│ (保留) │ │ Method │ │ Input Flg│ │ Color Flg│
└──────────┘ └──────────────┘ └──────────┘ └──────────┘

6.3 Disposal Method(处置方式)

处置方式决定当前帧渲染后,帧缓冲区如何处理被下一帧覆盖之前的背景区域:

名称 行为 使用场景
0 None / Unspecified 不做处理,直接覆盖新帧 全帧覆盖型动画
1 Do Not Dispose 保留当前帧图像 逐帧叠加(如画笔描边)
2 Restore to Background 将被下一帧覆盖的区域恢复为背景色 移动的小物体(背景不变)
3 Restore to Previous 恢复为上一帧的图像内容 覆盖型动画中仅需保存一帧

三种处置方式的渲染逻辑

void apply_disposal(uint32_t *canvas, uint32_t *prev_canvas,
int width, int height,
GifFrame *current, GifFrame *next,
int disposal_method, uint32_t bg_color) {
switch (disposal_method) {
case 0: // None: 不做任何处理
case 1: // Do Not Dispose: 保留(后续帧叠加在上面)
break;
case 2: // Restore to Background
// 将下一帧区域填充为背景色
for (int y = next->top; y < next->top + next->height; y++) {
for (int x = next->left; x < next->left + next->width; x++) {
canvas[y * width + x] = bg_color;
}
}
break;
case 3: // Restore to Previous
// 将下一帧区域恢复为上一帧的像素
for (int y = next->top; y < next->top + next->height; y++) {
memcpy(&canvas[y * width + next->left],
&prev_canvas[y * width + next->left],
next->width * sizeof(uint32_t));
}
break;
}
}

6.4 Delay Time

单位是 1/100 秒(centisecond)。例如 delay_time = 50 表示 0.5 秒(20 FPS)。

// 将 delay_time 转换为毫秒
int delay_ms = delay_time * 10; // 50 centiseconds → 500ms

七、Application Extension(NETSCAPE 2.0 循环控制)

Netscape Navigator 2.0 引入的应用扩展定义了 GIF 动画的循环播放次数

7.1 结构

偏移  大小    内容
+0 1 扩展引入符 (0x21)
+1 1 扩展标签 (0xFF = Application Extension)
+2 1 块大小 (固定 0x0B = 11 字节)
+3 11 应用标识符 "NETSCAPE" + 认证码 "2.0"
+14 1 子块大小 (0x03)
+15 1 子块 ID (0x01 = loop count)
+16 2 循环次数(小端序,0 = 无限循环)
+18 1 子块结束符 (0x00)

7.2 解析实现

int parse_netscape_ext(const uint8_t *data, size_t len, size_t *offset,
uint16_t *loop_count) {
if (data[*offset] != 0x21 || data[*offset + 1] != 0xFF) return -1;
*offset += 2;
uint8_t block_size = data[*offset++];

if (block_size != 11) return -2;
if (memcmp(data + *offset, "NETSCAPE2.0", 11) != 0) {
*offset += block_size;
return 0; // 不是 NETSCAPE 扩展,跳过
}
*offset += block_size;

// 读子块
while (*offset < len) {
uint8_t sub_size = data[*offset++];
if (sub_size == 0) break;
if (sub_size == 3 && data[*offset] == 1) {
*loop_count = data[*offset + 1] | (data[*offset + 2] << 8);
}
*offset += sub_size;
}
return 1;
}

八、Plain Text Extension 与 Comment Extension

8.1 Plain Text Extension(纯文本扩展)

允许在 GIF 图像上叠加栅格化文本。标签 0x01。实践中极少使用,多数解码器仅跳过而不渲染。

0x21 0x01 [块大小 0x0C] [左侧 2B] [顶部 2B] [宽 2B] [高 2B]
[字符宽 1B] [字符高 1B] [文本色 1B] [背景色 1B] [文本子块...] 0x00

8.2 Comment Extension(注释扩展)

标签 0xFE。包含 ASCII 文本注释子块。不影响图像渲染,解码器可安全跳过。

0x21 0xFE [注释子块...] 0x00

8.3 通用的扩展跳过逻辑

void skip_extension(const uint8_t *data, size_t len, size_t *offset) {
*offset += 2; // 跳过 0x21 + 标签
while (*offset < len) {
uint8_t block_size = data[*offset++];
if (block_size == 0) break; // 终止子块
*offset += block_size;
}
}

九、完整 C 语言 GIF 解码器实现

9.1 数据结构汇总

typedef enum {
GIF_STATE_HEADER,
GIF_STATE_LSD,
GIF_STATE_GCT,
GIF_STATE_BLOCK,
GIF_STATE_DONE
} GifParseState;

typedef struct {
uint32_t *pixels; // ARGB 像素数据
int left, top; // 帧在画布上的偏移
int width, height;
int delay_ms; // 帧延迟(毫秒)
int disposal; // 处置方式 0-3
int has_transparency; // 是否有透明色
uint8_t transparent_idx; // 透明色索引
} GifFrame;

typedef struct {
int version; // 87 或 89
uint16_t canvas_width;
uint16_t canvas_height;
uint8_t bg_color_idx;
int has_gct;
int gct_size; // 实际条目数
GifColor *global_ct;
int frame_count;
GifFrame *frames;
uint16_t loop_count; // 0 = 无限
int error;
const char *error_msg;
} GifDecoder;

9.2 主解析循环

int gif_parse(GifDecoder *decoder, const uint8_t *data, size_t len) {
size_t pos = 0;
memset(decoder, 0, sizeof(GifDecoder));

// 1. 解析 Header
if (pos + 6 > len) return -1;
decoder->version = validate_header(data, len);
if (decoder->version < 0) { decoder->error = decoder->version; return -1; }
pos = 6;

// 2. 解析 Logical Screen Descriptor
if (pos + 7 > len) return -2;
decoder->canvas_width = data[pos] | (data[pos+1] << 8); pos += 2;
decoder->canvas_height = data[pos] | (data[pos+1] << 8); pos += 2;
uint8_t packed = data[pos++];
decoder->bg_color_idx = data[pos++];
/* pixel_aspect_ratio = */ data[pos++];

decoder->has_gct = GIF_HAS_GCT(packed);
decoder->gct_size = GIF_GCT_SIZE(packed);

// 3. 解析全局颜色表
if (decoder->has_gct) {
decoder->global_ct = malloc(decoder->gct_size * sizeof(GifColor));
for (int i = 0; i < decoder->gct_size; i++) {
decoder->global_ct[i].r = data[pos++];
decoder->global_ct[i].g = data[pos++];
decoder->global_ct[i].b = data[pos++];
}
}

// 4. 预扫描计数帧数(用于分配 frames 数组)
size_t scan_pos = pos;
int frame_count = 0;
while (scan_pos < len) {
uint8_t c = data[scan_pos];
if (c == 0x2C) frame_count++;
else if (c == 0x3B) break;
else if (c == 0x21) skip_extension(data, len, &scan_pos);
else break;
if (c != 0x21) scan_pos++; // 继续扫描
}

decoder->frames = calloc(frame_count + 1, sizeof(GifFrame));
decoder->frame_count = 0;

// 5. 主解析循环
uint32_t *canvas = calloc(decoder->canvas_width * decoder->canvas_height,
sizeof(uint32_t));
uint32_t *prev_canvas = calloc(decoder->canvas_width * decoder->canvas_height,
sizeof(uint32_t));
int disposal = 0;
int delay_ms = 0;
int has_transp = 0;
uint8_t transp_idx = 0;
uint32_t bg_color = 0xFF000000; // 默认黑色不透明

if (decoder->has_gct) {
GifColor *bg = &decoder->global_ct[decoder->bg_color_idx];
bg_color = 0xFF000000 | (bg->r << 16) | (bg->g << 8) | bg->b;
}

while (pos < len) {
uint8_t block_type = data[pos];

if (block_type == 0x3B) { // Trailer
break;
} else if (block_type == 0x2C) { // Image Descriptor
pos++; // 跳过 0x2C
GifFrame *frame = &decoder->frames[decoder->frame_count];

// 解析 Image Descriptor
frame->left = data[pos] | (data[pos+1] << 8); pos += 2;
frame->top = data[pos] | (data[pos+1] << 8); pos += 2;
frame->width = data[pos] | (data[pos+1] << 8); pos += 2;
frame->height = data[pos] | (data[pos+1] << 8); pos += 2;

uint8_t img_packed = data[pos++];
int has_lct = (img_packed >> 7) & 1;
int interlaced = (img_packed >> 6) & 1;
int lct_size = (1 << ((img_packed & 0x07) + 1));

// 局部颜色表
GifColor *color_table = decoder->global_ct;
int ct_size = decoder->gct_size;

if (has_lct) {
GifColor *lct = malloc(lct_size * sizeof(GifColor));
for (int i = 0; i < lct_size; i++) {
lct[i].r = data[pos++];
lct[i].g = data[pos++];
lct[i].b = data[pos++];
}
color_table = lct;
ct_size = lct_size;
}

// LZW Min Code Size
uint8_t min_code_size = data[pos++];

// 读取 LZW 子块
uint8_t lzw_stream[65536];
size_t lzw_len = read_lzw_sub_blocks(
(uint8_t *)(data + pos), len - pos, lzw_stream, sizeof(lzw_stream));

// LZW 解码
size_t idx_pixel_count = frame->width * frame->height;
uint8_t *idx_pixels = malloc(idx_pixel_count);
int decoded = lzw_decode(lzw_stream, lzw_len, idx_pixels,
idx_pixel_count, min_code_size);

// 如果交错,反交错
if (interlaced) {
uint8_t *deint = malloc(idx_pixel_count);
deinterlace_pixels(deint, idx_pixels, frame->width, frame->height);
free(idx_pixels);
idx_pixels = deint;
}

// 将索引像素转为 ARGB
frame->pixels = malloc(frame->width * frame->height * sizeof(uint32_t));
for (int i = 0; i < frame->width * frame->height; i++) {
uint8_t idx = idx_pixels[i];
if (has_transp && idx == transp_idx) {
frame->pixels[i] = 0x00000000; // 透明
} else if (idx < ct_size) {
GifColor *c = &color_table[idx];
frame->pixels[i] = 0xFF000000 |
(c->r << 16) | (c->g << 8) | c->b;
} else {
frame->pixels[i] = bg_color;
}
}

frame->delay_ms = delay_ms;
frame->disposal = disposal;
frame->has_transparency = has_transp;
frame->transparent_idx = transp_idx;

free(idx_pixels);
if (has_lct) free(color_table);

// 应用处置方式到画布
if (decoder->frame_count > 0) {
GifFrame *prev = &decoder->frames[decoder->frame_count - 1];
apply_disposal(canvas, prev_canvas,
decoder->canvas_width, decoder->canvas_height,
prev, frame, prev->disposal, bg_color);
}

decoder->frame_count++;

// 重置帧级状态
disposal = 0;
delay_ms = 0;
has_transp = 0;
transp_idx = 0;

} else if (block_type == 0x21) { // Extension
uint8_t label = data[pos + 1];

if (label == 0xF9) { // Graphic Control Extension
pos += 2; // 跳过 0x21 0xF9
/* uint8_t block_size = */ data[pos++]; // 应为 0x04
uint8_t gce_packed = data[pos++];
disposal = (gce_packed >> 2) & 0x07;
has_transp = (gce_packed) & 0x01;
delay_ms = (data[pos] | (data[pos+1] << 8)) * 10;
pos += 2;
transp_idx = data[pos++];
/* uint8_t terminator = */ data[pos++]; // 0x00

} else if (label == 0xFF) { // Application Extension
parse_netscape_ext(data, len, &pos, &decoder->loop_count);
} else {
skip_extension(data, len, &pos);
}
} else {
pos++; // 未知字节,尝试跳过
if (pos >= len) break;
}
}

free(canvas);
free(prev_canvas);
return decoder->frame_count;
}

9.3 渲染到 Canvas 与动画控制

void render_frame(GifDecoder *decoder, int frame_idx,
uint32_t *canvas, int canvas_width) {
GifFrame *frame = &decoder->frames[frame_idx];
for (int y = 0; y < frame->height; y++) {
uint32_t *dst = canvas + (frame->top + y) * canvas_width + frame->left;
uint32_t *src = frame->pixels + y * frame->width;
for (int x = 0; x < frame->width; x++) {
if ((src[x] & 0xFF000000) != 0) { // 不透明像素才覆盖
dst[x] = src[x];
}
}
}
}

十、JNI 桥接与 Kotlin/Java 调用

10.1 JNI 接口设计

// gif_jni.c
#include <jni.h>
#include <android/bitmap.h>

static jlong native_init(JNIEnv *env, jobject thiz,
jobject byte_buffer, jint length) {
uint8_t *data = (uint8_t *)(*env)->GetDirectBufferAddress(env, byte_buffer);
GifDecoder *decoder = malloc(sizeof(GifDecoder));
int frames = gif_parse(decoder, data, length);
if (frames < 0) { free(decoder); return 0; }
return (jlong)(uintptr_t)decoder;
}

static jobject native_get_frame(JNIEnv *env, jobject thiz,
jlong handle, jint frame_idx,
jobject bitmap) {
GifDecoder *decoder = (GifDecoder *)(uintptr_t)handle;
if (frame_idx >= decoder->frame_count) return NULL;

GifFrame *frame = &decoder->frames[frame_idx];
uint32_t *pixels;

AndroidBitmap_lockPixels(env, bitmap, (void **)&pixels);
memcpy(pixels, frame->pixels,
frame->width * frame->height * sizeof(uint32_t));
AndroidBitmap_unlockPixels(env, bitmap);

// 创建并返回包含帧信息的 Java 对象
jclass frame_info_cls = (*env)->FindClass(env, "com/example/gif/GifFrameInfo");
jmethodID ctor = (*env)->GetMethodID(env, frame_info_cls, "<init>", "(III)V");
return (*env)->NewObject(env, frame_info_cls, ctor,
frame->delay_ms, frame->width, frame->height);
}

static jint native_get_frame_count(JNIEnv *env, jobject thiz, jlong handle) {
GifDecoder *decoder = (GifDecoder *)(uintptr_t)handle;
return decoder->frame_count;
}

static jint native_get_loop_count(JNIEnv *env, jobject thiz, jlong handle) {
GifDecoder *decoder = (GifDecoder *)(uintptr_t)handle;
return decoder->loop_count;
}

static void native_release(JNIEnv *env, jobject thiz, jlong handle) {
GifDecoder *decoder = (GifDecoder *)(uintptr_t)handle;
if (decoder) {
for (int i = 0; i < decoder->frame_count; i++) {
free(decoder->frames[i].pixels);
}
free(decoder->frames);
free(decoder->global_ct);
free(decoder);
}
}

static JNINativeMethod methods[] = {
{"nativeInit", "(Ljava/nio/ByteBuffer;I)J", (void *)native_init},
{"nativeGetFrame","(JILandroid/graphics/Bitmap;)Lcom/example/gif/GifFrameInfo;",
(void *)native_get_frame},
{"nativeGetFrameCount", "(J)I", (void *)native_get_frame_count},
{"nativeGetLoopCount", "(J)I", (void *)native_get_loop_count},
{"nativeRelease", "(J)V", (void *)native_release},
};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env;
(*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6);
jclass klass = (*env)->FindClass(env, "com/example/gif/GifDecoder");
(*env)->RegisterNatives(env, klass, methods, sizeof(methods)/sizeof(methods[0]));
return JNI_VERSION_1_6;
}

10.2 Kotlin 封装层

class GifDecoder {

private var nativeHandle: Long = 0
var frameCount: Int = 0
private set
var loopCount: Int = 0
private set
var canvasWidth: Int = 0
private set
var canvasHeight: Int = 0
private set

fun load(inputStream: InputStream): Boolean {
val bytes = inputStream.readBytes()
val buffer = ByteBuffer.allocateDirect(bytes.size)
buffer.put(bytes)
buffer.flip()

nativeHandle = nativeInit(buffer, bytes.size)
if (nativeHandle == 0L) return false
frameCount = nativeGetFrameCount(nativeHandle)
loopCount = nativeGetLoopCount(nativeHandle)
return true
}

fun getFrame(
frameIndex: Int,
bitmap: Bitmap
): GifFrameInfo? {
return nativeGetFrame(nativeHandle, frameIndex, bitmap)
}

fun release() {
if (nativeHandle != 0L) {
nativeRelease(nativeHandle)
nativeHandle = 0L
}
}

data class GifFrameInfo(
val delayMs: Int,
val width: Int,
val height: Int
)

// Native methods
private external fun nativeInit(buffer: ByteBuffer, length: Int): Long
private external fun nativeGetFrame(
handle: Long, frameIndex: Int, bitmap: Bitmap
): GifFrameInfo?
private external fun nativeGetFrameCount(handle: Long): Int
private external fun nativeGetLoopCount(handle: Long): Int
private external fun nativeRelease(handle: Long)
}

10.3 动画播放器

class GifPlayer(
private val imageView: ImageView,
private val decoder: GifDecoder
) {
private var currentFrame = 0
private var isRunning = false
private val handler = Handler(Looper.getMainLooper())
private val canvasBitmap: Bitmap = Bitmap.createBitmap(
decoder.canvasWidth, decoder.canvasHeight, Bitmap.Config.ARGB_8888
)

private val frameRunnable = object : Runnable {
override fun run() {
if (!isRunning) return

val frameInfo = decoder.getFrame(currentFrame, canvasBitmap)
if (frameInfo != null) {
imageView.setImageBitmap(canvasBitmap)
handler.postDelayed(this, frameInfo.delayMs.toLong())
}

currentFrame++
if (currentFrame >= decoder.frameCount) {
if (decoder.loopCount == 0) {
currentFrame = 0 // 无限循环
} else {
// 有限循环逻辑...
stop()
}
}
}
}

fun start() {
isRunning = true
currentFrame = 0
handler.post(frameRunnable)
}

fun stop() {
isRunning = false
handler.removeCallbacks(frameRunnable)
}
}

十一、Android 系统方案对比

11.1 Movie 类(已废弃)

android.graphics.Movie 自 API 1 起存在,通过 Movie.decodeStream() / Movie.decodeByteArray() 解码 GIF。其问题:

  • 不支持透明通道:所有帧以不透明方式渲染,无法正确处理 alpha。
  • 无帧级控制:无法获取每帧的延迟时间、处置方式等信息。
  • 解码质量差:内部基于 Skia 的旧 GIF 解码器,对某些 GIF89a 扩展支持不全。
  • **API 28 起标记为 @Deprecated**:官方推荐迁移至 ImageDecoder
// [deprecated] Movie 用法
Movie movie = Movie.decodeStream(inputStream);
long start = SystemClock.uptimeMillis();
int duration = movie.duration();
// 必须手动通过 Canvas 逐帧绘制
movie.setTime((int)((SystemClock.uptimeMillis() - start) % duration));
movie.draw(canvas, 0, 0);

11.2 ImageDecoder(API 28+)

android.graphics.ImageDecoder 是现代的图像解码框架,支持动画 GIF/WebPHEIF渐进式 JPEG 等格式。

创建动画 Drawable

val source = ImageDecoder.createSource(contentResolver, uri)
val drawable = ImageDecoder.decodeDrawable(source) { decoder, info, _ ->
// 可在此进行后处理
decoder.setPostProcessor { canvas ->
// 后处理逻辑
PixelFormat.TRANSLUCENT
}
}
// 类型应为 AnimatedImageDrawable
(drawable as? AnimatedImageDrawable)?.start()

逐帧解码方案(与本文 C 解码器对标)

val source = ImageDecoder.createSource(file)
val drawable = ImageDecoder.decodeDrawable(source)

if (drawable is AnimatedImageDrawable) {
// 获取帧数、重复次数
val frameCount = drawable.repeatCount // 注意:这是重复次数,非总帧数
drawable.repeatCount = AnimationDrawable.INFINITE // 无限循环

// 注册帧更新回调
drawable.registerAnimationCallback(object : Animatable2.AnimationCallback() {
override fun onAnimationStart(drawable: Drawable?) { ... }
override fun onAnimationEnd(drawable: Drawable?) { ... }
})
drawable.start()
}

11.3 三种方案对比

特性 Movie (deprecated) ImageDecoder (API 28+) 本文 C 解码器
动画支持 有限(手动循环) 完整(AnimatedImageDrawable) 完整(自控循环)
透明通道 不支持 完整支持 完整支持
处置方式 不支持 完整支持 完整支持
帧级控制 回调模式 索引式直接访问
内存控制 不可控 系统管理 精细控制(复用 Bitmap)
交错解码 自动 自动 手动实现
多平台 Android only Android only 跨平台(纯 C)
性能 一般 优秀(硬件加速) 可优化(位运算 + 缓存)

十二、结语

本文从 GIF 二进制格式的每个字节出发,逐层剖析了 Header、Logical Screen Descriptor、Color Table、Image Descriptor、LZW 压缩、Graphic Control Extension 和 Application Extension,并给出了一个可直接在 Android NDK 环境下编译运行的完整 C 语言解码器实现。

关键要点回顾:

  1. LZW 解码器的正确实现是 GIF 解码的核心挑战,尤其要注意 KωKωK 特例和字典重置时机。
  2. Disposal Method 是动画帧合成的关键,处置方式 2(恢复背景色)和 3(恢复上一帧)的实现直接影响动画播放的正确性。
  3. 子块(Sub-block)结构在 LZW 数据、扩展数据中广泛使用,按块读取而非假设连续性是稳健解析的基础。
  4. 自实现解码器的价值在于精细的内存控制(帧间复用 Bitmap、控制解码缓冲区大小)和跨平台可移植性

理解 GIF 格式不仅是为了解析 GIF 本身——LZW 压缩算法同样应用于 TIFF 和 PDF(早期版本),而调色板索引、位流操作、字典编码等技巧在图像/视频编解码领域具有普适性。


参考规范

打赏
  • 微信
  • 支付宝

评论