NDK加载动图(二)之代码解析

前言

上一篇我们讲完了gif动图格式,这篇文章我们将以代码的形式实现gif图片在手机屏幕上加载。

新建一个NDK项目,配置相关库、CMakeLists。


添加如下几个库到cpp文件夹中。

我们在代码中新建一个GifNdkDecoder类,并且添加如下方法:

public class GifNdkDecoder {
    private long gifPointer;

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

    public static native int getWidth(long gifPointer);

    public static native int getHeight(long gifPointer);

    public static native long loadGif(String path);

    public static native int updateFrame(Bitmap bitmap, long gifPointer);

    public GifNdkDecoder(long gifPoint) {
        this.gifPointer = gifPoint;
    }

    public static GifNdkDecoder load(String path) {
        long gifHander = loadGif(path);
        GifNdkDecoder gifNdkDecoder= new GifNdkDecoder(gifHander);
        return gifNdkDecoder;
    }

    public long getGifPointer() {
        return gifPointer;
    }
}

在这个类中,我们新增了四个native方法,分别对应获取宽高,加载gif图进内存,更新gif帧的方法。
然后我们看在native-lib.cpp的实现:

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

#define  LOG_TAG   "gif"
//#define  LOGE(...)  __android_log_print(ANDROID_LOG_ERROR,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_frames;
    int *delays; // 延迟数组
};

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

extern "C"
JNIEXPORT jlong JNICALL
Java_com_gif_GifNdkDecoder_loadGif(JNIEnv *env, jclass clazz, jstring path_) {
    const char *path = env->GetStringUTFChars(path_, 0);
    int error;
    // 保存GIF信息结构体
    GifFileType *gifFileType = DGifOpenFileName(path, &error);
    // gif初始化
    DGifSlurp(gifFileType);

    // 分配内存
    GifBean *gifBean = static_cast<GifBean *>(malloc(sizeof(GifBean)));
    memset(gifBean, 0, sizeof(GifBean));
    gifBean->delays = static_cast<int *>(malloc(sizeof(int) * gifFileType->ImageCount));
    memset(gifBean->delays, 0, sizeof(int) * gifFileType->ImageCount);
    ExtensionBlock *ext;

    // 复制给GifBean
    for (int i = 0; i < gifFileType->ImageCount; ++i) {
        SavedImage frame = gifFileType->SavedImages[i];
        for (int j = 0; i < frame.ExtensionBlockCount; ++j) {
            if (frame.ExtensionBlocks[j].Function == GRAPHICS_EXT_FUNC_CODE) {
                ext = &frame.ExtensionBlocks[j];
                break;
            }
        }
        if (ext) {
            // 拿到延迟时间
            // 小端模式存储,舍弃头部两个固定数据
            int frame_delay = (ext->Bytes[2] << 8 | ext->Bytes[1]) * 10;
            gifBean->delays[i] = frame_delay;
        }
    }

    gifBean->total_frames = gifFileType->ImageCount;

    // 方便后面取GifBean里面的信息
    gifFileType->UserData = gifBean;

    env->ReleaseStringUTFChars(path_, path);

    return (jlong) gifFileType;
}

