美文网首页
NDK加载动图(二)之代码解析

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

作者: 梦星夜雨 | 来源:发表于2021-03-10 20:41 被阅读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图做对比,结果很明显。

    相关文章

      网友评论

          本文标题:NDK加载动图(二)之代码解析

          本文链接:https://www.haomeiwen.com/subject/kwkrqltx.html