专栏首页包子的书架利用Android系统源码中giflib实现播放gif文件
原创

利用Android系统源码中giflib实现播放gif文件

前言

目前市场上流行的图片框架都是可以很好的处理gif图片,像glide是通过Java层来处理gif的展示,但是Java层来处gif的展示,始终会存在OOM的风险。今天学习了一下Android系统源码中拓展源码的giflib加载gif。

探讨一下.jpeg

开始扯犊子

想要实现gif的一帧一帧的播放,必须要了解一下gif的大体结构,关于gif格式图片的详细解析 ,这边大体介绍一下。

gif的结构介绍

GIF是Compuserve公司开发的图形文件格式,一共有两个版本87a和89a,现在市场上大部分使用的都是89a的版本,89a版本相对87a多了数据扩展块,也正是我们实现需要用到的内容。下图:列出gif大体文件构造元素。

GIF文件结构图.png

由图我们可以看出:

GIF文件结构:文件头,数据流,结束器。

GIF文件内部是按块划分的,包括控制块( Control Block )和数据块(Data Sub-blocks)两种。

数据流包含:逻辑屏幕标识,全局颜色列表,符图像块,图形控制拓展,应用程序拓展等。

  • 思考 一个gif有很多帧的文件图片,每一帧图片的展示需要通过绘制图片的像素和每一个像素的颜色值,以及还需要知道每一帧到下一帧的展示的时间。
  • 设计 1、利用giflib的DGIFOpenFileName以文件流的形式加载gif文件,获取到gif的图片结构包括:图片的宽高,像素,颜色表,像素帧,gif中的图片帧数,每一帧数需要播放的时间(距离下一帧图像出现的时间)。 2、创建GIFBean的结构体,用来保存:第一帧到下一帧的计算每一帧的延迟时间,gif图片中总共的图片帧数,当前播放的是哪一帧。 3、通过绘制每一帧的像素来达到展示,利用每一帧延迟时间对下一帧的图像进行播放。 4、绘制中,需要遍历每一帧的图像的像素,并从gif中的颜色表中取值,然后对像素进行填色。
    gif内部结构草图.png
敲代码.jpg

Android系统源码的giflib介绍

个人这边以Android8.1的系统源码的giflib来讲解 \android-8.1.0_r1\external\giflib

  • GifFileType结构体
typedef int GifWord;
typedef struct GifFileType {
    GifWord SWidth, SHeight;         /* gif的逻辑宽高 */
    GifWord SColorResolution;        /*  gif文件需要生成的多少种颜色,最大不会超过255 */
    GifWord SBackGroundColor;        /* 画布的背景颜色 */
    GifByteType AspectByte;	     /*  用来计算宽高比 */
    ColorMapObject *SColorMap;       /* 颜色列表,如果局部颜色列表为空,这边表示的是全局颜色列表,否则是局部颜色列表 */
    int ImageCount;                  /*  gif的图像帧数 */
    GifImageDesc Image;              /* 当前帧图像 */
    SavedImage *SavedImages;         /*用来存储已经读取过得图像数据 */
    int ExtensionBlockCount;         /* 表示图像扩展数据块数量 */
    ExtensionBlock *ExtensionBlocks; /* 用来存储图像的数据块 */    
    int Error;			     /* 错误码 */
    void *UserData;                  /* 用来存储开发者的数据,类似view设置tag */
    void *Private;                   /* Don't mess with this! */
} GifFileType;
  • 涉及到的结构体
typedef unsigned char GifByteType;
typedef int GifWord;

typedef struct GifColorType {
    GifByteType Red, Green, Blue;   /* rgb 三种色 */
} GifColorType;

//表示颜色表的结构体,用来存储每一个像素的rgb颜色
typedef struct ColorMapObject {
    int ColorCount;   
    int BitsPerPixel;
    bool SortFlag;
    GifColorType *Colors;    /* on malloc(3) heap */
} ColorMapObject;
// 图像的基本参数,宽高,颜色表,gif存储的是顺序或者是交错的方式
typedef struct GifImageDesc {
    GifWord Left, Top, Width, Height;   /* Current image dimensions. */
    bool Interlace;                     /* Sequential/Interlaced lines. */
    ColorMapObject *ColorMap;           /* The local color map */
} GifImageDesc;

