Bitmap与OOM

Bitmap所造成的OOM

图片是一个很耗内存的资源,因此经常会遇到OOM。比如从本地文件中读取图片,然后在GridView中显示出来,如果不做处理,OOM就极有可能发生。

Bitmap引起OOM的原因:

1.图片使用完成后,没有及时的释放,导致Bitmap占用的内存越来越大,而安卓提供给Bitmap的内存是有一定限制的,当超出该内存时,自然就发生了OOM

2.图片过大

这里的图片过大是指加载到内存时所占用的内存,并不是图片自身的大小。而图片加载到内存中时所占用的内存是根据图片的分辨率以及它的配置(ARGB值)计算的。举个例子:

假如有一张分辨率为2048x1536的图片,它的配置为ARGB_8888,那么它加载到内存时的大小就是2048x1526x4/1024/1024=12M.,因此当将这张图片设置到ImageView上时,将会出现OOM(超过了Android分配给Bitmap的上限8M)。

补充:ARGB表示图片的配置,分表代表:透明度、红色、绿色和蓝色。这几个参数的值越高代表图像的质量越好,那么也就越占内存。就拿ARGB_8888来说,A、R、G、B这几个参数分别占8位,那么总共占32位,代表一个像素点占32位大小即4个字节,那么一个100x100分辨率的图片就占了100x100x4/1024/1024=0.04M的大小的空间。

高效加载Bitmap

当将一个图片加载到内存,在UI上呈现时,需要考虑一下几个因素:

1.预计加载完整张图片所需要的内存空间

2.呈现这张图片时控件的大小

3.屏幕大小与屏幕像素密度

如果我们要加载的图片的分辨率比较大,而呈现它的控件(比如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;
}

关于采样率与图片分辨率压缩大小的关系:

1.如果inSample=1则表明与原图一样
2.如果inSample=2则表示宽和高均缩小为1/2
3.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 cacheSize = maxMemory/8;
    mMemoryCache = new LruCache<String,Bitmap>(cacheSize) {
        //计算缓存对象的大小
        @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.java

DiskLruCache的创建

public static DiskLruCache open(File directory,int appVersion,int valueCount,long maxSize)  

DiskLruCache不能通过构造方法来创建,而是用open方法,它有四个参数:
1.directory:表示磁盘缓存的文件路径可以选择Sd卡上的缓存目录:/sdcard/Android/data/package_name/cache,也可以选择其他的目录作为缓存目录,如果希望保留的缓存数据在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;
    bitmap = new SoftReference<Bitmap>(pBitmap);
       if(bitmap != null){  
       if(bitmap.get() != null && !bitmap.get().isRecycled()){ 
           bitmap.get().recycle(); 
           bitmap = null;  
       } 
   }

2.对高分辨率图片进行压缩,详情参见高效加载Bitmap部分

3.关于ListView和GridView加载大量图片时的优化:

 3.1. 不要在getView方法中执行耗时操作,比如加载Bitmap,应将加载动作放到一个异步任务中,比如AsyncTask
 3.2. 在快速滑动列表的时候,停止加载Bitmap,当用户停止滑动时再去加载。因为当用户快速上下滑动时,如果去加载Bitmap的话可能会产生大量的异步任务,会造成线程池的拥堵以及大量的更新UI操作,因此会造成卡顿。
 3.3 对当前的Activity开启硬件加速。
 3.4 为防止因异步下载图片而造成错位问题,对ImageView设置Tag,将图片的Url作为tag的标记,当设置图片时,去判断当前ImageView的tag是否等于当前的图片的url,如果相当则显示否则的话不予加载。

参考链接: http://hukai.me/android-training-course-in-chinese/graphics/displaying-bitmaps/load-bitmap.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容