VC调用giflib(4):内存泄漏与功能缺失
作者:马健
邮箱:stronghorse_mj@hotmail.com
主页:http://www.comicer.com/stronghorse
发布:2020.03.14
一、EGifSpew的内存泄漏与功能缺失
在上一篇笔记里说过,我因为要计算全局调色板和局部调色板,所以只能用EGifSpew对GIF进行编码,但giflib中对这个函数的实现存在严重的内存泄漏问题。
1)EGifPutScreenDesc函数调用造成的内存泄漏
在EGifSpew函数开头部分是这样调用EGifPutScreenDesc函数的:
int EGifSpew(GifFileType *GifFileOut) { int i, j; if (EGifPutScreenDesc(GifFileOut, GifFileOut->SWidth, GifFileOut->SHeight, GifFileOut->SColorResolution, GifFileOut->SBackGroundColor, GifFileOut->SColorMap) == GIF_ERROR) { return (GIF_ERROR); } ……
而在EGifPutScreenDesc函数中,是这样操作的:
int EGifPutScreenDesc(GifFileType *GifFile, const int Width, const int Height, const int ColorRes, const int BackGround, const ColorMapObject *ColorMap) { GifByteType Buf[3]; GifFilePrivateType *Private = (GifFilePrivateType *) GifFile->Private; const char *write_version; GifFile->SColorMap = NULL; …… if (ColorMap) { GifFile->SColorMap = GifMakeMapObject(ColorMap->ColorCount, ColorMap->Colors); …… } else GifFile->SColorMap = NULL;
即一进EGifPutScreenDesc函数,三不管就把GifFile->SColorMap赋空了,后面再重新给它分配内存。而在我的源代码中,是先给GifFile->SColorMap分配了全局调色板的,结果进去后就成了野指针。这个操作够不够骚?说实话我刚看到的时候真的是被雷到了,不由感慨我还是太年轻,这世上还有无数的奇葩我还没见识过。
解决办法就是在调用EGifPutScreenDesc函数前先对GifFile->SColorMap指针进行备份,调用后释放掉即可。
2)EGifCloseFile函数调用造成的内存泄漏
一般使用EGifSpew函数生成动画GIF的过程,都是先把各帧图像存储到GifFileType结构体的SavedImages数组中,然后再调用EGifSpew把数组中的各帧顺序编码、存盘。但是EGifSpew的问题就在于各帧编码完成后,并没有释放SavedImages数组中的各帧内存,直接就调用EGifCloseFile函数释放掉GifFileType结构体,结果SavedImages数组中的各帧就成了没爹没娘的野指针。
解决的办法就是在调用EGifCloseFile之前,先调用GifFreeSavedImages释放掉SavedImages数组中分配的内存。
除了上述内存泄漏问题外,EGifSpew函数中还存在一个功能缺失:在EGifPutScreenDesc后,没有写入动画GIF的循环次数,导致生成的GIF文件只能动一次。正常情况下循环次数是写在帧数据前面的,所以应该在EGifPutScreenDesc之后,在for循环写各帧数据之前。
最后我实在是没办法,只能把原版EGifSpew函数复制一份出来修改成MyEGifSpew,完整代码如下:
// MyEGifSpew要用到此函数,但在egif_lib.c中此函数是static局部函数,所以照抄一遍 static int EGifWriteExtensions(GifFileType *GifFileOut, ExtensionBlock *ExtensionBlocks, int ExtensionBlockCount) { if (ExtensionBlocks) { int j; for (j = 0; j < ExtensionBlockCount; j++) { ExtensionBlock *ep = &ExtensionBlocks[j]; if (ep->Function != CONTINUE_EXT_FUNC_CODE) if (EGifPutExtensionLeader(GifFileOut, ep->Function) == GIF_ERROR) return (GIF_ERROR); if (EGifPutExtensionBlock(GifFileOut, ep->ByteCount, ep->Bytes) == GIF_ERROR) return (GIF_ERROR); if (j == ExtensionBlockCount - 1 || (ep+1)->Function != CONTINUE_EXT_FUNC_CODE) if (EGifPutExtensionTrailer(GifFileOut) == GIF_ERROR) return (GIF_ERROR); } } return (GIF_OK); } // 原版的EGifSpew存在内存漏洞,而且没有设置循环播放次数,导致生成的GIF只能播放一次 // nLoopTimes:循环播放次数,0表示无限循环播放 static int MyEGifSpew(GifFileType *GifFileOut, int nLoopTimes) { int i, j; // EGifPutScreenDesc会破坏GifFileOut->SColorMap,所以调用前必须先备份,否则出现野指针 ColorMapObject* SColorMap = GifFileOut->SColorMap; if (EGifPutScreenDesc(GifFileOut, GifFileOut->SWidth, GifFileOut->SHeight, GifFileOut->SColorResolution, GifFileOut->SBackGroundColor, GifFileOut->SColorMap) == GIF_ERROR) { return (GIF_ERROR); } // 在EGifPutScreenDesc中重新分配了GifFileOut->SColorMap,所以手工释放前面保存的 GifFreeMapObject(SColorMap); // 在共享调色板之后,写入循环次数。原版EGifSpew少了这一项,所以生成的GIF只能动一次 { /* Create a Netscape 2.0 loop block */ unsigned char params[3] = {1, 0, 0}; // 后两个字节是循环次数,0表示无限循环 params[1] = (nLoopTimes & 0xff); params[2] = (nLoopTimes >> 8) & 0xff; if (EGifPutExtensionLeader(GifFileOut, APPLICATION_EXT_FUNC_CODE) != GIF_OK || EGifPutExtensionBlock(GifFileOut, 11, "NETSCAPE2.0") != GIF_OK || EGifPutExtensionBlock(GifFileOut, 3, params) != GIF_OK || EGifPutExtensionTrailer(GifFileOut) != GIF_OK ) return (GIF_ERROR); } for (i = 0; i < GifFileOut->ImageCount; i++) { SavedImage *sp = &GifFileOut->SavedImages[i]; int SavedHeight = sp->ImageDesc.Height; int SavedWidth = sp->ImageDesc.Width; /* this allows us to delete images by nuking their rasters */ if (sp->RasterBits == NULL) continue; if (EGifWriteExtensions(GifFileOut, sp->ExtensionBlocks, sp->ExtensionBlockCount) == GIF_ERROR) return (GIF_ERROR); if (EGifPutImageDesc(GifFileOut, sp->ImageDesc.Left, sp->ImageDesc.Top, SavedWidth, SavedHeight, sp->ImageDesc.Interlace, sp->ImageDesc.ColorMap) == GIF_ERROR) return (GIF_ERROR); if (sp->ImageDesc.Interlace) { /* * The way an interlaced image should be written - * offsets and jumps... */ int InterlacedOffset[] = { 0, 4, 2, 1 }; int InterlacedJumps[] = { 8, 8, 4, 2 }; int k; /* Need to perform 4 passes on the images: */ for (k = 0; k < 4; k++) for (j = InterlacedOffset[k]; j < SavedHeight; j += InterlacedJumps[k]) { if (EGifPutLine(GifFileOut, sp->RasterBits + j * SavedWidth, SavedWidth) == GIF_ERROR) return (GIF_ERROR); } } else { for (j = 0; j < SavedHeight; j++) { if (EGifPutLine(GifFileOut, sp->RasterBits + j * SavedWidth, SavedWidth) == GIF_ERROR) return (GIF_ERROR); } } } if (EGifWriteExtensions(GifFileOut, GifFileOut->ExtensionBlocks, GifFileOut->ExtensionBlockCount) == GIF_ERROR) return (GIF_ERROR); // 原版EGifSpew少了这一句,导致大量的内存漏洞 GifFreeSavedImages(GifFileOut); if (EGifCloseFile(GifFileOut, NULL) == GIF_ERROR) return (GIF_ERROR); return (GIF_OK); }
上面的代码修正了两处内存泄漏,并允许调用时设置循环次数。在写入循环次数的时候,使用了最流行的NETSCAPE2.0,没有用比较少见的ANIMEXTS1.0。
另外如果是对细节比较在意的程序员,在调用EGifSpew开始写GIF文件之前,还应该调用EGifGetGifVersion函数,让giflib自动设置输出的GIF文件的版本。虽然不调用也不会有啥大影响,但细节终归是细节。
二、EGifPutImageDesc函数中的内存泄露
这个内存泄漏比较隐蔽。先看该函数中的一段代码:
if (ColorMap != GifFile->Image.ColorMap) { if (ColorMap) { // 用ColorMap更新GifFile->Image.ColorMap …… } else { GifFile->Image.ColorMap = NULL; } }
如果满足第一层条件ColorMap != GifFile->Image.ColorMap,说明这两个指针总有一个非空,第二层判断的else块中ColorMap为空情况下,GifFile->Image.ColorMap就必然不为空,这个时候把它直接赋空,妥妥的内存泄露。
解决方法很简单,在
GifFile->Image.ColorMap = NULL;
之前加一句
GifFreeMapObject(GifFile->Image.ColorMap);
即可。
我喜欢VC的原因之一,就是VC有内存泄漏报告机制,即在debug状态下应用退出后,会逐条报告所有未被释放的内存及该条内存申请时的编号,有了编号再找导致内存泄漏的源代码就相对比较容易,下条件断点即可。
(全文完)