void drawFrame(GifFileType *gifFileType, GifBean *gifBean, AndroidBitmapInfo info, void *pixels) {
    SavedImage savedImage = gifFileType->SavedImages[gifBean->current_frame];
    // 当前帧的图像信息
    GifImageDesc imageInfo = savedImage.ImageDesc;
    int *px = (int *) pixels;// 图像首地址
    ColorMapObject *colorMapObject = imageInfo.ColorMap;
    if (colorMapObject == NULL) {
        colorMapObject = gifFileType->SColorMap;
    }

    // 方向心偏移量
    px = reinterpret_cast<int *>((char *) px + info.stride * imageInfo.Top);
    int pointPixel;// 记录像素位置
    GifByteType gifByteType;
    GifColorType gifColorType;
    int *line;// 每一行的首地址
    for (int y = imageInfo.Top; y < imageInfo.Top + imageInfo.Height; ++y) {
        line = px;
        for (int x = imageInfo.Width; x < imageInfo.Width + imageInfo.Width; ++x) {
            pointPixel = (y - imageInfo.Top) * imageInfo.Width + (x - imageInfo.Left);
            // 拿到像素数据
            gifByteType = savedImage.RasterBits[pointPixel];// 实际上就是LZW算法中的索引
            // 给像素赋予颜色
            if (colorMapObject != NULL) {
                gifColorType = colorMapObject->Colors[gifByteType];
                line[x] = argb(255, gifColorType.Red, gifColorType.Green, gifColorType.Blue);
            }
        }
        px = reinterpret_cast<int *>((char *) px + info.stride);
    }
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_gif_GifNdkDecoder_getWidth(JNIEnv *env, jclass clazz, jlong gif_pointer) {
    GifFileType *gifFileType = (GifFileType *) (gif_pointer);
    return gifFileType->SWidth;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_gif_GifNdkDecoder_getHeight(JNIEnv *env, jclass clazz, jlong gif_pointer) {
    GifFileType *gifFileType = (GifFileType *) (gif_pointer);
    return gifFileType->SHeight;
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_gif_GifNdkDecoder_updateFrame(JNIEnv *env, jclass clazz, jobject bitmap,
                                       jlong gif_pointer) {
    GifFileType *gifFileType = (GifFileType *) gif_pointer;
    GifBean *gifBean = (GifBean *) gifFileType->UserData;

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

    void *pixels;// 像素数组
    // 锁定bitmap
    AndroidBitmap_lockPixels(env, bitmap, &pixels);
    // 绘制一帧图像
    drawFrame(gifFileType, gifBean, info, pixels);
    gifBean->current_frame += 1;
    if (gifBean->current_frame >= gifBean->total_frames) {
        gifBean->current_frame = 0;
    }

    AndroidBitmap_unlockPixels(env, bitmap);
    return gifBean->delays[gifBean->current_frame];
}

我们先分析loadGif(),这个方法的作用是将gif图加载进内存,然后根据gif图的格式取到延迟时间,并计算有多少帧。然后保存在GifBean这个结构体中。
获取宽高的方法getWidth(),getHeight()就是根据当前GifFileType结构体的指针可以直接取到,对应的还能取到的数据如下:

typedef struct GifFileType {
    GifWord SWidth, SHeight;         /* Size of virtual canvas */
    GifWord SColorResolution;        /* How many colors can we generate? */
    GifWord SBackGroundColor;        /* Background color for virtual canvas */
    GifByteType AspectByte;      /* Used to compute pixel aspect ratio */
    ColorMapObject *SColorMap;       /* Global colormap, NULL if nonexistent. */
    int ImageCount;                  /* Number of current image (both APIs) */
    GifImageDesc Image;              /* Current image (low-level API) */
    SavedImage *SavedImages;         /* Image sequence (high-level API) */
    int ExtensionBlockCount;         /* Count extensions past last image */
    ExtensionBlock *ExtensionBlocks; /* Extensions past last image */    
    int Error;               /* Last error condition reported */
    void *UserData;                  /* hook to attach user data (TVT) */
    void *Private;                   /* Don't mess with this! */
} GifFileType;

我们看updateFrame()方法。
这里我们通过AndroidBitmapInfo这个结构体获取需要绘制的信息,然后在drawFrame()方法中绘制一帧的图像。
然后我们看MainActivity中的调用。

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.

    private static final String TAG = "MainActivity";

    private ImageView image;
    private Bitmap bitmap;
    private GifNdkDecoder gifNdkDecoder;
    private Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(@NonNull Message msg) {
            long mNextFrameRenderTime =
                    gifNdkDecoder.updateFrame(bitmap, gifNdkDecoder.getGifPointer());
            handler.sendEmptyMessageDelayed(1, mNextFrameRenderTime);
            image.setImageBitmap(bitmap);
            return true;
        }
    });

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

    }

    public void ndkLoadGif(View view) {
        new MyAsyncTask().execute();
    }

    class MyAsyncTask extends AsyncTask<Void, Void, Void> {
        private ProgressDialog progressDialog;

        @Override
        protected void onPreExecute() {
            progressDialog = new ProgressDialog(MainActivity.this);
            progressDialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
            progressDialog.setTitle("正在加载Gif图片...");
            progressDialog.setCancelable(false);
            progressDialog.show();
        }

        @Override
        protected Void doInBackground(Void... voids) {
            File file = new File(Environment.getExternalStorageDirectory(), "demo.gif");
            long start = System.currentTimeMillis();
            gifNdkDecoder = GifNdkDecoder.load(file.getAbsolutePath());
            Log.e(TAG, "load gif 耗时" + (System.currentTimeMillis() - start));
            int width = gifNdkDecoder.getWidth(gifNdkDecoder.getGifPointer());
            int height = gifNdkDecoder.getHeight(gifNdkDecoder.getGifPointer());
            bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            return null;
        }

        @Override
        protected void onPostExecute(Void aVoid) {
            progressDialog.dismiss();
            long mNextFrameRenderTime =
                    gifNdkDecoder.updateFrame(bitmap, gifNdkDecoder.getGifPointer());
            handler.sendEmptyMessageDelayed(1, mNextFrameRenderTime);
        }
    }

    public void glideLoadGif(View view) {
        Glide.with(this).load(new File(Environment.getExternalStorageDirectory(), "demo.gif")).into(image);
    }
}

调用很简单,原理就是通过gif图片中的延迟时间不断的更新ImageView。
这里,我们还将NDK加载gif和Glide加载gif图的性能做了一个对比,发现NDK的加载时间要远远小于Glide加载,具体数据我就不展示了。感兴趣的小伙伴可以通过大的gif图做对比,结果很明显。

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

推荐阅读更多精彩内容