Android开发中处理图片OOM (OutOfMemoryError) 的若干方法小结

Android开发中处理图片OOM的若干方法小结

众所周知,每个Android应用程序在运行时都有一定的内存限制,限制大小一般为16MB或24MB(视平台而定)。因此在开发应用时需要特别关注自身的内存使用量,而一般最耗内存量的资源,一般是图片、音频文件、视频文件等多媒体资源;由于Android系统对音频、视频等资源做了边解析便播放的处理,使用时并不会把整个文件加载到内存中,一般不会出现内存溢出(以下简称OOM)的错误,因此它们的内存消耗问题暂不在本文的讨论范围。本文重点讨论的是图片的内存消耗问题,如果你要开发的是一款图片浏览器应用,例如像Android系统自带的Gallery那样的应用,这个问题将变得尤为突出;如果你开发的是目前的购物客户端,有时候处理不当也会碰到这种问题。

目前碰到的OOM场景,无外乎以下几种情形,不过无论是哪种情形,解决问题的思路都是一致的。

(1)显示单张图片,图片文件体积达到3000*4000级别的时候;

(2)在ListView或Gallery等控件中一次性加载大量图片时;

相关知识介绍

1.颜色模型

常见的颜色模型有RGB、YUV、CMYK等,在大多数图像API中采用的都是RGB模型,Android也是如此;另外,在Android中还有包含透明度Alpha的颜色模型,即ARGB。关于颜色模型更加详细的信息暂不在本文的讨论范围之内。

2.计算机中颜色值的数字化编码

在不考虑透明度的情况下,一个像素点的颜色值在计算机中的表示方法有以下3种:

(1)浮点数编码:比如float: (1.0, 0.5, 0.75),每个颜色分量各占1个float字段,其中1.0表示该分量的值为全红或全绿或全蓝;

(2)24位的整数编码:比如24-bit:(255, 128, 196),每个颜色分量各占8位,取值范围0-255,其中255表示该分量的值为全红或全绿或全蓝;

(3)16位的整数编码:比如16-bit:(31, 45, 31),第1和第3个颜色分量各占5位,取值范围0-31,第2个颜色分量占6位,取值范围0-63;

在Java中,float类型的变量占32位,int类型的变量占32位,short和char类型的变量都在16位,因此可以看出,用浮点数表示法编码一个像素的颜色,内存占用量是96位即12字节;而用24位整数表示法编码,只要一个int类型变量,占用4个字节(高8位空着,低24位用于表示颜色);用16位整数表示法编码,只要一个short类型变量,占2个字节;因此可以看出采用整数表示法编码颜色值,可以大大节省内存,当然,颜色质量也会相对低一些。在Android中获取Bitmap的时候一般也采用整型编码。

以上2种整型编码的表示法中,R、G、B各分量的顺序可以是RGB或BGR,Android里采用的是RGB的顺序,本文也都是遵循此顺序来讨论。在24位整型表示法中,由于R、G、B分量各占8位,有时候业内也以RGB888来指代这一信息;类似的,在16位整型表示法中,R、G、B分量分别占5、6、5位,就以RGB565来指代这一信息。

现在再考虑有透明度的颜色编码,其实方式与无透明度的编码方式一样:24位整型编码RGB模型采用int类型变量,其闲置的高8位正好用于放置透明度分量,其中0表示全透明,255表示完全不透明;按照A、R、G、B的顺序,就可以以ARGB8888来概括这一情形;而16位整型编码的RGB模型采用short类型变量,调整各分量所占为数分别至4位,那么正好可以空出4位来编码透明度值;按照A、R、G、B的顺序,就可以以ARGB4444来概括这一情形。回想一下Android的BitmapConfig类中,有ARGB_8888、ARGB_4444、RGB565等常量,现在可以知道它们分别代表了什么含义。同时也可以计算一张图片在内存中可能占用的大小,比如采用ARGB_8888编码载入一张1920*1200的图片,大概就会占用1920*1200*4/1024/1024=8.79MB的内存。

3.Bitmap在内存中的存储区域

http://www.7dot9.com/2010/08/android-bitmap%E5%86%85%E5%AD%98%E9%99%90%E5%88%B6/ 一文中对Android内存限制问题做了一些探讨,作者认为Bitmap对象通过栈上的引用来指向堆上的Bitmap对象,而Bitmap对象又对应了一个使用了外部存储的native图像,实际上使用的是byte[]来存储的内存空间。但为了确保外部分配内存成功,应该保证当前已分配的内存加上当前需要分配的内存值,大小不能超过当前堆的最大内存值,而且内存管理上将外部内存完全当成了当前堆的一部分。