// 数据块: 字节数,字节表,块函数编码,表示是哪一种数据块(图形控制扩展,图形文本扩展等)
typedef struct ExtensionBlock {
    int ByteCount;
    GifByteType *Bytes; /* on malloc(3) heap */
    int Function;       /* The block function code */
#define CONTINUE_EXT_FUNC_CODE    0x00    /* continuation subblock */
#define COMMENT_EXT_FUNC_CODE     0xfe    /* 注释扩展*/
#define GRAPHICS_EXT_FUNC_CODE    0xf9    /* 图形控制扩展(89a版本才有的) */
#define PLAINTEXT_EXT_FUNC_CODE   0x01    /* 图形文本扩展*/
#define APPLICATION_EXT_FUNC_CODE 0xff    /* 应用程序扩展 */
} ExtensionBlock;

上面介绍的结构体,都是涉及到gif文件结构的一些信息。

撸起袖子开始搬砖

  • 首先加载gif文件图片,获取图片的基本信息 giflib有两种加载方式:以文件流方式打开gif文件和以文件句柄方式 这边直接采用文件流方式
const char * path = env->GetStringUTFChars(path_, NULL);
int error;
//以文件流来打开gif文件,读取gif文件信息,并且返回一个结构体
GifFileType *gifFileType = DGifOpenFileName(path, &error);
//将整个GIF读入内核,挂起它所有的状态信息 GifFileType指针
DGifSlurp(gifFileType);

创建一个我们自己的结构体用来存储:

  1. 每一帧的延迟时间
  2. 总的图像帧数
  3. 当前的播放帧位置
typedef struct GifBean{
    int current_frame;
    int total_frame;
    int *delays;    /* 每一帧的延迟时间都有可能不一样, 所以列表形式存储 */
} GifBean;
  • 接着计算每一帧的延迟时间(每一帧的延迟时间都有可能不一样)和存储所有的帧数
 //给自定义的结构体GifBean分配空间
    GifBean *gifBean = (GifBean*)(malloc(sizeof(GifBean)));
    //初始化,清空内存
    memset(gifBean, 0, sizeof(GifBean));
    //将我们创建的gifbean的数据指针传到GifFileType的用户数据指针变量
    gifFileType->UserData = gifBean;

    //给gif所有帧播放的时间,分配内存空间
    gifBean->delays = (int *)malloc(sizeof(int) * gifFileType->ImageCount);
    //清空初始化分配好的内存
    memset(gifBean->delays, 0, sizeof(int) * gifFileType->ImageCount);

    //存储gif文件一共有多少帧
    gifBean->total_frame = gifFileType->ImageCount;
    //定义拓展块结构体
    ExtensionBlock *extensionBlock;
    for (int i = 0; i < gifFileType->ImageCount; ++i) {
        // SaveImages变量用来存储已经读取过得图像数据。
        // 获取每一帧图像的资源
        SavedImage savedImage = gifFileType->SavedImages[i];
        // 遍历当前这一帧的图片中的拓展块的数据
        for (int j = 0; j < savedImage.ExtensionBlockCount; ++j) {
            //拿到图形拓展的内容
            if (savedImage.ExtensionBlocks[j].Function == GRAPHICS_EXT_FUNC_CODE) {
                extensionBlock = &savedImage.ExtensionBlocks[j];
                break;
            }
        }
        if (extensionBlock) {
            //拿到图形拓展块后,计算每一帧的播放时长
            int frame_delay = 10 * (extensionBlock->Bytes[2] << 8 | extensionBlock->Bytes[1]);
            LOGE("时间  %d   ",frame_delay);
            gifBean->delays[i] = frame_delay;
        }
    }
    LOGE("gif  长度大小    %d  ",gifFileType->ImageCount);
    env->ReleaseStringUTFChars(path_, path);
    //到这做了两件事:一是计算gif图片中,每一帧图片的播放时长,二是保持总共多少帧图片
    //返回结构体的首地址
    return (jlong) gifFileType;
  • 获取gif的宽高
