现在应用很难离开图片,就像美女上网不能少“美颜”工具一样。以前的手机内存空间比较小,每个进程能使用的堆内存往往只有32M(有的只有16M,如G1手机),所以会发现加几张大图之后,系统一言不合就给你个OutOfMemoryError。不过,现在的手机一般堆内存都比较多了(看过一些手机基本上都接近200M),其实对于现在的APK动不动就几十M以上也不算特别富余,内存的使用还是要谨慎合理,特别是在处理图片的时候。
面试题:有没有遇到Bitmap OOM的问题?如何优化图片占用的内存空间?
我觉得在回答上述问前,我们首先要回答一个问题:
Android系统上的图片指的是什么?
首先看Android支持的图片格式:
Android支持JPEG和PNG格式的图片,4.0之后添加了WEBP图片的支持(不过有些手机产商并不一定支持,如小米2S 4.1的SDK版本,但还是显示不了WEBP格式),5.0之后增加了对GIF和BMP图片的支持。
其实图片最终要显示在屏幕上,都会对应一个屏幕上的点,即对应一个颜色值。不同格式的图片,只是不同压缩编码和解压算法。也就是说,我们看到的.jpg、.png图片的文件大小只有几十KB,担把它们加载到内存中时,每张图片最终都按长X宽展开,计算其占用内存大小的就变成了(ARGB_8888格式的图片,每像素占用 4 Byte,而 RGB565则是 2 Byte,假设是ARGB_8888):
内存占用=长 X 宽 X 4bytes
这种算法其这还忽略屏幕的Density,如我们上Day 21的面试题中也提到过的,放在不同的drawable目录中的图片显示时会根据Denisty有一定的缩放。所以有时候图片占用的内存会比我们上面公式计算出来的还要大很多。
Bitmap和Drawable
Bitmap是Android系统中的图像处理的最重要类。可以简单地说,Bitmap代表的是图片资源在内存中的数据结构,如它的像素数据,长宽等属性都存放在Bitmap对象中。Bitmap类的构造函数是私有的,只能是通过JNI实例化,系统提供BitmapFactory工厂类给我们从从File、Stream和byte[]创建Bitmap的方式。
Drawable官文文档说明为可绘制物件的一般抽象。View也是可以绘制的,但Drawable与View不同,Drawable不接受事件,无法与用户进行交互。我们知道很多UI控件都提供设置Drawable的接口,如ImageView可以通过setImageDrawable(Drawable drawable)设置它的显示,这个drawable可以是来自Bitmap的BitmapDrawable,也可以是其他的如ShapeDrawable。
也就是Drawable是一种抽像,最终实现的方式可以是绘制Bitmap的数据或者图形、Color数据等。理解了这些,你很容易明白为什么我们有时候需要进行两者之间的转换。
优化手段
在Android 3.0之前的版本,Bitmap像素数据存放在Native内存中,而且Nativie内存的释放是不确定的,容易内存溢出而Crash,所以一般我们不使用的图片要调用recycle()。
从3.0开始,Bitmap像素数据和Bitmap对象一起存放在Dalvik堆内存中(中从源代码上看是多了一个byte[] buffer用来存放数据),也就是我们常说的Java Heap内存。
除了这点改变之外,3.0版本的还增加了一个inBitmap属性(BitmapFactory.Options.inBitmap)。如果设置了这个属性则会重用这个Bitmap的内存从而提升性能。但是这个重用是有条件的,在Android4.4之前只能重用相同大小的Bitmap,Android4.4+则只要比重用Bitmap小即可。
当然优化的手段还有很多,比如使用采样率(inSampleSize),如果最终要压缩图片,如显示缩列图,我们并不需要加载完整的图片数据,只需要按一定的比例加载即可;使用Matrix变形等,比如使用Matrix进行放大,虽然图像大了,但并没有占用更多的内存。
使用第三方图片库也是一种优化吧,它们帮我们完成了很多工作。Facebook的Fresco还自己开辟了Native内存用于存储图片,以得到更大的内存空间(其实我还不确认这种方式是否有兼容性问题)。
要加载很大的图片怎么办?
如果图片很大,比如他们的占用内存算下来就直接OOM了,那么我们肯定不能直接加载它。解决主法还是有很多的,系统也给我们提供了一个类BitmapRegionDecoder,可以用来分块加载图片。
小结
虽然网上有很多开源的和图片相关的库,但我们还是有必要了解一下Bitmap的相关的知识的,有时候我们只需要一个简单的功能,可以手动自己实现,不一定非要加进一个开源库。代码越是简单,可控性和可维护性越好,对吧。