概述
如何高效地加载Bitmap?其实核心思想很简单,那就是采用BitmapFactory.Options加载所需尺寸的图片。有时候我们用ImageView加载图片,图片的原始尺寸远远大于ImageView。这个时候把图片完全加载进来没有必要,因为ImageView也显示不出来原始的图片。
我们可以使用BitmapFactory.Options对图片进行预加载,然后对图片进行压缩,将缩小后的图片放在ImageView中展示。这样提高了Bitmap加载的性能,一定程度上避免了OOM。
Bitmap加载图片
Bitmap的加载离不开BitmapFactory类,关于Bitmap官方介绍:
Creates Bitmap objects from various sources, including files, streams, and byte-arrays.
BitmapFactory类提供了四类方法用来加载Bitmap:
- decodeFile(),从文件系统加载。
- decodeResource(),资源文件中加载。
- decodeStream(),从输入流加载。
- decodeByteArray(),从字节数组中加载。
注意:查看源码可以发现,decodeFile()和decodeResource()间接调用decodeStream()。
Bitmap的内存位置
在Android3.0之前:Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中。
从Android3.0开始:Bitmap的内存就全部在Dalvik Heap里了 。
Bitmap的内存回收
在Android3.0之前,需要使用Bitmap.recycle()进行Bitmap的内存回收。
从Android3.0开始,不需要手动回收Bitmap了。
Bitmap的内存复用
从Android3.0开始,在Bitmap中引入了一个新的字段BitmapFactory.Options.inBitmap,设置此字段为true后,解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。
Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用。
从Android4.4(API 19)开始被复用的Bitmap的内存大于需要新申请内存的Bitmap的内存就可以了。
使用缓存
LruCache+DiskLruCache
出于对性能和app的考虑,我们肯定是想着第一次从网络中加载到图片之后,能够将图片缓存在内存和sd卡中,这样,我们就不用频繁的去网络中加载图片,可以很好地控制内存问题。
一般都会考虑使用LruCache+DiskLruCache,LruCache作为Bitmap在内存中的存放容器,在sd卡则使用DiskLruCache来统一管理磁盘上的图片缓存。
SoftReference+inBitmap
之前提到,可以采用LruCache作为存放Bitmap的容器,而在LruCache中有一个方法值得留意,那就是entryRemoved(),按照文档给出的说法,在LruCache容器满了需要淘汰存放其中的对象腾出空间的时候会调用此方法。
注意:这里只是对象被淘汰出LruCache容器,但并不意味着对象的内存会立即被Dalvik虚拟机回收掉。
此时可以在此方法中将Bitmap使用SoftReference包裹起来,并用事先准备好的一个HashSet容器来存放这些即将被回收的Bitmap,有人会问,这样存放有什么意义?
之前我们提到将inmutable设置为true,Bitmap的内存可以被复用,当然肯定要满足之前所说的条件。
解码方法对图片进行decode的时候会检查内存中是否有可复用的Bitmap,避免我们频繁地去SD卡上加载图片而造成系统性能的下降,毕竟从直接从内存中复用要比在SD卡上进行IO操作的效率要高很多。
Bitmap的像素格式
- ALPHA_8:颜色信息只由透明度组成,占8位。
- ARGB_4444:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。
- ARGB_8888:颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。
- RGB_565:颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。
通常我们优化Bitmap时,当需要做性能优化或者防止OOM,我们通常会使用RGB_565,因为ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。
Bitmap的内存计算
Bitmap类中有一个方法getByteCount():
/** |
还有一个方法getAllocationByteCount():
/** |
通过方法注释我们可以了解到,getByteCount()代表存储Bitmap的色素需要的最少内存,而getAllocationByteCount()代表在内存中为Bitmap分配的内存大小。
其实getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。从API19开始getAllocationByteCount()方法代替了getByteCount()。
一般情况下getByteCount()和getAllocationByteCount()是相等的。但是Bitmap内存如果复用之后,两者就不一样了。
通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小。
那getByteCount()和getAllocationByteCount()值是怎么计算出来的呢?
例子
下面就来举个例子计算理论上Bitmap加载一张图片时,所占内存的大小,和getByteCount()的结果比较一下。
假设
一张像素为522*686的PNG图片,把它放到drawable-xxhdpi目录下,在三星s6上加载,getByteCount()的结果是2547360B。
推导
第一步
默认的像素格式是ARGB_8888,之前已经说到了ARGB_8888格式下的一个像素点占用32位内存即4个字节。
所以结果是:int res = 522*686*4
,1432368。
显然和答案不一样啊。
第二步
假设中说把图片放到drawable-xxhdpi目录下,在三星s6上加载。并不是随口一说的,它们也是影响Bitmap所占内存大小的重要因素。
我们读取的是drawable目录下面的图片,用的是decodeResource方法,该方法本质上就两步:
读取原始资源,这个调用了Resource.openRawResource方法,这个方法调用完成之后会对TypedValue进行赋值,其中包含了原始资源的density等信息。
调用decodeResourceStream对原始资源进行解码和适配。这个过程实际上就是原始资源的density到屏幕density的一个映射。
原始资源的density其实取决于资源存放的目录(比如xxhdpi对应的是480),而屏幕density的值是和设备的硬件有关的,三星s6的值为640。加载时,原始的资源会自动进行缩放。
所以结果是:int res = (522 * 640 / 480) * (686 * 640 / 480) * 4
,2546432。
第三步
好像还是差那么一点,其实系统是进行了精度处理。
所以最终结果是:int res = (522 * 640 / 480f + 0.5) * (686 * 640 / 480f + 0.5) * 4
,2547360。
inScaled
上面说的缩放和一个参数inScaled有关:
public static class Options { |
如果inScaled设置为true,就缩放。设置为false,则不进行缩放。默认的值从上面代码可以看到,就是true。
缩放的比例为inTargetDensity / inDensity。
但是缩放也只针对资源文件有效,对于其他来源的图片不起效果,我们可以从源码中参数上的解释得知:
/**
* The pixel density to use for the bitmap. This will always result
* in the returned bitmap having a density set for it (see
* {@link Bitmap#setDensity(int) Bitmap.setDensity(int)}). In addition,
* if {@link #inScaled} is set (which it is by default} and this
* density does not match {@link #inTargetDensity}, then the bitmap
* will be scaled to the target density before being returned.
*
* <p>If this is 0,
* {@link BitmapFactory#decodeResource(Resources, int)},
* {@link BitmapFactory#decodeResource(Resources, int, android.graphics.BitmapFactory.Options)},
* and {@link BitmapFactory#decodeResourceStream}
* will fill in the density associated with the resource. The other
* functions will leave it as-is and no density will be applied.
*
* @see #inTargetDensity
* @see #inScreenDensity
* @see #inScaled
* @see Bitmap#setDensity(int)
* @see android.util.DisplayMetrics#densityDpi
*/
public int inDensity;
其中有一句The other functions will leave it as-is and no density will be applied
就是这个意思。
以上所说inDensity和inTargetDensity其实是DPI(dots per inch),关于DPI的概念请移步 全面理解Android中的Px,DPI,DIP,Density,Sp等概念。
结论
Bitmap加载资源文件在内存当中占用的大小取决于以下三点:
- 像素格式,前面我们已经提到,如果是ARGB8888那么就是一个像素4个字节,如果是RGB565那就是2个字节。
- 原始文件存放的资源目录(是hdpi还是xxhdpi)
- 目标屏幕的DPI(同等条件下,红米在资源方面消耗的内存肯定是要小于三星S6的)
Bitmap加载其他来源的图片,就和像素格式有关。
减少Bitmap内存占用
合理选择Bitmap的像素格式
不需要透明度的情况下,我们通常使用RGB_565。
使用采样
inSampleSize的值必须大于1时才会有效果,且采样率同时作用于宽和高。当inSampleSize=1时,采样后的图片为图片的原始大小。
当inSampleSize=n时,采样后的图片的宽高均为原始图片宽高的1/n,这时像素为原始图片的1/(nn),占用内存也为原始图片的1/(nn)。
inSampleSize的取值应该总为2的整数倍,否则会向下取整,取一个最接近2的整数倍,比如inSampleSize=3时,系统会取inSampleSize=2。
假设一张1024*1024
,模式为ARGB_8888的图片,inSampleSize=2,原始占用内存大小是4MB,采样后的图片占用内存大小就是(1024/2) * (1024/2 )* 4 = 1MB
。
inSampleSize
下面我们来介绍inSampleSize这个参数,当这个参数为1时,采样后的图片大小和原来一样;当这个参数为2时,采样后的图片宽高均为原来的1/2,大小也就成了原来的1/4。也就是说,采样后的大小等于原始大小除以采样率的平方。
官方文档规定,inSampleSize的值应为2的非负整数次幂(1,2,4,… ),否则会被系统向下取整并找到一个最接近的值。
通过设置inSampleSize我们就能够将图片缩放到一个合理的大小,那么该如何设置inSampleSize的值呢?
在讲解这个之前,我们先来考虑以下情况:我们的ImageView的大小为100 * 100,要显示的图片大小为300 * 400,此时我们应该将inSampleSize设为多少呢?
首先我们通过计算可以得到图片宽是ImageView的3倍,而图片高是ImageView的4倍。那么我们应该将图片宽高缩小为原来的4倍吗?假如我们把图片宽高都变为原来的1/4,那么现在图片大小为75 * 100,ImageView大小为100 * 100,图片要显示在ImageView中需要进行拉伸,而拉伸的话可能会导致图片失真。所以我们应该把图片宽高变为原来的1/3,以保证它不小于ImageView的大小,这样尽管多占用一些内存,但不会造成图片质量的下降,这还是很有必要的。
通过以上分析,我们知道了在设置inSampleSize时应该注意使得缩放后的图片大小不小于相应的ImageView大小。
计算inSampleSize的步骤
- 获取图片的原始宽高,通过将Options的inJustDecodeBounds属性设为true后调用decodeResource方法,可以实现不真正加载图片而只是获取图片的尺寸信息
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, |
- 根据原始宽高计算出inSampleSize
public static int calculateInSampleSize( |
图片的质量压缩
上述用inSampleSize压缩是尺寸压缩,Android中还有一种压缩方式叫质量压缩。质量压缩是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,经过它压缩的图片文件大小(kb)会有改变,但是导入成Bitmap后占得内存是不变的,宽高也不会改变。因为要保持像素不变,所以它就无法无限压缩,到达一个值之后就不会继续变小了。显然这个方法并不适用与缩略图,其实也不适用于想通过压缩图片减少内存的适用,仅仅适用于想在保证图片质量的同时减少文件大小的情况而已。
使用矩阵
我们之前使用inSampleSize对图片进行采样,采样之后内存是小了,可是图的尺寸也小了,我们要用Canvas绘制原始大小的图片该怎么办?就可以使用矩阵:
Matrix matrix = new Matrix(); |
这样,绘制出来的图就是放大以后的效果了,不过占用的内存却仍然是我们采样出来的大小。
如果我要把图片放到ImageView当中呢?一样可以,请看:
Matrix matrix = new Matrix(); |
参考:
1.Android坑档案:你的Bitmap究竟占多大内存?
2.Android性能优化:谈谈Bitmap的内存管理与优化
3.Android 之Bitmap
4.Android性能优化(五)之细说Bitmap
5.softReference+LruCache优化Android缓存
6.玩转Android Bitmap