版权声明:本文为博主原创文章,未经博主允许不得转载
系列博客:
源码:
大家要是看到有错误的地方或者有啥好的建议,欢迎留言评论
前言:阅读优秀的源码可以大大提高我们的开发水平,遂开个新坑 记录优秀源码(Android源代码、各种开源库等等)的分析和解读,学习别人是怎样实现某个功能的。本期我们的主角是 GIF的解码,我们将从GIF解码的源码 GifDecoder入手,分析其实现的原理和过程,希望能帮到大家~( GifDecoder源码(博主已对源码里面各方法及参数进行了注释,请放心食用 ~)链接已在上方贴出来了,该源码参考了解析GIF部分的代码,但由于是很久之前看到的,具体出处已无从考证,有知道的小伙伴可以留言告诉我)
目录
- GIF结构简述
- GifDecoder的初始化
- 判断传入文件格式
- 读取GIF大小、颜色深度等全局属性
- 提取各帧图片
GIF结构简述
相关博文链接
在分析源码之前,我们得先对GIF图片的构成有一个初步的了解(详细解析请看上方链接),见下图
图中加粗部分既是保存我们所需要提取图片的地方(一帧图像对应一个图像块)。虽然我们知道了存储每一帧图像信息的位置,但我们不能直接从中取出图片,因为在计算机中,所有的文件都是以二进制的形式存储的,而Java读取文件需要按顺序一个一个字节地读。因此GIF的解码过程,实际上就是从文件头(File Header)开始,按顺序遍历每一个字节,当读到我们需要的信息(图像数据)时,就将其提取出来。下面我们就开始分析GifDecoder是如何实现GIF解码的
GifDecoder的初始化
先来看看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 Vectorframes;//存放各帧对象的数组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 (); 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来判断此文件格式符不符合要求
读取GIF大小、颜色深度等全局属性
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就基本分析完了,如果有讲解不到位的地方欢迎大家留言指正。如果大家看了感觉还不错麻烦点个赞,你们的支持是我最大的动力~