美文网首页android技术收藏Android-NDK/JNI
Android-JNI开发系列《十一》实践-利用Android

Android-JNI开发系列《十一》实践-利用Android

作者: 后厂村追寻 | 来源:发表于2020-11-27 22:59 被阅读0次

    人间观察

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

    声明

    此篇文章只为记录和学习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文件,可以选择性的拷贝源文件和头文件,这里就直接全部拷贝了。

    基本的实现流程:

    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

    https://www.jianshu.com/p/73cf5eacda00

    https://www.jianshu.com/p/c0fea0e32817

    相关文章

      网友评论

        本文标题:Android-JNI开发系列《十一》实践-利用Android

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