最近在做一个弹幕的功能,涉及到弹幕头像、礼物等下载、缓存等。使用了开源的<Android 开源框架Universal-Image-Loader>,做起来比较顺手。原因是该开源框架比较完善,调用者很容易上手,用起来和方便。在此就不说开源框架了,其实自己要写一个开源框架,最主要的是把原理弄懂。先谈一谈这个bitmap吧。有些知识是从网络摘操,因为写的很好。
bitmap是位图,它将图像定义为由点(像素)组成,每个点可以由多种色彩表示,包括2、4、8、16、24和32位色彩。例如,一幅1024×768分辨率的32位真彩图片,其所占存储字节数为:1024×768×32/(8*1024)=3072KB。
Android中展示不同格式的图像,在内存中都是一个二进制数据。都是用Bitmap对象存储的。这个内存包括图片的像素数据和本身对象。图像占用内存过多会导致OOM。但是不恰当的使用,使得OOM发生的几率更大。那为什么会发生OOM?相信大家都知道,Android系统分配给每个应用程序的内存是有限的(在Android 3.1以及更高的版本中,可以在AndroidManifest.xml的application标签中增加一个值等于“true”的android:largeHeap属性来通知Dalvik虚拟机应用程序需要使用较大的Java Heap,)。当使用内存超过限定,就会发生。所以,控制好图片内存是非常关键的。经过Android系统的更新,不同版本对Bitmap的存储有些差异。
在Android2.3.3(API 10)及之前的版本中,Bitmap对象与其像素数据是分开存储。
Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)中或者说Derict Memory(直接内存)中。
这使得存储在Native Memory中的像素数据的释放是不可预知的,我们可以调用recycle()方法来对Native Memory中的像素数据进行释放。
前提是你可以清楚的确定Bitmap已不再使用了,如果你调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现"Canvas: trying to use a recycled bitmap"错误。
在Android3.0(API 11)之后,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做。2.3以后采用并发垃圾回收,会通过垃圾回收算法检查对象是否可以回收(不同的垃圾回收器具有不同的回收算法),如果在回收器没有及时回收图片,或者某个时候创建大量的bitmap,此时就有可能会OOM。
那么怎么恰当的处理图片减少OOM的几率呢?
1.在Android2.3以下,使用图片,先判断图片是否有效即是否已经回收,未回收则需用recycle()方法来释放内存。比如在大量图片在加载的listview,gridview中,这可以通过计数器记录该图片引用次数,在未被引用情形下,及时回收图片。如下:
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
2.在Android以上,垃圾回收主动回收图片,所以不用考虑回收问题。但是,因为图片的解码要耗大量内存,可以在图片解码做优化处理。怎么处理呢?
(1)BitmapFactory.Options第一次先获取图片大小,再和要显示的view大小做一个scale,之后再做解码为view大小的图片。这样,如果原图很大,显示的view很小,就不会占用太多内存。如下:
public static Bitmap decodeStream(InputStream is, Config pixelFormat, int outWidth, int outHeight, int maxWidth, int maxHeight) {
byte[] decode_buf = _local_buf.get();
if (decode_buf == null) {
decode_buf = new byte[64 * 1024];
_local_buf.set(decode_buf);
}
try {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inTempStorage = decode_buf;
opts.inPreferredConfig = pixelFormat;
opts.inJustDecodeBounds = true; //先获取图片大小
is.mark(64 * 1024);
BitmapFactory.decodeStream(is, null, opts);//第一次解码获取图片打下
try {
is.reset();
} catch (IOException e) {
e.printStackTrace();
is.close();
return null;
}
if ((outWidth > 0 && outHeight >= 0) || (outWidth >= 0 && outHeight > 0) ||
(maxWidth > 0 && maxHeight >= 0) || (maxWidth >= 0 && maxHeight > 0)) {
opts.inScaled = true;
opts.inTargetDensity = DisplayMetrics.DENSITY_DEFAULT;
int scale_x;
int scale_y;
if (maxWidth > 0 || maxHeight > 0) {
if (outHeight == 0 && outWidth == 0) {
outWidth = Math.min(opts.outWidth, maxWidth);
outHeight = Math.min(opts.outHeight, maxHeight);
} else {
if (outWidth > 0 && maxWidth > 0) {
outWidth = Math.min(outWidth, maxWidth);
}
if (outHeight > 0 && maxHeight > 0) {
outHeight = Math.min(outHeight, maxHeight);
}
}
}
if (outWidth == 0) {
scale_x = scale_y = opts.inTargetDensity * opts.outHeight / outHeight;
} else if (outHeight == 0) {
scale_x = scale_y = opts.inTargetDensity * opts.outWidth / outWidth;
} else {
scale_x = opts.inTargetDensity * opts.outWidth / outWidth;
scale_y = opts.inTargetDensity * opts.outHeight / outHeight;
}
opts.inDensity = Math.min(scale_x, scale_y);
if (opts.inDensity == opts.inTargetDensity) {
opts.inScaled = false;
outWidth = opts.outWidth;
outHeight = opts.outHeight;
} else {
outWidth = XulUtils.roundToInt((float) opts.outWidth * opts.inTargetDensity / opts.inDensity);
outHeight = XulUtils.roundToInt((float) opts.outHeight * opts.inTargetDensity / opts.inDensity);
}
} else {
outWidth = opts.outWidth;
outHeight = opts.outHeight;
}
Log.i(TAG, " width " + opts.outWidth + " height " + opts.outHeight + " ==> width " + outWidth + " height " + outHeight);
opts.inJustDecodeBounds = false;
opts.inPurgeable = false;
opts.inMutable = true;
if (!opts.inScaled ) {
opts.inBitmap = createBitmapFromRecycledBitmaps(opts.outWidth, opts.outHeight, pixelFormat);//可以设置图片重用,
} else {
++_newCount;
}
opts.inSampleSize = 1;
Bitmap bm = BitmapFactory.decodeStream(is, null, opts);//第二次解码图片,ops带scale
return bm;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
以上代码注意第一次设置inJustDecodeBounds为true,获取图片尺寸。再进行解码获取图片。
另外BitmapFactory.Options.inBitmap的这个字段,假如这个字段被设置了,在解码Bitmap的时候,就会去重用inBitmap设置的Bitmap,减少内存的分配和释放,提高了应用的性能(该参数不是用来减少内存占用,而是用来内存重用,避免大内存块的申请与释放。)。因为内存的释放(GC)可能影响所有线程绘制。
什么条件会被重用?
Android 4.4之前,设置的Bitmap只有同等大小的图片所占用的内存才能被重用。 即宽高相等,且inSampleSize为1。 而4.4之后你可以重用任何bitmap的内存区域,只要这块内存比将要分配内存的bitmap大就可以。例如给inBitmap赋值的图片大小为100-100,那么新申请的bitmap必须也为100-100才能够被重用。从SDK 19开始,新申请的bitmap大小必须小于或者等于已经赋值过的bitmap大小。解码新申请的bitmap与旧的bitmap必须有相同的解码格式。那么就不能支持4444与565格式的bitmap了,不过可以通过创建一个包含多种典型可重用bitmap的对象池,这样后续的bitmap创建都能够找到合适的“模板”去进行重用。
在实际的项目中,有些时候设备在设置重用图片后,会导致图片出现花屏的现象,即解码失败导致。这样的情况,就不要开启重用图片的功能。