前言 众所周知,每个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便是承担此职责。加载图片部分的代码如下: @Override public Bitmap loadBitmapImage(String path) { return BitmapFactory.decodeFile(path); } @Override public Drawable loadDrawableImage(String path) { return new BitmapDrawable(path); } 演示结果:在模拟器上图片只能加载1-3张,之后便会出现OOM错误;在Defy上不会出现错误;原因是两者内存限制不同,Defy上运行的是第三方ROM,内存分配有40MB。另外gallery每次显示一张图片时,都要重新解析获得一张图片,尽管在Defy上还未曾出错,但当图片量加大,GC回收不及时时,还是有可能出现OOM。 2.演示二 为图片加载的添加一个软引用缓存,每次图片从缓存中获取图片对象,若缓存中不存在,才会从Sdcard加载图片,并将该对象加入缓存。同时软引用的对象也有助于GC在内存不足的时候回收它们。ImageLoaderWithCache.class负责这个职责,关键代码如下: private HashMap<String, SoftReference<Bitmap>> mImageCache; @Override public Bitmap loadBitmapImage(String path) { if(mImageCache.containsKey(path)) { SoftReference<Bitmap> softReference = mImageCache.get(path); Bitmap bitmap = softReference.get(); if(null != bitmap) return bitmap; } Bitmap bitmap = BitmapFactory.decodeFile(path); mImageCache.put(path, new SoftReference<Bitmap>(bitmap)); return bitmap; } @Override public Drawable loadDrawableImage(String path) { return new BitmapDrawable(loadBitmapImage(path)); } 演示结果:在模拟器上,能不无缓存时多加载1-2张图片,但还是会出现OOM;在Defy上不曾出错。由于本次所用的图片都相对比较占内存,在GC还未来得及回收软引用对象时,就又要申请超出剩余量的内存空间,因此仍然没能完全避免OOM。如果换成加载大量的小图片,比如100*100规格的,缓存中软引用的作用可能就发挥出来了。(这一假设可以进一步试验证明一下) 3.演示三 为了进一步避免OOM,除了缓存,还可以对图片进行压缩,进一步节省内存,多数情况下调整图片大小并不会影响应用的表现力。ImageLoaderWithScale.class便是负责这个职责,调整大小的代码如下: BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); if (options.mCancel || options.outWidth == -1 || options.outHeight == -1) { Log.d(“OomDemo”, “alert!!!” + String.valueOf(options.mCancel) + ” ” + options.outWidth + options.outHeight); return null; } options.inSampleSize = Util.computeSampleSize(options, 600, (int) (1 * 1024 * 1024)); Log.d(“OomDemo”, “inSampleSize: ” + options.inSampleSize); options.inJustDecodeBounds = false; options.inDither = false; options.inPreferredConfig = Bitmap.Config.ARGB_8888; 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中,便增加了回收图片资源的方法: @Override public void releaseImage(String path) { if(mImageCache.containsKey(path)) { SoftReference<Bitmap> reference = mImageCache.get(path); Bitmap bitmap = reference.get(); if(null != bitmap) { Log.d(“OomDemo”, “recyling ” + path); bitmap.recycle(); } mImageCache.remove(path); } } 演示结果:图片压缩限制仍然维持在1*1024*1024,在adapter中,及时调用releaseImage方法,回收暂时不需要的图片。此时模拟器中也从未出现过OOM,所以总的来讲,综合缓存、调整大小、回收等各种手段,还是能够有效避免OOM的。 小结 本文介绍了软引用缓存、调整大小、回收等手段来避免OOM,总体来说效果还是明显的。但实际应用场景中,图片的应用不想本文所演示的那样简单,有时候图片资源可能来自与网络,这时需要配合异步加载的方式先下载图片并通过回调的方法来显示;有时候图片资源还需要加边框、加文字等额外修饰,所以在图片加载之后还要另做处理。 另外由于本人能力所限以及时间关系,本文还有诸多不完善之处。比如对Android内存分配的理解不深,没能透彻地解释Bitmap的内存占用情况;通过自定义堆内存分配大小,优化Dalvik虚拟机的堆内存分配的方法来解决OOM,本文也没有给予演示;再比如在上文的演示试验里,没有把内存占用情况的详细信息用图像形式直观地展示出来;还有演示所用的图片数量过少、规格单一、测试环境偏少,所有没能进行更加严谨科学的对比试验,遗漏了某些意外情况。最后欢迎大家来共同探索、交流并提出建议。 |