GifDecoder是android下用于解析gif文件的开源代码,网上即可下载,但是在一些低端机,具体多低不好说,
我在开发过程中发现在一台总共600M内存的android手机上运行时,抛出了OutOfMemoryError的错误。
当然除了解析gif文件自己一张一张显示外,也可以使用movie来显示gif图片。但本文想针对gifDecoder进行优化
优化目标:
1.加快gif显示,gifdecoder需要把所有的image解析后才显示,一般17-20帧的gif图片,解码大概会需要1-2秒时间
2.占用内存降低,使其可以在低端手机上正常运行
首先gifDecoder占用内存主要是在每张image的内存空间,1080*1400的gif图片,完全解析出来大概会占用4-5M的内存空间,那么15张图片需要的内存空间
就会超过60多M,显示在低端手机申请会失败,当然我也尝试使用android:largeHeap="true"来强制app使用大的heap,但是这类方法也只能缓解一时的内存不够用问题。
占用内存的代码:
private void setPixels() {
<span style="white-space:pre"> </span>//这里的dest在gif图片的宽和高一样情况下,完全可以重复使用,没必要每次分配
int[] dest = new int[width * height];
// fill in starting image contents based on last image's dispose code
if (lastDispose > 0) {
if (lastDispose == 3) {
// use image before last
int n = frameCount - 2;
if (n > 0) {
lastImage = getFrameImage(n - 1);
} else {
lastImage = null;
}
}
if (lastImage != null) {
lastImage.getPixels(dest, 0, width, 0, 0, width, height);
// copy pixels
if (lastDispose == 2) {
// fill last image rect area with background color
int c = 0;
if (!transparency) {
c = lastBgColor;
}
for (int i = 0; i < lrh; i++) {
int n1 = (lry + i) * width + lrx;
int n2 = n1 + lrw;
for (int k = n1; k < n2; k++) {
dest[k] = c;
}
}
}
}
}
// copy each source line to the appropriate place in the destination
int pass = 1;
int inc = 8;
int iline = 0;
for (int i = 0; i < ih; i++) {
int line = i;
if (interlace) {
if (iline >= ih) {
pass++;
switch (pass) {
case 2:
iline = 4;
break;
case 3:
iline = 2;
inc = 4;
break;
case 4:
iline = 1;
inc = 2;
}
}
line = iline;
iline += inc;
}
line += iy;
if (line < height) {
int k = line * width;
int dx = k + ix; // start of line in dest
int dlim = dx + iw; // end of dest line
if ((k + width) < dlim) {
dlim = k + width; // past dest edge
}
int sx = i * iw; // start of line in source
while (dx < dlim) {
// map color and insert in destination
int index = ((int) pixels[sx++]) & 0xff;
int c = act[index];
if (c != 0) {
dest[dx] = c;
}
dx++;
}
}
}
<span style="white-space:pre"> </span>//每次创建一张image
image = Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
}
两处问题:
1. dest其实可以重复使用,只要分配一次即可,没必要每次重新分配,可以改成:
if(dest == null)
dest = new int[width * height];
2. 占用内存处,但是一下子不好优化
Bitmap.createBitmap(dest, width, height, Config.ARGB_4444);
</pre><p></p><p>我想了一个办法 ,就是能不能边解析,边显示,显示时,再把前面已经显示过的image给recycle。</p><p>关键代码就在:</p><p></p><pre code_snippet_id="572210" snippet_file_name="blog_20150105_7_145681" name="code" class="java">private void readContents() {
// read GIF file content blocks
boolean done = false;
while (!(done || err())) {
int code = read();
switch (code) {
case 0x2C: // image separator
readImage();
break;
case 0x21: // extension
code = read();
switch (code) {
case 0xf9: // graphics control extension
readGraphicControlExt();
break;
case 0xff: // application extension
readBlock();
String app = "";
for (int i = 0; i < 11; i++) {
app += (char) block[i];
}
if (app.equals("NETSCAPE2.0")) {
readNetscapeExt();
} else {
skip(); // don't care
}
break;
default: // uninteresting extension
skip();
}
break;
case 0x3b: // terminator
done = true;
break;
case 0x00: // bad byte, but keep going and see what happens
break;
default:
status = STATUS_FORMAT_ERROR;
}
}
}
使用这里的while的done条件判断,在读到目标帧数后,while循环即刻退出,比如刚开始,我们解析2帧用于显示,在frameCount达到2时,则退出这个while循环,
由于 InputStream in;对应的缓存并没有删除,下次回来只要继续从in里读取即可。
于是我们在读到2帧后,gifDecoder返回的GifFrame其实只有2帧image,然后在imageview显示到第2帧时,由于gif显示都有一个delay时间,比如200ms,此时我们正好利用这个delay时间去加载第3帧,并且删除第1帧占用的image空间,主要代码如下:
if(curFrame != null && curFrame.nextFrame == null)
{
//下一张已经为空时,则需要再去读一张gif图片
GifDecoder.getInstance().continueReadFrame(curFrame);
}
我们可以把这些时间算在delay时间里,postdelay时减去这些耗去的时间即可。以下是新增加的continueReadFrame代码
public boolean continueReadFrame(GifFrame frame)
{
if(frame == null || read_contents_over)
{
return read_contents_over;
}
read_contents_done = false;
readed_frame_count ++;
readContents();
int cnt = frames.size();
if(cnt < readed_frame_count)
{
Log.i("this time no read, not added element!");
<span style="white-space:pre"> </span>
}else
{
frame.nextFrame = frames.lastElement();
}
return read_contents_over;
}
然后你可以在适当的地方调用以下函数:
fr.image.recycle();
来清除之前的image cache,调用之前判断一下是不是isRecycle()了。
经过如此优化,我们的gifDecoder基本上已经满足了上面两个目标,加载延迟基本上感觉不到了,而且可以在低端的android机器上跑起来了。
注意事项:
1.在gif图片显示一轮完成后,如果需要再次循环显示,则请重新再做一遍相同的逻辑即可
2.增加了ImageView和gifDecoder之间的耦合性,所以在一些不需要考虑低端机器或gif图片只有5张以内时,就不需要采用此方法
3.如果嫌麻烦,则直接使用movie即可
4.网上也有一个版本是通过回调的方法来通知加载前frame已经解析完毕,也可以在这个版本的基础上,自己修改下,把之前的frame的image释放掉即可。应该更加简单一些。
https://code.google.com/p/gifview/