图片占用内存的处理在整个应用中是非常重要的,特别是 Dalvik 版本虚拟机在 GC 后不能压缩内存。也就是说,当对象被释放时,留下的空间不是连续的。如果需要显示一张更大的图片,即使剩余内存比图片占用内存大,但由于其不连续性,仍然会导致 GC。
一张图片占用的内存(BitMap) = width × height × 单位像素占用的字节数
在Android默认情况下,当图片文件解码成位图时,会被处理成32bit/像素。
红色、绿色、蓝色和透明通道各8bit,即使是没有透明通道的图片,如 jpeg 格式是没有透明通道的,但然后会处理成32bit位图,这样分配的32bit中的8bit透明通道数据是没有任何用处的,这完全没有必要,并且在这些图片被屏幕渲染之前,它们首先要被作为纹理传送到 GPU,这意味着每一张图片会同时占用 CPU 内存和 GPU 内存。
示例:一张 400 * 400 尺寸的图片,它所占用的空间计算为:
32bit * 400 * 400 = 625KB
Android提供了多种位图格式,根据开发者或产品需求,可以选择不同的格式解码图片:
最高的是RGB_8888,也就是系统默认的位图格式,其他几种都减小了位图通道位,可以减少内存开销并提升图片显示的性能。当然,这种空间的节省是要付出视觉质量受损的代价的,比如从RGB_8888改成使用RGB_565,会损失较多的图片数据,但不是不能用,根据不同场景可以选择不同的规格。
格式 | 每像素占用空间 | 说明 | 应用场景 |
---|---|---|---|
ARGB_8888 | 32 | Android 默认 | 图片质量要求较高 |
RGB_565 | 16 | 除了大图模式,一般都可以使用,并且几乎看不出差别 | 1.显示局部图片,比如列表中的图片 2.图片质量要求不高的场景 3.不需要透明通道 |
ARGB_4444 | 16 | 1.需要更小的格式,但又需要透明通道 2.视觉差异比较大,一般用于用户头像,特别是圆角 | |
ALPHA_8 | 8 | 1.主要用于Alpha通道模板,相当于作一个染色。 2.图像要渲染2次,虽然减少内存,但增加了绘制的开销。 |
如果内存中的图片大于屏幕显示出的图片大小,或者大于指定屏幕区域的大小,这些高分辨率图片会导致严重的性能问题。因此,需要改变内存的占用,避免此类问题。
这里的问题在于,占据内存中实际未使用的区块,这些图片占用了内存堆中的大量空间,使应用空间变少,然后重置这些图片大小,让它们符合实际显示的大小,既能减小内存的开销,也能提高显示的效率,这样载入内存的图片规格符合实际显示规格,而不是完整的分辨率。
位图功能对象中的inSampleSize属性实现了位图的缩放功能,代码如下:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
BitmapFactory.decodeStream(is, null, options);
将这个属性设置为1时,可以在不加载完整大小图片的前提下,生成一张只有原始图片部分大小的新图片,如inSampleSize为2时获得只有1/2大小的图片,同理设置为4就是1/4大小的图片。因此,图片大小总会比原始图片小一倍以上。
虽然 inSampleSize 可以实现图片的缩放,都是指数幂的缩放,如果想更细地缩放图片,就需要使用位图的 inScaled、inDensity 和 inTargetDensity 功能。
options.inScaled = true;
options.inDensity = srcWidth;
options.inTargetDensity = dstWidth;
当inScaled设置为true时,系统会按照现有的密度来划分目标密度,通过派生绽放数来应用到位图上,使用这个方法会重设图片大小,并对它应用一个新的过滤。
虽然这些方法都非常好用,并且减少图片显示需要的内存,但因为过多的算法,导致图片显示的过程需要更多的时间开销,如果图片很多的话,就影响到图片的显示效果。最好的方案是结合这两个方法,达到最佳的性能结合,首先使用 insamplsesie 处理图片,转换为接近目标的2次幂,然后用 inDensity 和 inTargetdensy 生成最终想要的准确大小,因为inSamplesize会减少像素的数量,而基于输出密码的需要对像素重新过滤。但获取资源图片的大小,需要设置位图对象的 inJustDecodeBounds 值为 true,然后继续解码图片文件,这样才能生成图片的宽高数据,并允许继续优化图片。
options.inJusDecodeBounds = true;
BitmapFactory.decodeStream(is, null, options);
options.inScaled = true;
options.inDensity = options.outWidth;
options.inSampleSize = 4;
options.inTargetDensity = dstWidth * options.inSampleSize;
options.inJusDecodeBounds = false;
BitmapFactory.decodeStream(is, null, options);
Android 3.0(API Level 11)引进了BitmapFactory.Options.inBitmap字段,如果设置了该属性,那么当使用了带有该Options参数的decode方法加载内容时,decode方法会尝试重用一个已经存在的位图。这就意味着位图内存已经被重用了,从而改善了性能,并且没有内存的分配和释放过程。
注意:新申请的Bitmap与旧的Bitmap必须有相同的解码格式,并且在Android 4.4之前,只能重用相同大小的Bitmap的内存区域,而Android 4.4之后可以重用任何bitmap的内存区域。
有几个非常好的开源库,如 Picasso、Glide,提供了更多、更优秀的处理位图的方式,并且实现了异步加载图片。
在 Android 单个进程分配有限的空间基础上,由于图片占用空间比较大,对图片的处理尤为重要。善用 Android 自带的优化方案,再结合一些优化的开源库,可以为内存的优化带来极大的帮助。