人間觀察
人往往都是多面性的,一個人的時候是一個樣子,一羣人的時候是另一個樣子。
聲明
此篇文章只爲記錄和學習JNI以及瞭解GIF的解碼原理。借鑑了網上的有關gif文章介紹和代碼。如果是自己學習,建議自己敲一遍jni的代碼,不要眼高手低。
建議
如果在項目中使用實現GIF的播放的功能,建議java版本glide
的解碼GIF或者c版本的
https://github.com/koral–/android-gif-drawable
爲什麼呢? 因爲我測試了下多個gif文件用glide
或者android-gif-drawable
都可以正常播放,但是如果用本篇文章的實現有些gif播放出現花屏/局部黑色(應該是系統的源碼去掉了alpha
通道導致,當然我們也寫死了alpha=255,因爲看了下Android 系統的源碼GifColorType
並沒有提供解壓後的alpha的通道的字段,所以沒有辦法設置)。所以本篇我們學習瞭解GIF的解碼原理和熟悉jni即可。
利用Android 系統的源碼來實現GIF的解碼播放
Android 系統的gif的解碼在/external/giflib
目錄下,源碼如下
http://androidos.net.cn/android/9.0.0_r8/xref/external/giflib
Android 系統的源碼支持gif的解碼和生成gif文件,可以選擇性的拷貝源文件和頭文件,這裏就直接全部拷貝了。
基本的實現流程:
- 在jni層中打開gif文件並加載得到gif的總播放幀數和每幀之間的延遲時間
- 在java層中創建
ARGB_8888
格式的bitmap
(jni層是按照每像素4字節處理的) - 在jni層獲取步驟2中的bitmap對應的像素數據指針
void**
- 解碼gif的每幀的數據然後對步驟3中的像素數據指針進行賦值,像素格式還是
abgr
。這樣java層調用imageView.setImageBitmap(bitmap);
數據就是修改後的。 - 定時刷新執行步驟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];
}
效果圖
效果圖略顯異常,是因爲錄屏的原因
源碼
https://github.com/ta893115871/GifJniDecode
注意下讀寫sd卡的權限
參考網上的有關文章
https://blog.csdn.net/poisx/article/details/79122506