4.Java对象的引用类型

(1)强引用(StrongReference)如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

(2)软引用(SoftReference)如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

(3)弱引用(WeakReference)弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

(4)虚引用(PhantomReference)“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。

解决OOM的常用方案

内存限制是Android对应用的一个系统级限制,作为应用层开发人员,没有办法彻底去消灭这个限制,但是可以通过一些手段去合理使用内存,从而规避这个问题。以下是个人总结的一些常用方法:

(1)缓存图像到内存,采用软引用缓存到内存,而不是在每次使用的时候都从新加载到内存;

(2)调整图像大小,手机屏幕尺寸有限,分配给图像的显示区域本身就更小,有时图像大小可以做适当调整;

(3)采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省内存;

(4)及时回收图像,如果引用了大量Bitmap对象,而应用又不需要同时显示所有图片,可以将暂时用不到的Bitmap对象及时回收掉;

(5)自定义堆内存分配大小,优化Dalvik虚拟机的堆内存分配;

本文主要将对前面4种方式做演示和分析。

演示试验说明

为了说明出现OOM的场景和解决OOM的方法,本人制作了一个Android应用——OomDemo来演示,此应用的基本情况说明如下:

(1)该应用展示一个gallery,该gallery只加载图片,gallery的adapter中传入图片的路径而不是图片对象本身,adapter动态加载图片;

(2)演示所用的图片预存储到sdcard的cache目录下,文件名分别为a.jpg,b.jpg…r.jpg,总共18张;

(3)图片为规格1920*1200的jpg图片,文件大小在423KB-1.48MB范围内;

(4)运行环境:模拟器——android2.2版本系统——480*320屏幕尺寸;Moto Defy——2.3.4版本CM7系统——854*480屏幕尺寸;

(5)程序基本结构图:

 

演示结果与说明

1.演示一

首先采用最简单的图片加载方式,不带任何图片缓存、调整大小或者回收,SimpleImageLoader.class便是承担此职责。加载图片部分的代码如下:

[java]  view plain copy
  1. @Override  
  2.   
  3. public Bitmap loadBitmapImage(String path) {  
  4.   
  5.        return BitmapFactory.decodeFile(path);  
  6.   
  7. }  
  8.   
  9. @Override  
  10.   
  11. public Drawable loadDrawableImage(String path) {  
  12.   
  13.        return new BitmapDrawable(path);  
  14.   
  15. }  

 

演示结果:在模拟器上图片只能加载1-3张,之后便会出现OOM错误;在Defy上不会出现错误;原因是两者内存限制不同,Defy上运行的是第三方ROM,内存分配有40MB。另外gallery每次显示一张图片时,都要重新解析获得一张图片,尽管在Defy上还未曾出错,但当图片量加大,GC回收不及时时,还是有可能出现OOM。

2.演示二

为图片加载的添加一个软引用缓存,每次图片从缓存中获取图片对象,若缓存中不存在,才会从Sdcard加载图片,并将该对象加入缓存。同时软引用的对象也有助于GC在内存不足的时候回收它们。ImageLoaderWithCache.class负责这个职责,关键代码如下:

 

[java]  view plain copy
  1. private HashMap<String, SoftReference<Bitmap>> mImageCache;  
  2.   
  3.        @Override  
  4.   
  5.        public Bitmap loadBitmapImage(String path) {  
  6.   
  7.               if(mImageCache.containsKey(path)) {  
  8.   
  9.                      SoftReference<Bitmap> softReference = mImageCache.get(path);  
  10.   
  11.                      Bitmap bitmap = softReference.get();  
  12.   
  13.                      if(null != bitmap)  
  14.   
  15.                             return bitmap;  
  16.   
  17.               }  
  18.   
  19.               Bitmap bitmap = BitmapFactory.decodeFile(path);  
  20.   
  21.               mImageCache.put(path, new SoftReference<Bitmap>(bitmap));  
  22.   
  23.               return bitmap;  
  24.   
  25.        }  
  26.   
  27.        @Override  
  28.   
  29.        public Drawable loadDrawableImage(String path) {  
  30.   
  31.               return new BitmapDrawable(loadBitmapImage(path));  
  32.   
  33.        }  

 