JNIEXPORT jint JNICALL
Java_com_jason_ndk_gifdemo_GifHandler_getGifWidth(JNIEnv *env, jobject thiz, jlong gif_point) {
    GifFileType *gifFileType = (GifFileType *) gif_point;
    //返回gif的宽度
    return gifFileType->SWidth;
}

JNIEXPORT jint JNICALL
Java_com_jason_ndk_gifdemo_GifHandler_getGifHeight(JNIEnv *env, jobject thiz, jlong gif_point) {
    GifFileType *gifFileType = (GifFileType *) gif_point;
    //返回gif的高度
    return gifFileType->SHeight;
}
  • 绘制每一帧图像
JNIEXPORT jint JNICALL
Java_com_jason_ndk_gifdemo_GifHandler_updateFrame(JNIEnv *env, jobject thiz, jlong gif_point,
                                                  jobject bitmap) {
    GifFileType *gifFileType = (GifFileType *) gif_point;
    //拿出我们所保持在GifFileType的GIFBean数据
    GifBean *gifBean = (GifBean *)gifFileType->UserData;
    AndroidBitmapInfo info;
    //代表一幅图片的像素数组
    void *pixel;
    //获取bitmap的图片信息
    AndroidBitmap_getInfo(env, bitmap, &info);
    //锁定bitmap
    AndroidBitmap_lockPixels(env, bitmap, &pixel);
    //绘制每一帧
    drawFrame(gifFileType, gifBean, info, pixel);
    //播放完成之后,循环到下一帧
    gifBean->current_frame += 1;
    LOGE("当前帧  %d  ",gifBean->current_frame);
    if (gifBean->current_frame >= gifBean->total_frame-1) {
        //当前播放到最后一帧,重置为第一帧,开始重新播放
        gifBean->current_frame = 0;
        LOGE("重新过来  %d  ",gifBean->current_frame);
    }
    //接触锁定bitmap
    AndroidBitmap_unlockPixels(env, bitmap);
    return gifBean->delays[gifBean->current_frame];
}
  • 绘制每一帧的像素
void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo info, void *pixels) {
    //播放底层代码
    //拿到当前帧
    SavedImage savedImage = gifFileType->SavedImages[gifBean->current_frame];
    //
    GifImageDesc frameInfo = savedImage.ImageDesc;
    //整幅图片的首地址
    int* px = (int *)pixels;
    //每一行的首地址
    int *line;

    //其中一个像素的位置  不是指针  在颜色表中的索引
    int pointPixel;
    //gif的字节类型
    GifByteType  gifByteType;
    //gif图片的像素颜色类型
    GifColorType gifColorType;
    //获取gif图片的颜色表
    ColorMapObject* colorMapObject=frameInfo.ColorMap;
    //计算像素,对每一个像素进行颜色填充
    //px = px + info.stride * frameInfo.Top;   //会直接被向上去值,px是int,stride是char,位数不同
    px = (int *) ((char*)px + info.stride * frameInfo.Top);
    for (int y = frameInfo.Top; y < frameInfo.Top + frameInfo.Height; ++y) {
        //更新行数
        line = px;
        for (int x = frameInfo.Left; x < frameInfo.Left + frameInfo.Width; ++x) {
            pointPixel = (y - frameInfo.Top) * frameInfo.Width + (x - frameInfo.Left);
            //获取gif这个像素点的颜色类型,是red,green,blue中的一个,GifByteType 本身是无符号的字符 unsigned char
            gifByteType = savedImage.RasterBits[pointPixel];
            //获取到当前像素的颜色RGB的结构体
            gifColorType = colorMapObject->Colors[gifByteType];
            line[x] = argb(255,gifColorType.Red, gifColorType.Green, gifColorType.Blue);
        }
        //像素位置更新
        px = (int *) ((char*)px + info.stride);  //info.stride 表示是每行字节数的数量
    }
}
  • 在Java中的调用和显示
