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

2021-03-10  本文已影响0人  梦星夜雨

前言

上一篇我们讲完了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图做对比,结果很明显。

上一篇 下一篇

猜你喜欢

热点阅读