演示结果:在模拟器上,能不无缓存时多加载1-2张图片,但还是会出现OOM;在Defy上不曾出错。由于本次所用的图片都相对比较占内存,在GC还未来得及回收软引用对象时,就又要申请超出剩余量的内存空间,因此仍然没能完全避免OOM。如果换成加载大量的小图片,比如100*100规格的,缓存中软引用的作用可能就发挥出来了。(这一假设可以进一步试验证明一下)

3.演示三

为了进一步避免OOM,除了缓存,还可以对图片进行压缩,进一步节省内存,多数情况下调整图片大小并不会影响应用的表现力。ImageLoaderWithScale.class便是负责这个职责,调整大小的代码如下: 

 

[java]  view plain copy
  1. BitmapFactory.Options options = new BitmapFactory.Options();  
  2.   
  3.        options.inJustDecodeBounds = true;  
  4.   
  5.        BitmapFactory.decodeFile(path, options);   
  6.   
  7.        if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) {  
  8.   
  9.               Log.d(“OomDemo”, “alert!!!” + String.valueOf(options.mCancel) + ” ” + options.outWidth + options.outHeight);  
  10.   
  11.               return null;  
  12.   
  13.        }  
  14.   
  15.        options.inSampleSize = Util.computeSampleSize(options, 600, (int) (1 * 1024 * 1024));  
  16.   
  17.        Log.d(“OomDemo”, “inSampleSize: ” + options.inSampleSize);  
  18.   
  19.        options.inJustDecodeBounds = false;  
  20.   
  21.        options.inDither = false;  
  22.   
  23.        options.inPreferredConfig = Bitmap.Config.ARGB_8888;  
  24.   
  25.        Bitmap bitmap = BitmapFactory.decodeFile(path, options);   

 

演示结果:在上述代码中,首先解码图片的边界,在不需要得到Bitmap对象的前提下就能获得图像宽高(宽高值分别被设置到options.outWidth和options.outHeight两个属性中)。computeSampleSize这个方法的参数分别为“解析图片所需的BitmapFactory.Options”、“调整后图片最小的宽或高值”、“调整后图片的内存占用量上限”。结合原始图片的宽高,此方法可以计算得到一个调整比例,再用此比例调整原始图片并加载到内存中,此时图片所消耗的内存不会超出事先指定的大小。在模拟器中,限制图片所占内存大小为1*1024*1024时,比未压缩过时能加载更多图片,但仍然会出现OOM;若限制图片所占内存大小为0.5*1024*1024,则能完整的载入所有图片。所以调整图片大小还是能够有效节省内存的。在Defy中不会出错,原因同上。

4.演示四

在有些情况下,严重缩小图片还是会影响应用的显示效果的,所以有必要在尽可能少地缩小图片的前提下展示图片,此时手动去回收图片就变得尤为重要。在类ImageLoaderWithRecyle.class中,便增加了回收图片资源的方法:

[java]  view plain copy
  1. @Override  
  2.   
  3.        public void releaseImage(String path) {  
  4.   
  5.               if(mImageCache.containsKey(path)) {  
  6.   
  7.                      SoftReference<Bitmap> reference = mImageCache.get(path);  
  8.   
  9.                      Bitmap bitmap = reference.get();  
  10.   
  11.                      if(null != bitmap) {  
  12.   
  13.                             Log.d(“OomDemo”, “recyling ” + path);  
  14.   
  15.                             bitmap.recycle();  
  16.   
  17.                      }  
  18.   
  19.                      mImageCache.remove(path);  
  20.   
  21.               }  
  22.   
  23.        }  

 

演示结果:图片压缩限制仍然维持在1*1024*1024,在adapter中,及时调用releaseImage方法,回收暂时不需要的图片。此时模拟器中也从未出现过OOM,所以总的来讲,综合缓存、调整大小、回收等各种手段,还是能够有效避免OOM的。

小结

本文介绍了软引用缓存、调整大小、回收等手段来避免OOM,总体来说效果还是明显的。但实际应用场景中,图片的应用不想本文所演示的那样简单,有时候图片资源可能来自与网络,这时需要配合异步加载的方式先下载图片并通过回调的方法来显示;有时候图片资源还需要加边框、加文字等额外修饰,所以在图片加载之后还要另做处理。

