Android-JNI开发系列《十一》实践-利用Android C源码实现GIF图片的播放

人间观察

人往往都是多面性的,一个人的时候是一个样子,一群人的时候是另一个样子。

声明

此篇文章只为记录和学习JNI以及了解GIF的解码原理。借鉴了网上的有关gif文章介绍和代码。如果是自己学习,建议自己敲一遍jni的代码,不要眼高手低。

一些开源的gif解码项目

java版本glide的解码GIF或者c版本的
https://github.com/koral--/android-gif-drawable

本篇我们学习了解GIF的解码原理和熟悉jni即可。

利用Android 系统的源码来实现GIF的解码播放

Android 系统的gif的解码在/external/giflib目录下,源码如下
http://androidos.net.cn/android/9.0.0_r8/xref/external/giflib

Android 系统的源码支持gif的解码和生成gif文件,可以选择性的拷贝源文件和头文件,这里就直接全部拷贝了。

基本的实现流程:

  1. 在jni层中打开gif文件并加载得到gif的总播放帧数和每帧之间的延迟时间
  2. 在java层中创建ARGB_8888格式的bitmap(jni层是按照每像素4字节处理的)
  3. 在jni层获取步骤2中的bitmap对应的像素数据指针void**
  4. 解码gif的每帧的数据然后对步骤3中的像素数据指针进行赋值,像素格式还是abgr。这样java层调用imageView.setImageBitmap(bitmap);数据就是修改后的。
  5. 定时刷新执行步骤4即可,来达到循环播放gif

java代码:

package com.bj.gxz.gifjnidecode;

import android.graphics.Bitmap;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.widget.ImageView;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;

import java.io.File;

public class MainActivity extends AppCompatActivity {
    private GifJni gifJni;
    private ImageView imageView;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = findViewById(R.id.id_iv);

//        File file = new File(Environment.getExternalStorageDirectory(), "demo.gif");
        // java
//        Glide.with( this )
//                .load( file.getAbsolutePath() )
//                .into( imageView );

        // c实现,建议
        // https://github.com/koral--/android-gif-drawable
//        try {
//            GifDrawable gifFromPath = new GifDrawable(file.getAbsolutePath());
//            imageView.setImageDrawable(gifFromPath);
//        } catch (IOException e) {
//            e.printStackTrace();
//        }
    }

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

        @Override
        public void handleMessage(@NonNull Message msg) {
            super.handleMessage(msg);
            updateFrame();
        }
    };

    // Android源码中的,有些gif解码有些问题
    public void ndkGif(View view) {
        File file = new File(Environment.getExternalStorageDirectory(), "demo.gif");
        gifJni = new GifJni(file.getAbsolutePath());
        int width = gifJni.getWidth();
        int height = gifJni.getHeight();
        Log.d("GIF", "width:" + width);
        Log.d("GIF", "height:" + height);
        bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Log.d("GIF", "bitmap:" + bitmap.getConfig().name());
        updateFrame();
    }

    private void updateFrame() {
        long cost = System.currentTimeMillis();
        //下一帧的刷新时间
        int delayShowTime = gifJni.updateFrame(bitmap);
        cost = System.currentTimeMillis() - cost;

        int realDelay = (int) (delayShowTime - cost);
        // 真正的延时,需要减去更新一帧所消耗的时间
        realDelay = Math.max(realDelay, 0);

        Log.d("GIF", "realDelay:" + realDelay + ",cost:" + cost);
        imageView.setImageBitmap(bitmap);
        handler.sendEmptyMessageDelayed(0, realDelay);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacksAndMessages(null);
    }
}

jni层的代码

public class GifJni {

    static {
        System.loadLibrary("native-lib");
    }

    // Convenience for JNI access
    public long mNativePtr;


    public GifJni(String path) {
        this.mNativePtr = loadPath(path);
    }

    public int getWidth(){
        return getWidth(mNativePtr);
    }

    public int getHeight(){
        return getHeight(mNativePtr);
    }

    public int updateFrame(Bitmap bitmap){
        return updateFrame(mNativePtr,bitmap);
    }

    //通过路径加载gif图片(这里使用的是本地图片,源码中的gif加载是支持流的格式的)
    public native long loadPath(String path);

    //获取gif的宽
    public native int getWidth(long mNativePtr);

    //获取gif的高
    public native int getHeight(long mNativePtr);

    //每隔一段时间刷新一次,返回的int值表示下次刷新的时间间隔
    public native int updateFrame(long mNativePtr, Bitmap bitmap);
}

jni层的代码

#include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <android/log.h>
#include "gif_lib.h"

#define  LOG_TAG "GIF"
#define  LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,__VA_ARGS__)

#define  argb(a, r, g, b) ( ((a) & 0xff) << 24 ) | ( ((b) & 0xff) << 16 ) | ( ((g) & 0xff) << 8 ) | ((r) & 0xff)


typedef struct GifBean {
    // 当前帧
    int current_frame;
    // 总帧数
    int total_frame;
    // 延迟时间数组,长度不确定,根据gif帧数计算
    int *delays;
} GifBean;

void drawFrame(GifFileType *pType, GifBean *pBean, AndroidBitmapInfo info, void *pVoid);

