Bitmap与OOM
Bitmap所造成的OOM
图片是一个很耗内存的资源,因此经常会遇到OOM。比如从本地文件中读取图片,然后在GridView中显示出来,如果不做处理,OOM就极有可能发生。
Bitmap引起OOM的原因:
1. 图片使用完成后,没有及时的释放,导致Bitmap占用的内存越来越大,而安卓提供给Bitmap的内存是有一定限制的, 当超出该内存时,自然就发生了OOM
2. 图片过大
这里的图片过大是指加载到内存时所占用的内存,并不是图片自身的大小。而图片加载到内存中时所占用的内存是根据图片的分辨率以及它的配置(ARGB值)计算的。举个例子:
假如有一张分辨率为2048x1536的图片,它的配置为ARGB8888,那么它加载到内存时的大小就是2048x1526x4/1024/1024=12M.,因此当将这张图片设置到ImageView上时,将会出现OOM(超过了Android分配给Bitmap的上限8M)。
补充:ARGB表示图片的配置,分表代表:透明度、红色、绿色和蓝色。这几个参数的值越高代表图像的质量越好,那么也就越占内存。就拿ARGB8888来说,A、R、G、B这几个参数分别占8位,那么总共占32位,代表一个像素点占32位大小即4个字节,那么一个100x100分辨率的图片就占了100x100x4/1024/1024=0.04M的大小的空间。高效加载Bitmap
当将一个图片加载到内存,在UI上呈现时,需要考虑一下几个因素:
- 预计加载完整张图片所需要的内存空间
- 呈现这张图片时控件的大小
- 屏幕大小与屏幕像素密度
如果我们要加载的图片的分辨率比较大,而呈现它的控件(比如ImageView)比较小,那我们如果直接将这张图片加载到这个控件上显然是不合适的,因此我们需要对图片的分辨率就行压缩。如何去进行图片的压缩呢?
BitmapFactory提供了四种解码(decode)的方法(decodeByteArray(), decodeFile(), decodeResource(),decodeStream()),每一种方法都可以通过BitmapFactory.Options设置一些附加的标记,以此来指定解码选项。
Options有一个inJustDecodeBunds属性,当我们将其设置为true时,表示此时并不加载Bitmap到内存中,而是返回一个null,但是此时我们可以通过options获取到当前bitmap的宽和高,根据这个宽和高,我们再根据目标宽和高计算出一个合适的采样率采样率inSampleSize ,然后将其赋值给Options.inSampleSize属性,这样在加载图片的时候,将会得到一个压缩的图片到内存中。以下是示例代码:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) { // 第一次加载时 将inJustDecodeBounds设置为true 表示不真正加载图片到内存 final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeResource(res, resId, options); // 根据目标宽和高 以及当前图片的大小 计算出压缩比率 options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 将inJustDecodeBounds设置为false 真正加载图片 然后根据压缩比率压缩图片 再去解码 options.inJustDecodeBounds = false; return BitmapFactory.decodeResource(res, resId, options); } //计算压缩比率 android官方提供的算法 public static int calculateInSampleSize( BitmapFactory.Options options, int reqWidth, int reqHeight) { // Raw height and width of image final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1; if (height > reqHeight || width > reqWidth) { //将当前宽和高 分别减小一半 final int halfHeight = height / 2; final int halfWidth = width / 2; // Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width. while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { inSampleSize *= 2; } } return inSampleSize; }
关于采样率与图片分辨率压缩大小的关系:
- 如果inSample=1则表明与原图一样
- 如果inSample=2则表示宽和高均缩小为1/2
- inSample的值一般为2的幂次方
假如 一个分辨率为2048x1536的图片,如果设置 inSampleSize 为4,那么会产出一个大约512x384大小的Bitmap。加载这张缩小的图片仅仅使用大概0.75MB的内存,如果是加载完整尺寸的图片,那么大概需要花费12MB(前提都是Bitmap的配置是 ARGB_8888.
缓存Bitmap 当需要加载大量的图片时,图片的缓存机制就特别重要。因为在移动端,用户大多都是使用的移动流量,如果每次都从网络获取图片,一是会耗费大量的流量,二是在网络不佳的时候加载会非常的慢,用户体验均不好。因此需要定义一种缓存策略可以应对上述问题。关于图片的缓存通常有两种:
1.内存缓存,对应的缓存算法是LruCache<k,v>(近期最少使用算法),Android提供了该算法
LruCache是一个泛型类,它的内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,其提供了get和put方法来完成缓存的获取和添加操作,当缓存满时,LruCache会移除较早使用的缓存对象,然后再添加新的缓存对象。
补充:之所以使用LinkedHashMap来实现LruCache是因为LinkedHashMap内部采用了双向链表的方式,它可以以访问顺序进行元素的排序。比如通过get方法获取了一个元素,那么就将这个元素放到链表的尾部,通过不断的get操作就得到了一个访问顺序的链表,这样位于链表头部的就是较早的元素。因此非常适合于LruCache算法的思想,在缓存满时,将链表头部的对象移除即可。LruCache经典使用方式:
//app最大可用内存 int maxMemory = (int) (Runtime.getRuntime().maxMemory()/1024); //缓存大小 int ***Size = maxMemory/8; mMemoryCache = new LruCache<String,Bitmap>(***Size) { //计算缓存对象的大小 @Override protected int sizeOf(String key, Bitmap value) { return value.getRowBytes()*value.getHeight()/1024; } }; //获取缓存对象 mMemoryCache.get(key); //添加缓存对象 mMemoryCache.put(key,bitmap);
2.磁盘缓存,对应的缓存算法是DiskLruCache,虽然不是官方提供的,但得到官方的认可。可以通过下面的链接进行源码下载:
http://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.javaDiskLruCache的创建
public static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)
DiskLruCache不能通过构造方法来创建,而是用open方法,它有四个参数:
1. directory:表示磁盘缓存的文件路径可以选择Sd卡上的缓存目录:/sdcard/Android/data/package_name/***,也可以选择其他的目录作为缓存目录,如果希望保留的缓存数据在app卸载时,也删除,那么应该选择sd卡上的缓存目录,否则的话选择其他的目录。
2.appVersion:app版本号,初始设为1.该参数表示当版本发生变化时,DiskLruCache会清空之前的缓存文件
3.valueCount 表示同一个key可以对应多少个文件,一般为1
4.maxSize 缓存的总大小
具体的使用参考这个博客:http://blog.csdn.net/guolin_blog/article/details/28863651使用Bitmap的一些优化方法
1. 对图片采用软引用,调用recycle,及时的回收Bitmap所占用的内存。比如:View如果使用了bitmap,就应该在这个View不再绘制了的时候回收;如果Activity使用了bitmap,就可以在onStop或者onDestroy方法中回收。
SoftReference bitmap; bitmap = new SoftReference(pBitmap);
if(bitmap != null){ if(bitmap.get() != null && !bitmap.get().isRecycled()){ bitmap.get().recycle(); bitmap = null; } }
2.对高分辨率图片进行压缩,详情参见高效加载Bitmap部分
3.关于ListView和GridView加载大量图片时的优化:
- 不要在getView方法中执行耗时操作,比如加载Bitmap,应将加载动作放到一个异步任务中,比如AsyncTask
- 在快速滑动列表的时候,停止加载Bitmap,当用户停止滑动时再去加载。因为当用户快速上下滑动时,如果去加载Bitmap的话可能会产生大量的异步任务,会造成线程池的拥堵以及大量的更新UI操作,因此会造成卡顿。
- 对当前的Activity开启硬件加速。
- 为防止因异步下载图片而造成错位问题,对ImageView设置Tag,将图片的Url作为tag的标记,当设置图片时,去判断当前ImageView的tag是否等于当前的图片的url,如果相当则显示否则的话不予加载。
参考链接:http://hukai.me/android-training-course-in-chinese/graphics/displaying-bitmaps/load-bitmap.html