另外由于本人能力所限以及时间关系,本文还有诸多不完善之处。比如对Android内存分配的理解不深,没能透彻地解释Bitmap的内存占用情况;通过自定义堆内存分配大小,优化Dalvik虚拟机的堆内存分配的方法来解决OOM,本文也没有给予演示;再比如在上文的演示试验里,没有把内存占用情况的详细信息用图像形式直观地展示出来;还有演示所用的图片数量过少、规格单一、测试环境偏少,所有没能进行更加严谨科学的对比试验,遗漏了某些意外情况。最后欢迎大家来共同探索、交流并提出建议。

 

原文出处:http://www.longerian.me/?p=28

作者分析很全面写得很棒,我因最近被OOM搞烦了,找到这个帖子顿时豁然开朗。

另外参考上面的解决方式,便有下面完整代码试例

[java]  view plain copy
  1. public class BitmapCache {  
  2.     static private BitmapCache cache;  
  3.     /** 用于Chche内容的存储 */  
  4.     private Hashtable<Integer, MySoftRef> hashRefs;  
  5.     /** 垃圾Reference的队列(所引用的对象已经被回收,则将该引用存入队列中) */  
  6.     private ReferenceQueue<Bitmap> q;  
  7.   
  8.     /** 
  9.      * 继承SoftReference,使得每一个实例都具有可识别的标识。 
  10.       */  
  11.     private class MySoftRef extends SoftReference<Bitmap> {  
  12.         private Integer _key = 0;  
  13.   
  14.         public MySoftRef(Bitmap bmp, ReferenceQueue<Bitmap> q, int key) {  
  15.             super(bmp, q);  
  16.             _key = key;  
  17.         }  
  18.     }  
  19.   
  20.     private BitmapCache() {  
  21.         hashRefs = new Hashtable<Integer, MySoftRef>();  
  22.         q = new ReferenceQueue<Bitmap>();  
  23.     }  
  24.   
  25.     /** 
  26.      * 取得缓存器实例 
  27.       */  
  28.     public static BitmapCache getInstance() {  
  29.         if (cache == null) {  
  30.             cache = new BitmapCache();  
  31.         }  
  32.         return cache;  
  33.     }  
  34.   
  35.     /** 
  36.      * 以软引用的方式对一个Bitmap对象的实例进行引用并保存该引用 
  37.       */  
  38.     private void addCacheBitmap(Bitmap bmp, Integer key) {  
  39.         cleanCache();// 清除垃圾引用  
  40.          MySoftRef ref = new MySoftRef(bmp, q, key);  
  41.         hashRefs.put(key, ref);  
  42.     }  
  43.   
  44.     /** 
  45.      * 依据所指定的drawable下的图片资源ID号(可以根据自己的需要从网络或本地path下获取),重新获取相应Bitmap对象的实例 
  46.      */  
  47.     public Bitmap getBitmap(int resId, Context context) {  
  48.         Bitmap bmp = null;  
  49.         // 缓存中是否有该Bitmap实例的软引用,如果有,从软引用中取得。  
  50.          if (hashRefs.containsKey(resId)) {  
  51.             MySoftRef ref = (MySoftRef) hashRefs.get(resId);  
  52.             bmp = (Bitmap) ref.get();  
  53.         }  
  54.         // 如果没有软引用,或者从软引用中得到的实例是null,重新构建一个实例,  
  55.          // 并保存对这个新建实例的软引用  
  56.          if (bmp == null) {  
  57.             // 传说decodeStream直接调用JNI>>nativeDecodeAsset()来完成decode,  
  58.               // 无需再使用java层的createBitmap,从而节省了java层的空间。  
  59.               bmp = BitmapFactory.decodeStream(context.getResources()  
  60.                     .openRawResource(resId));  
  61.             this.addCacheBitmap(bmp, resId);  
  62.         }  
  63.         return bmp;  
  64.     }  
  65.   
  66.     private void cleanCache() {  
  67.         MySoftRef ref = null;  
  68.         while ((ref = (MySoftRef) q.poll()) != null) {  
  69.             hashRefs.remove(ref._key);  
  70.         }  
  71.     }  
  72.   
  73.     /** 
  74.      * 清除Cache内的全部内容 
  75.      */  
  76.     public void clearCache() {  
  77.         cleanCache();  
  78.         hashRefs.clear();  
  79.         System.gc();  
  80.         System.runFinalization();  
  81.     }  
  82. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值