本系列文章以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內存,會產生大量的內存碎片。