前言 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(文件尾)
2.1 文件头(6 字节) typedef struct __attribute__ ((packed )) { char signature[3 ]; char version[3 ]; } 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; } 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))
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;
例如 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; uint16_t left; uint16_t top; 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_size 或 lzw_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);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; size_t size; uint32_t bit_pos; uint32_t bit_buf; int bit_count; } BitReader; 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; } 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 ; 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; 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) { 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; 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 { 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]; } } 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 子块结束
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 : case 1 : break ; case 2 : 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 : 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)。
int delay_ms = delay_time * 10 ;
七、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 ; } *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 ; }
8.1 Plain Text Extension(纯文本扩展) 允许在 GIF 图像上叠加栅格化文本 。标签 0x01。实践中极少使用,多数解码器仅跳过而不渲染。
0x21 0x01 [块大小 0x0C] [左侧 2B] [顶部 2B] [宽 2B] [高 2B] [字符宽 1B] [字符高 1B] [文本色 1B] [背景色 1B] [文本子块...] 0x00
标签 0xFE。包含 ASCII 文本注释子块。不影响图像渲染,解码器可安全跳过。
8.3 通用的扩展跳过逻辑 void skip_extension (const uint8_t *data, size_t len, size_t *offset) { *offset += 2 ; 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; int left, top; int width, height; int delay_ms; int disposal; int has_transparency; uint8_t transparent_idx; } GifFrame; typedef struct { int version; 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; 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)); if (pos + 6 > len) return -1 ; decoder->version = validate_header(data, len); if (decoder->version < 0 ) { decoder->error = decoder->version; return -1 ; } pos = 6 ; 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++]; data[pos++]; decoder->has_gct = GIF_HAS_GCT(packed); decoder->gct_size = GIF_GCT_SIZE(packed); 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++]; } } 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 ; 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 ) { break ; } else if (block_type == 0x2C ) { pos++; GifFrame *frame = &decoder->frames[decoder->frame_count]; 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; } uint8_t min_code_size = data[pos++]; uint8_t lzw_stream[65536 ]; size_t lzw_len = read_lzw_sub_blocks( (uint8_t *)(data + pos), len - pos, lzw_stream, sizeof (lzw_stream)); 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; } 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 ) { uint8_t label = data[pos + 1 ]; if (label == 0xF9 ) { pos += 2 ; data[pos++]; 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++]; data[pos++]; } else if (label == 0xFF ) { 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 接口设计 #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); 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 ) 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。
Movie movie = Movie.decodeStream(inputStream);long start = SystemClock.uptimeMillis();int duration = movie.duration();movie.setTime((int )((SystemClock.uptimeMillis() - start) % duration)); movie.draw(canvas, 0 , 0 );
11.2 ImageDecoder(API 28+) android.graphics.ImageDecoder 是现代的图像解码框架,支持动画 GIF/WebP 、HEIF 、渐进式 JPEG 等格式。
创建动画 Drawable :
val source = ImageDecoder.createSource(contentResolver, uri)val drawable = ImageDecoder.decodeDrawable(source) { decoder, info, _ -> decoder.setPostProcessor { canvas -> PixelFormat.TRANSLUCENT } } (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 语言解码器实现。
关键要点回顾:
LZW 解码器的正确实现 是 GIF 解码的核心挑战,尤其要注意 KωKωK 特例和字典重置时机。
Disposal Method 是动画帧合成的关键,处置方式 2(恢复背景色)和 3(恢复上一帧)的实现直接影响动画播放的正确性。
子块(Sub-block)结构 在 LZW 数据、扩展数据中广泛使用,按块读取而非假设连续性是稳健解析的基础。
自实现解码器的价值在于精细的内存控制 (帧间复用 Bitmap、控制解码缓冲区大小)和跨平台可移植性 。
理解 GIF 格式不仅是为了解析 GIF 本身——LZW 压缩算法同样应用于 TIFF 和 PDF(早期版本),而调色板索引、位流操作、字典编码等技巧在图像/视频编解码领域具有普适性。
参考规范