在Android中,Bitmap很容易引起内存空间耗尽而导致程序崩溃的问题
由于是一下几点:
1 高效地加载大图片。
BitmapFactory类提供了一些加载图片的方法:decodeByteArray(), decodeFile(), decodeResource(), 等等。
为了避免占用较大内存,经常使用BitmapFactory.Options 类,设置inJustDecodeBounds属性为true。
// BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds =true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
为了避免java.lang.OutOfMemory 的异常,我们在真正decode图片之前检查它的尺寸,除非你确定这个数据源提供了准确无误的图片且不会导致占用过多的内存。
加载一个按比例缩小的版本到内存中。例如,如果把一个原图是1024*768 pixel的图片显示到ImageView为128*96 pixel的缩略图就没有必要把整张图片都加载到内存中。为了告诉解码器去加载一个较小的图片到内存,需要在你的BitmapFactory.Options 中设置 inSampleSize 为true 。例如, 一个分辨率为2048x1536 的图片,如果设置inSampleSize 为4,那么会产出一个大概为512x384的图片。加载这张小的图片仅仅使用大概0.75MB,如果是加载全图那么大概要花费12MB(假设bitmap的配置是ARGB_8888).
2 不要在主线程处理图片。
众所周知的问题,不再赘述。
注意两点:1. 为了保证使用的资源能被回收,建议使用WeakReference, 以应用内存内存紧张时,回收部分资源,保证程序进程不被杀死。
2. 避免异步任务的长时间耗时操作,在任务执行结束后,及时释放资源。
3 管理Bitmap内存。
在Android开发中,加载一个图片到界面很容易,但如果一次加载大量图片就复杂多了。在很多情况下(比如:ListView,GridView或ViewPager),能够滚动的组件需要加载的图片几乎是无限多的。
有些组件的child view在不显示时会回收,并循环使用,如果没有任何对bitmap的持久引用的话,垃圾回收器会释放你加载的bitmap。这没什么问题,但当这些图片再次显示的时候,要想避免重复处理这些图片,从而达到加载流畅的效果,就要使用内存缓存和本地缓存了,这些缓存可以让你快速加载处理过的图片。
3.1 内存缓存
内存缓存以牺牲内存的代价,带来快速的图片访问。LruCache类(API Level 4之前可以使用Support Library)非常适合图片缓存任务,在一个LinkedHashMap中保存着对Bitmap的强引用,当缓存数量超过容器容量时,删除最近最少使用的成员(LRU)。
注意:在过去,非常流行用SoftReference或WeakReference来实现图片的内存缓存,但现在不再推荐使用这个方法了。因为从Android 2.3 (API Level 9)之后,垃圾回收器会更积极的回收soft/weak的引用,这将导致使用soft/weak引用的缓存几乎没有缓存效果。顺带一提,在Android3.0(API Level 11)以前,bitmap是储存在native 内存中的,所以系统以不可预见的方式来释放bitmap,这可能会导致短时间超过内存限制从而造成崩溃。
为了给LruCache一个合适的容量,需要考虑很多因素,比如:
你其它的Activity 和/或 Application是怎样使用内存的?
屏幕一次显示多少图片?需要多少图片为显示到屏幕做准备?
屏幕的大小(size)和密度(density)是多少?像Galaxy Nexus这样高密度(xhdpi)的屏幕在缓存相同数量的图片时,就需要比低密度屏幕Nexus S(hdpi)更大的内存。
每个图片的尺寸多大,相关配置怎样的,占用多大内存?
图片的访问频率高不高?不同图片的访问频率是否不一样?如果是,你可能会把某些图片一直缓存在内存中,或需要多种不同缓存策略的LruCache。
你能平衡图片的质量和数量吗?有时候,缓存多个质量低的图片是很有用的,而质量高的图片应该(像下载文件一样)在后台任务中加载。
这里没有适应所有应用的特定大小或公式,只能通过分析具体的使用方法,来得出合适的解决方案。缓存太小的话没有实际用处,还会增加额外开销;缓存太大的话,会再一次造成OutOfMemory异常,并给应用的其他部分留下很少的内存。
3.2 使用磁盘缓存
内存缓存能够加快对最近显示过的图片的访问速度,然而你不能认为缓存中的图片全是有效的。像GridView这样需要大量数据的组件是很容易填满内存缓存的。你的应用可能会被别的任务打断(比如一个来电),它可能会在后台被杀掉,其内存缓存当然也被销毁了。当用户恢复你的应用时,应用将重新处理之前缓存的每一张图片。
在这个情形中,使用磁盘缓存可以持久的储存处理过的图片,并且缩短加载内存缓存中无效的图片的时间。当然从磁盘加载图片比从内存中加载图片要慢的多,并且由于磁盘读取的时间是不确定的,所以要在后台线程进行磁盘加载。
注意:如果以更高的频率访问图片,比如图片墙应用,使用ContentProvider可能更适合储存图片缓存。
读取图片尺寸使用inJustDecodeBounds参数
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(filePath, options);
int width = options.outWidth;
int height = options.outHeight
使用inSampleSize解码低分辨率图片
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inSampleSize = 4; // inSampleSize为2的次方
Bitmap bmp = BitmapFactory.decodeFile(filePath, options);
利用DrawingCache截取View并缩放(不推荐)
public static Bitmap captureFromView(View view, float scale) {
if (view == null) {
return null;
}
view.setDrawingCacheEnabled(true);
Bitmap bitmap = null;
try {
Bitmap cacheBitmap = view.getDrawingCache();
if (null != cacheBitmap) {
bitmap = Bitmap.createScaledBitmap(cacheBitmap,
(int) (cacheBitmap.getWidth() * scale),
(int) (cacheBitmap.getHeight() * scale), false);
}
} catch (OutOfMemoryError e) {
return null;
} finally {
view.setDrawingCacheEnabled(false);
view.destroyDrawingCache();
}
return bitmap;
}
利用Canvas截取View,并缩小一半
public static Bitmap captureFromView(View view) {
if (view == null) {
return null;
}
Bitmap bitmap = Bitmap.createBitmap(view.getWidth() / 2, view.getHeight() / 2, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
canvas.scale(0.5f, 0.5f); // Canvas坐标轴缩小一倍
view.draw(canvas);
return bitmap;
}
举例:PNG资源文件绘制到Bitmap中
int width = 720;
int height = 960;
int resId = R.drawable.ic_background;
Bitmap result = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(result);
原始代码:
从资源文件创建Bitmap,然后缩放成新的Bitmap,再绘制到Canvas上。
期间最多时同时有3个Bitmap存在,占用内存很大。
第一个bmp没有调用
recycle()
方法触发内存回收。
Bitmap bmp = BitmapFactory.decodeResource(context.getResources(), resId);
bmp = Bitmap.createScaledBitmap(bmp, width, height, false);
canvas.drawBitmap(bmp, 0, 0, null);
bmp.recycle();
优化后的代码:
- 直接使用BitmapDrawable绘制。
- BitmapDrawable内部做了缓存,避免了自己创建Bitmap。
- 图片的缩放由Canvas在JNI层由系统完成,不占用Java层内存。
Drawable drawable = context.getDrawable(resId);
if (drawable != null) {
drawable.setBounds(0, 0, width, height);
drawable.draw(canvas);
}