public void load(ImageView imageView, final String path) {
    imageViewWeakReference = new WeakReference<ImageView>(imageView);
    new Thread() {
      @Override
      public void run() {
        load(path);
      }
    }.start();
  }

  private void load(String path) {

    File file = new File(path);
    if(!file.exists()) {
      System.err.println("not found gif file");
      return;
    }

    GifHandler gifHandler = new GifHandler(file.getAbsolutePath());
    int width = gifHandler.getWidth();
    int height = gifHandler.getHeight();
    Bitmap  bitmap= Bitmap.createBitmap(width,height, Bitmap.Config.ARGB_8888);
      
    int nextFrame = gifHandler.updateFrame(bitmap);
    handler.sendEmptyMessageDelayed(1,nextFrame);
  }

  private Handler handler = new Handler(Looper.getMainLooper()){

    @Override
    public void handleMessage(Message msg) {
      ImageView gifIv = imageViewWeakReference.get();
      if(gifIv == null) {
        return;
      }
      int mNextFrame = gifHandler.updateFrame(bitmap);
      handler.sendEmptyMessageDelayed(1,mNextFrame);
      gifIv.setImageBitmap(bitmap);
    }
  };

最后运行就可以看到gif图片播放了。

活不过今晚了.gif

参考文献

https://blog.csdn.net/wzy198852/article/details/17266507

https://www.cnblogs.com/xiaoxiaoboke/archive/2012/02/13/2349770.html

https://www.liangzl.com/get-article-detail-163059.html

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • MFC的自定义控件之控件封装

    包子388321
  • 简单的通过demo了解C++的基础语法笔记

    许久未碰C++的相关内容,已经有些被大脑的程序执行Lru算法了,导致近期在做NDK开发的时候,各种操作卡顿,决心还是回忆整理一下相关的基础知识。

    包子388321
  • Android TextView处理html的图片和<a>标签事件

    https://gitee.com/adminfun/HTMLTextView https://blog.csdn.net/songmingzhan/arti...

    包子388321
  • 1349. Maximum Students Taking Exam(DP,状态压缩)

    Share Given a m * n matrix seats that represent seats distributions in a classr...

    ShenduCC
  • 连接远程数据库ORACLE11g,错误百出!

    首先,我已经提前在虚拟机上配置了windows2008+oracle11g,为什么用server2008呢?我没有别的,win10做虚拟机觉得不太好,win7镜...

    CN_Simo
  • CPUFreq驱动

    CPUFreq子系统位于 drivers/cpufreq目录下,负责进行运行过程中CPU频率和电压的动态调整,即DvFS( Dynamic Voltage Fr...

    233333
  • Apache CloudStack系统VM架构选择

    最近我和一些人讨论了为什么现在有一个32位或64位系统虚拟机和CloudStack 4.3 (一个云计算平台)的选项。我提供了一个答案,并且回复了一些邮件列表...

    用户1042889
  • 史上最严重数据车祸:100+车厂机密全曝光,通用丰田特斯拉统统中招

    100多家车厂,从通用汽车、菲亚特克莱斯勒、福特、丰田,大众到特斯拉,现在机密数据统统被供应商的共同服务器曝光。

    量子位
  • 从零开始搭建Java开发环境第二篇:如何在windows10里安装MySQL

    [外链图片转存失败(img-oesO8K09-1566652568838)(data:image/gif;base64,R0lGODlhAQABAPABAP//...

    黄小斜
  • 必看的数据库使用规范

    关于MySQL数据库规范,相信大家多少看过一些文档。本篇文章给大家详细分类总结了数据库相关规范,从库表命名设计规范讲起,到索引设计规范,后面又给出SQL编写方面...

    MySQL技术

扫码关注云+社区

领取腾讯云代金券