extern "C"
JNIEXPORT jint JNICALL
Java_com_bj_gxz_gifjnidecode_GifJni_getWidth(JNIEnv *env, jobject thiz, jlong native_address_ptr) {
    GifFileType *gifFileType = reinterpret_cast<GifFileType *>(native_address_ptr);

    return gifFileType->SWidth;
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_bj_gxz_gifjnidecode_GifJni_getHeight(JNIEnv *env, jobject thiz, jlong native_address_ptr) {

    GifFileType *gifFileType = reinterpret_cast<GifFileType *>(native_address_ptr);

    return gifFileType->SHeight;
}


extern "C"
JNIEXPORT jlong JNICALL
Java_com_bj_gxz_gifjnidecode_GifJni_loadPath(JNIEnv *env, jobject thiz, jstring path) {

    const char *c_path = (char *) env->GetStringUTFChars(path, 0);

    int error;
    // 打开gif文件
    GifFileType *gifFileType = DGifOpenFileName(c_path, &error);
    DGifSlurp(gifFileType);

    // 自定义一个bean类,来存储当前播放帧,总帧数,播放延迟的数组,并把bean对象与gifFileType绑定
    GifBean *gifBean = (GifBean *) malloc(sizeof(GifBean));
    memset(gifBean, 0, sizeof(GifBean));
    //初始化当前帧和总帧数
    gifBean->current_frame = 0;
    gifBean->total_frame = gifFileType->ImageCount;
    gifBean->delays = (int *) (malloc(sizeof(int) * gifFileType->ImageCount));
    memset(gifBean->delays, 0, sizeof(int) * gifFileType->ImageCount);

    // 把自己定义的数据结构保存到UserData,便于其它方法的获取使用
    gifFileType->UserData = gifBean;

    ExtensionBlock *extensionBlock;
    //遍历每一帧
    for (int i = 0; i < gifFileType->ImageCount; i++) {
        //遍历每一帧中的扩展块(度娘Gif编码)
        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];
            }
        }
        //获取延迟时间,extensionBlock的第二,三个元素一起存放延迟时间低8位和高8位向左偏移8位,进行或运算
        //乘10因为编码的时间单位是1/100秒 乘10换算为毫秒
        if (extensionBlock) {
            int frame_delay = 10 * (extensionBlock->Bytes[1] | extensionBlock->Bytes[2] << 8);
            gifBean->delays[i] = frame_delay;
            // LOGD("delays[%d]=%d", i, frame_delay);
        }
    }
    env->ReleaseStringUTFChars(path, c_path);
    return reinterpret_cast<jlong>(gifFileType);
}


// 直接修改bitmap的像素
void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo info, void *pixels) {

    // 先拿到当前帧
    SavedImage savedImage = gifFileType->SavedImages[gifBean->current_frame];


    GifImageDesc imageDesc = savedImage.ImageDesc;

    // 获取color map
    //字典,存放的是gif压缩rgb数据
    ColorMapObject *colorMap = imageDesc.ColorMap;
    //部分图片某些帧的ColorMapObject取到为null
    if (colorMap == NULL) {
        colorMap = gifFileType->SColorMap;
    }

    GifByteType gifByteType;
    int pointPixels;
    // bitmap的像素的指针
    int *px = (int *) pixels;
    // 在jni c层中真实存储是 A B G R
    int bitmapLineStart;    // bitmap每一行的首地址
    for (int y = imageDesc.Top; y < imageDesc.Top + imageDesc.Height; y++) {
        // 更新行的首地址
        bitmapLineStart = y * info.width;
        for (int x = imageDesc.Left; x < imageDesc.Left + imageDesc.Width; x++) {
            // 拿到一个坐标的位置索引 --> 数据
            pointPixels = (y - imageDesc.Top) * imageDesc.Width + (x - imageDesc.Left);
            // 解压 gif中为了节省内存rgb采用lzw压缩,所以取rgb信息需要解压
            // 通过index拿到的是一个压缩数据
            gifByteType = savedImage.RasterBits[pointPixels];
            // 拿到真正的rgb
            GifColorType gifColorType = colorMap->Colors[gifByteType];
            // 转成一个int值,并赋值给对应的像素点
            px[bitmapLineStart + x] =
                    argb(255, gifColorType.Red, gifColorType.Green, gifColorType.Blue);

        }

    }
}


extern "C"
JNIEXPORT jint JNICALL
Java_com_bj_gxz_gifjnidecode_GifJni_updateFrame(JNIEnv *env, jobject thiz, jlong native_address_ptr,
                                                jobject bitmap) {

    GifFileType *gifFileType = reinterpret_cast<GifFileType *>(native_address_ptr);
    GifBean *gifBean = (GifBean *) gifFileType->UserData;

    AndroidBitmapInfo info;
    AndroidBitmap_getInfo(env, bitmap, &info);

    void *addrPtr;
    AndroidBitmap_lockPixels(env, bitmap, &addrPtr);

    drawFrame(gifFileType, gifBean, info, addrPtr);
    // 循环播放
    gifBean->current_frame += 1;
    if (gifBean->current_frame >= gifBean->total_frame - 1) {
        gifBean->current_frame = 0;
    }

    AndroidBitmap_unlockPixels(env, bitmap);

    // 把下一帧的延迟时间返回上去
    return gifBean->delays[gifBean->current_frame];
}

效果图

录屏的原因,效果显得不异常,真实正常。

demo.gif

源码

https://github.com/ta893115871/GifJniDecode

注意下读写sd卡的权限

参考网上的有关文章

https://blog.csdn.net/poisx/article/details/79122506

//www.greatytc.com/p/73cf5eacda00

//www.greatytc.com/p/c0fea0e32817

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352