解读Android GIF文件native渲染之OpenGL

该博客基于koral的android-gif-drawable库,深入解析Android中使用GIFLIB通过native层进行GIF文件渲染的过程。文章详细介绍了如何创建GIF解析句柄、读取每帧图像数据以及实现OpenGL中的纹理指定与替换。虽然避免了JAVA层的OOM问题,但大量GIF加载可能产生内存碎片。

本系列文章以koral实现的GIF文件native渲染为根据,解读实现的大致过程以及关键代码分析,github地址:https://github.com/koral–/android-gif-drawable
由前文中纹理贴图native实现可知,若想实现图像的OpenGL渲染(主要glTexImage2D函数),首先需要解析出该图像文件(当时PNG为例)的图像数据以及其它相关信息,然后利用native OpenGL API实现,而GIF文件的渲染过程稍微麻烦一些,因为涉及到多帧图像的渲染,具体过程可以大致分为以下几个步骤。

(1)创建解析GIF文件的句柄

利用GIFLIB解析GIF文件,把相关信息保存在GifInfo结构体中,并将该结构体的指针(long)返回给JAVA层,作为访问GIF文件这些信息的句柄。从JAVA层到GIFLIB的大致实现流程如下:
这里写图片描述
GifInfo结构体:

struct GifInfo {
    void (*destructor)(GifInfo *, JNIEnv *);
    GifFileType *gifFilePtr;
    GifWord originalWidth, originalHeight;
    uint_fast16_t sampleSize;
    long long lastFrameRemainder;
    long long nextStartTime;
    uint_fast32_t currentIndex;
    GraphicsControlBlock *controlBlock;
    argb *backupPtr;
    long long startPos;
    unsigned char *rasterBits;
    uint_fast32_t rasterSize;
    char *comment;
    uint_fast16_t loopCount;
    uint_fast16_t currentLoop;
    RewindFunc rewindFunction;
    jfloat speedFactor;
    int32_t stride;
    jlong sourceLength;
    bool isOpaque;
    void *frameBufferDescriptor;
};

解析GIF文件相关信息的关键代码:

/******************************************************************************
This routine should be called before any other DGif calls. Note that
this routine is called automatically from DGif file open routines.
******************************************************************************/
int
DGifGetScreenDesc(GifFileType *GifFile) {
//    bool SortFlag;
    GifByteType Buf[3];
//    GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private;

//    if (!IS_READABLE(Private)) {
//        /* This file was NOT open for reading: */
//        GifFile->Error = D_GIF_ERR_NOT_READABLE;
//        return GIF_ERROR;
//    }

    /* Put the screen descriptor into the file: */
    if (DGifGetWord(GifFile, &GifFile->SWidth) == GIF_ERROR ||
        DGifGetWord(GifFile, &GifFile->SHeight) == GIF_ERROR)
        return GIF_ERROR;

    if (((GifFilePrivateType *) GifFile->Private)->Read(GifFile, Buf, 3) != 3) {
        GifFile->Error = D_GIF_ERR_READ_FAILED;
        GifFreeMapObject(GifFile->SColorMap);
        GifFile->SColorMap = NULL;
        return GIF_ERROR;
    }
//    GifFile->SColorResolution = (((Buf[0] & 0x70) + 1) >> 4) + 1;
//    SortFlag = (Buf[0] & 0x08) != 0;
    uint_fast8_t BitsPerPixel = (uint_fast8_t) ((Buf[0] & 0x07) + 1);
    GifFile->SBackGroundColor = Buf[1];
//    GifFile->AspectByte = Buf[2];
    if (Buf[0] & 0x80) {    /* Do we have global color map? */
        uint_fast16_t i;

        GifFile->SColorMap = GifMakeMapObject(BitsPerPixel, NULL);
        if (GifFile->SColorMap == NULL) {
            GifFile->Error = D_GIF_ERR_NOT_ENOUGH_MEM;
            return GIF_ERROR;
        }

        /* Get the global color map: */
//  GifFile->SColorMap->SortFlag = SortFlag;
        for (i = 0; i < GifFile->SColorMap->ColorCount; i++) {
            if (((GifFilePrivateType *) GifFile->Private)->Read(GifFile, Buf, 3) != 3) {
                GifFreeMapObject(GifFile->SColorMap);
                GifFile->SColorMap = NULL;
                GifFile->Error = D_GIF_ERR_READ_FAILED;
                return GIF_ERROR;
            }
            GifFile->SColorMap->Colors[i].Red = Buf[0];
            GifFile->SColorMap->Colors[i].Green = Buf[1];
            GifFile->SColorMap->Colors[i].Blue = Buf[2];
        }
    } else {
        GifFile->SColorMap = NULL;
    }

    return GIF_OK;
}

