GifDecoder解析gif文件时抛出的OutOfMemory问题

本文介绍了一种优化GifDecoder的方法,使它能在低端Android设备上流畅显示GIF图片,通过边解析边显示的方式减少内存占用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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/


评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值