前言:阅读优秀的源码可以大大提高我们的开发水平,遂开个新坑 记录优秀源码(Android 源代码、各种开源库等等)的分析和解读,学习别人是怎样实现某个功能的。本期我们的主角是 GIF 的解码,我们将从 GIF 解码的源码 GifDecoder 入手,分析其实现的原理和过程,希望能帮到大家~( GifDecoder 源码(博主已对源码里面各方法及参数进行了注释,请放心食用 ~)链接已在上方贴出来了,该源码参考了 Glide 开源库 解析 GIF 部分的代码,但由于是很久之前看到的,具体出处已无从考证,有知道的小伙伴可以留言告诉我)
目录
- GIF 结构简述
- GifDecoder 的初始化
- 判断传入文件格式
- 读取 GIF 大小、颜色深度等全局属性
- 提取各帧图片
相关博文链接
在分析源码之前,我们得先对 GIF 图片的构成有一个初步的了解(详细解析请看上方链接),见下图
图中加粗部分既是保存我们所需要提取图片的地方(一帧图像对应一个图像块)。虽然我们知道了存储每一帧图像信息的位置,但我们不能直接从中取出图片,因为在计算机中,所有的文件都是以二进制的形式存储的,而 Java 读取文件需要按顺序一个一个字节地读。因此 GIF 的解码过程,实际上就是从文件头 (File Header) 开始,按顺序遍历每一个字节,当读到我们需要的信息(图像数据)时,就将其提取出来。下面我们就开始分析 GifDecoder 是如何实现 GIF 解码的
先来看看 GifDecoder 的初始化和使用示例,代码如下
- try {
- InputStream is = getContentResolver().openInputStream(uri);
- GifDecoder gifDecoder = new GifDecoder();
- int code = gifDecoder.read(is);
- if (code == GifDecoder.STATUS_OK) {//解码成功
- GifDecoder.GifFrame[] frameList = gifDecoder.getFrames();
- } else if (code == gifDecoder.STATUS_FORMAT_ERROR) {//图片格式不是GIF
- } else {//图片读取失败
- }
- }catch (FileNotFoundException e){
- e.printStackTrace();
- }
其中参数 uri 为 GIF 图片的 Uri 路径,frameList 为解码的结果,即 GIF 图片中各帧的集合, 里面包括各帧静态图 Bitmap 和延迟时间。GifFrame 是保存各帧的对象,具体实现和内部属性如下
- /**
- * 各帧对象
- */
- public static class GifFrame {
- public Bitmap image;//静态图Bitmap
- public int delay;//图像延迟时间
- public
- GifFrame
- (Bitmap im, int del)
- {
- image = im;
- delay = del;
- }
- }
GifDecoder 定义了三种解码状态
- public static final int STATUS_OK = 0;//解码成功
- public static final int STATUS_FORMAT_ERROR = 1;//图片格式错误
- public static final int STATUS_OPEN_ERROR = 2;//打开图片失败
从 GifDecoder 的使用示例中,我们可以看到 GifDecoder 解码 GIF 图片的入口为 read(InputStream is) 方法,具体实现如下
- protected int status;//解码状态
- protected Vector<GifFrame> frames;//存放各帧对象的数组
- protected int frameCount;//帧数
- protected int[] gct; //全局颜色列表
- protected int[] lct; //局部颜色列表
- /**
- * 解码入口,读取GIF图片输入流
- * @param is
- * @return
- */
- public
- int
- read
- (InputStream is)
- {
- init();
- if (is != null) {
- in = is;
- readHeader();
- if (!err()) {
- readContents();
- if (frameCount < 0) {
- status = STATUS_FORMAT_ERROR;
- }
- }
- } else {
- status = STATUS_OPEN_ERROR;
- }
- try {
- is.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
- return status;
- }
- /**
- * 初始化参数
- */
- protected
- void
- init
- ()
- {
- status = STATUS_OK;
- frameCount = 0;
- frames = new Vector<GifFrame>();
- gct = null;
- lct = null;
- }
- /**
- * 判断当前解码过程是否出错
- * @return
- */
- protected
- boolean
- err
- ()
- {
- return status != STATUS_OK;
- }
可以看到 read(InputStream is) 方法中体现了完整的解码流程以及状态判断,其调用的 readHeader() 和 readContents() 即为具体的 GIF 内部数据读取方法。下一节我们将深入 readHeader() 方法看看 GifDecoder 是如何处理 GIF 文件头的
解码之前肯定要先判断解码的对象是否为 GIF 图片,readHeader() 中就实现了此判断过程,判断文件格式的代码部分如下
- /**
- * 读取GIF 文件头、逻辑屏幕标识符、全局颜色列表
- */
- protected
- void
- readHeader
- ()
- {
- //根据文件头判断是否GIF图片
- String id = "";
- for (int i = 0; i < 6; i++) {
- id += (char) read();
- }
- if (!id.toUpperCase().startsWith("GIF")) {
- status = STATUS_FORMAT_ERROR;
- return;
- }
- //解析GIF逻辑屏幕标识符和全局颜色列表
- ...
- }
- /**
- * 按顺序一个一个读取输入流字节,失败则设置读取失败状态码
- * @return
- */
- protected
- int
- read
- ()
- {
- int curByte = 0;
- try {
- curByte = in.read();
- } catch (Exception e) {
- status = STATUS_FORMAT_ERROR;
- }
- return curByte;
- }
怎么理解这段代码呢?前文我们提到文件头 (File Header) 中包含了 GIF 的文件署名和版本号,共占 6 个字节(见下图),其中前 3 个字节存放的是 GIF 的文件署名,即 G、I、F 三个字符,那么这段代码就很好理解了,就是根据读取出来的文件头字符串开头是否为 G、I 、F 来判断此文件格式符不符合要求
readHeader 方法中还有一部分代码,如下
- protected boolean gctFlag;//是否使用了全局颜色列表
- protected int bgIndex; //背景颜色索引
- protected int gctSize; //全局颜色列表大小
- protected int bgColor; //背景颜色
- protected
- void
- readHeader
- ()
- {
- //根据文件头判断是否GIF图片
- ...
- //读取GIF逻辑屏幕标识符
- readLSD();
- //读取全局颜色列表
- if (gctFlag && !err()) {
- gct = readColorTable(gctSize);
- bgColor = gct[bgIndex];//根据索引在全局颜色列表拿到背景颜色
- }
- }
其对应的正是 GIF 数据流 (GIF Data Stream) 的前两部分逻辑屏幕标识符 (Logical Screen Descriptor) 与全局颜色列表 (Global Color Table) 的解析,也就是说 readHeader()完成了读取 GIF 图像数据前所有全局属性、配置信息的读取与解析。接下来我们先看 readLSD()方法是如何解析逻辑屏幕标识符(Logical Screen Descriptor)(见下图)的
- protected int width;//完整的GIF图像宽度
- protected int height;//完整的GIF图像高度
- protected int pixelAspect; //像素宽高比(Pixel Aspect Radio)
- /**
- * 读取逻辑屏幕标识符(Logical Screen Descriptor)与全局颜色列表(Global Color Table)
- */
- protected
- void
- readLSD
- ()
- {
- //获取GIF图像宽高
- width = readShort();
- height = readShort();
- /**
- * 解析全局颜色列表(Global Color Table)的配置信息
- * 配置信息占一个字节,具体各Bit存放的数据如下
- * 7 6 5 4 3 2 1 0 BIT
- * | m | cr | s | pixel |
- */
- int packed = read();
- gctFlag = (packed & 0x80) != 0;//判断是否有全局颜色列表(m,0x80在计算机内部表示为1000 0000)
- gctSize = 2 << (packed & 7);//读取全局颜色列表大小(pixel)
- //读取背景颜色索引和像素宽高比(Pixel Aspect Radio)
- bgIndex = read();
- pixelAspect = read();
- }
- /**
- * 读取两个字节的数据
- * @return
- */
- protected
- int
- readShort
- ()
- {
- return read() | (read() << 8);
- }
根据 readLSD()的读取结果,我们知道了此 GIF 图像中是否含有全局颜色列表 (Global Color Table)(见下图),如果有,就调用 readColorTable(int ncolors) 方法获取全局颜色列表
- /**
- * 读取颜色列表
- * @param ncolors 列表大小,即颜色数量
- * @return
- */
- protected int[] readColorTable(int ncolors) {
- int nbytes = 3 * ncolors; //一个颜色占3个字节(r g b 各占1字节),因此占用空间为 颜色数量*3 字节
- int[] tab = null;
- byte[] c = new byte[nbytes];
- int n = 0;
- try {
- n = in.read(c);
- } catch(Exception e) {
- e.printStackTrace();
- }
- if (n < nbytes) {
- status = STATUS_FORMAT_ERROR;
- } else { //开始解析颜色列表
- tab = new int[256]; //设置最大尺寸避免边界检查
- int i = 0;
- int j = 0;
- while (i < ncolors) {
- int r = ((int) c[j++]) & 0xff;
- int g = ((int) c[j++]) & 0xff;
- int b = ((int) c[j++]) & 0xff;
- tab[i++] = 0xff000000 | (r << 16) | (g << 8) | b;
- }
- }
- return tab;
- }
至此 readHeader 方法我们就分析完了,接下来分析 readContents 方法是如何提取 GIF 图像的各帧图片的
我们先直接观察 readContents 方法内部是如何运作的
- /**
- * 读取图像块内容
- */
- protected void readContents() {
- boolean done = false;
- while (! (done || err())) {
- int code = read();
- switch (code) {
- //图象标识符(Image Descriptor)开始
- case 0x2C:
- readImage();
- break;
- //扩展块开始
- case 0x21:
- //扩展块标识,固定值0x21
- code = read();
- switch (code) {
- case 0xf9:
- //图形控制扩展块标识(Graphic Control Label),固定值0xf9
- readGraphicControlExt();
- break;
- case 0xff:
- //应用程序扩展块标识(Application Extension Label),固定值0xFF
- readBlock();
- String app = "";
- for (int i = 0; i < 11; i++) {
- app += (char) block[i];
- }
- if (app.equals("NETSCAPE2.0")) {
- readNetscapeExt();
- } else {
- skip(); // don't care
- }
- break;
- default:
- //其他扩展都选择跳过
- skip();
- }
- break;
- case 0x3b:
- //标识GIF文件结束,固定值0x3B
- done = true;
- break;
- case 0x00:
- //可能会出现的坏字节,可根据需要在此处编写坏字节分析等相关内容
- break;
- default:
- status = STATUS_FORMAT_ERROR;
- }
- }
- }
readContents()的核心流程就是根据块的标识来判断当前解码的位置,调用相应的方法对数据块进行解码。如果 GIF 版本为 89a,则数据块中可能含有扩展块(可选)。其中图像延迟时间存放在图形控制扩展 (Graphic Control Extension) 中,因此我们重点分析如何读取图形控制扩展(Graphic Control Extension)(见下图),其他扩展块解码大家可以对照着代码注释和 GIF 结构的相关知识自行研究,这里就不多赘述了
解码图形控制扩展 (Graphic Control Extension) 的方法为 readGraphicControlExt(),有了上图对各字节的说明其代码也就很容易理解了,如下
- /**
- * 读取图形控制扩展块
- */
- protected
- void
- readGraphicControlExt
- ()
- {
- read();//按读取顺序,此处为块大小
- int packed = read();//读取处置方法、用户输入标志等
- dispose = (packed & 0x1c) >> 2; //从packed中解析出处置方法(Disposal Method)
- if (dispose == 0) {
- dispose = 1; //elect to keep old image if discretionary
- }
- transparency = (packed & 1) != 0;//从packed中解析出透明色标志
- delay = readShort() * 10;//读取延迟时间(毫秒)
- transIndex = read();//读取透明色索引
- read();//按读取顺序,此处为标识块终结(Block Terminator)
- }
GIF 中可能含有多个图像块,图像块包含图象标识符 (Image Descriptor)(见下图)、局部颜色列表 (Local Color Table)(根据局部颜色列表标志确定是否存在)以及基于颜色列表的图象数据 (Table-Based Image Data)
readContents() 方法中遍历了所有图像块,并调用 readImage 方法进行解码,代码及注释如下
- protected boolean lctFlag;//局部颜色列表标志(Local Color Table Flag)
- protected boolean interlace;//交织标志(Interlace Flag)
- protected int lctSize;//局部颜色列表大小(Size of Local Color Table)
- /**
- * 按顺序读取图像块数据:
- * 图象标识符(Image Descriptor)
- * 局部颜色列表(Local Color Table)(有的话)
- * 基于颜色列表的图象数据(Table-Based Image Data)
- */
- protected
- void
- readImage
- ()
- {
- /**
- * 开始读取图象标识符(Image Descriptor)
- */
- ix = readShort();//x方向偏移量
- iy = readShort();//y方向偏移量
- iw = readShort();//图像宽度
- ih = readShort();//图像高度
- int packed = read();
- lctFlag = (packed & 0x80) != 0;//局部颜色列表标志(Local Color Table Flag)
- interlace = (packed & 0x40) != 0;//交织标志(Interlace Flag)
- // 3 - sort flag
- // 4-5 - reserved
- lctSize = 2 << (packed & 7);//局部颜色列表大小(Size of Local Color Table)
- /**
- * 开始读取局部颜色列表(Local Color Table)
- */
- if (lctFlag) {
- lct = readColorTable(lctSize);//解码局部颜色列表
- act = lct;//若有局部颜色列表,则图象数据是基于局部颜色列表的
- } else {
- act = gct; //否则都以全局颜色列表为准
- if (bgIndex == transIndex) {
- bgColor = 0;
- }
- }
- int save = 0;
- if (transparency) {
- save = act[transIndex];//保存透明色索引位置原来的颜色
- act[transIndex] = 0;//根据索引位置设置透明颜色
- }
- if (act == null) {
- status = STATUS_FORMAT_ERROR;//若没有颜色列表可用,则解码出错
- }
- if (err()) {
- return;
- }
- /**
- * 开始解码图像数据
- */
- decodeImageData();
- skip();
- if (err()) {
- return;
- }
- frameCount++;
- image = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
- setPixels(); //将像素数据转换为图像Bitmap
- frames.addElement(new GifFrame(image, delay));//添加到帧图集合
- // list
- if (transparency) {
- act[transIndex] = save;//重置回原来的颜色
- }
- resetFrame();
- }
readImage 方法中分三步进行:读取图象标识符 (Image Descriptor)、读取局部颜色列表(Local Color Table) 和解码图像数据。其中图像数据是如何解码并转换成 Bitmap 图像因为太复杂这里就不详细展开描述了,以后可能会专门写个番外篇进行分析,当然小伙伴们也可以自行阅读分析这部分源码:decodeImageData()、setPixels()
至此 GifDecoder 就基本分析完了,如果有讲解不到位的地方欢迎大家留言指正。如果大家看了感觉还不错麻烦点个赞,你们的支持是我最大的动力~
来源: https://juejin.im/post/5a31d40b5188256de205e554