(2)读取每帧的图像数据

由于GIF文件是多帧的,所以GIF文件的渲染需要根据每帧的持续时间来获取不同帧图像来渲染,大致流程如下图:
这里写图片描述

getBitmap用于获取当前帧的图像数据,并保存在GifInfo结构的frameBufferDescriptor所指向的TexImageDescriptor结构中frameBuffer。
TexImageDescriptor结构:

typedef struct {
    struct pollfd eventPollFd;
    void *frameBuffer;
    pthread_mutex_t renderMutex;
    pthread_t slurpThread;
} TexImageDescriptor;

关键代码如下:

static void *slurp(void *pVoidInfo) {
    GifInfo *info = pVoidInfo;

    while (true) {
        long renderStartTime = getRealTime();
        // reset gif info
        DDGifSlurp(info, true, false);
        TexImageDescriptor *texImageDescriptor = info->frameBufferDescriptor;
        pthread_mutex_lock(&texImageDescriptor->renderMutex);
        if (info->currentIndex == 0) {
            prepareCanvas(texImageDescriptor->frameBuffer, info);
        }
        const uint_fast32_t frameDuration = getBitmap(texImageDescriptor->frameBuffer, info);
        pthread_mutex_unlock(&texImageDescriptor->renderMutex);

        const long long invalidationDelayMillis = calculateInvalidationDelay(info, renderStartTime, frameDuration);
        int pollResult = poll(&texImageDescriptor->eventPollFd, 1, (int) invalidationDelayMillis);
        DDGifSlurpTest(info, true, false);

        eventfd_t eventValue;
        if (pollResult < 0) {
            throwException(getEnv(), RUNTIME_EXCEPTION_ERRNO, "Could not poll on eventfd ");
            break;
        } else if (pollResult > 0) {
            const int readResult = TEMP_FAILURE_RETRY(eventfd_read(texImageDescriptor->eventPollFd.fd, &eventValue));
            if (readResult != 0) {
                throwException(getEnv(), RUNTIME_EXCEPTION_ERRNO, "Could not read from eventfd ");
            }
            break;
        }
    }
    DetachCurrentThread();
    return NULL;
}

(3)实现指定和替换纹理

在前面过程中,我们可以拿到GIF文件的相关信息以及当前所需绘制的帧图像,这样再实现纹理的指定和替换就很简单了,代码如下:
1.纹理指定

Java_pl_droidsonroids_gif_GifInfoHandle_glTexImage2D(JNIEnv *__unused unused, jclass __unused handleClass, jlong gifInfo, jint target, jint level) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    if (info == NULL || info->frameBufferDescriptor == NULL) {
        return;
    }
    const GLsizei width = (const GLsizei) info->gifFilePtr->SWidth;
    const GLsizei height = (const GLsizei) info->gifFilePtr->SHeight;
    TexImageDescriptor *descriptor = info->frameBufferDescriptor;
    void *const pixels = descriptor->frameBuffer;
    pthread_mutex_lock(&descriptor->renderMutex);
    glTexImage2D((GLenum) target, level, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    pthread_mutex_unlock(&descriptor->renderMutex);
}

2、纹理替换或修改

Java_pl_droidsonroids_gif_GifInfoHandle_glTexSubImage2D(JNIEnv *__unused env, jclass __unused handleClass, jlong gifInfo, jint target, jint level) {
    GifInfo *info = (GifInfo *) (intptr_t) gifInfo;
    if (info == NULL || info->frameBufferDescriptor == NULL) {
        return;
    }
    const GLsizei width = (const GLsizei) info->gifFilePtr->SWidth;
    const GLsizei height = (const GLsizei) info->gifFilePtr->SHeight;
    TexImageDescriptor *descriptor = info->frameBufferDescriptor;
    void *const pixels = descriptor->frameBuffer;
    pthread_mutex_lock(&descriptor->renderMutex);
    glTexSubImage2D((GLenum) target, level, 0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
    pthread_mutex_unlock(&descriptor->renderMutex);
}

由于整个实现过程都在native中,基本上不申请JAVA堆内存,因此不会出现OOM问题,但是显然不适宜列表加载很多GIF文件,因为频繁申请和释放native内存,会产生大量的内存碎片。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值