Android 开发中经常考虑的一个问题就是 OOM(Out Of Memory),也就是内存溢出,一方面大量加载图片时有可能出现 OOM, 通过采样压缩图片可避免 OOM,另一方面,如一张 1024 x 768 像素的图像被缩略显示在 128 x 96 的 ImageView 中,这种做法显然是不值得的,可通过采样加载一个合适的缩小版本到内存中,以减小内存的消耗,Bitmap 的优化主要有两个方面如下:
- 有效的处理较大的图
- 缓存位图
这篇文章主要侧重于如何有效的处理较大的位图。
此外,在 Android 中按照位图采样的方法加载一个缩小版本到内存中应该考虑因素?
- 估计加载完整图像所需要的内存
- 加载这个图片所需的空间带给其程序的其他内存需求
- 加载图片的目标 ImageView 或 UI 组件的尺寸
- 当前设备的屏幕尺寸或密度
位图采样
图像有不同的形状的和大小,读取较大的图片时会耗费内存。读取一个位图的尺寸和类型,为了从多种资源创建一个位图,BitmapFactory 类提供了许多解码的方法,根据图像数据资源选择最合适的解码方法,这些方法试图请求分配内存来构造位图,因此很容易导致 OOM 异常。每种类型的解码方法都有额外的特征可以让你通过 BitMapFactory.Options 类指定解码选项。当解码时设置 inJustDecodeBounds 为true,可在不分配内存之前读取图像的尺寸和类型,下面的代码实现了简单的位图采样:
|
|
注意:其他 decode… 方法与 decodeResource 类似,这里都以 decodeRedource 为例。
实际使用时,必须根据具体的宽高要求计算合适的 inSampleSize 来进行位图的采样,比如,将一个分辨率为 2048 x 1536 的图像使用 inSampleSize 值为 4 去编码产生一个 512 x 384 的图像,这里假设位图配置为 ARGB_8888,加载到内存中仅仅是 0.75M 而不是原来的 12M,关于图像所占内存的计算将在下文中介绍,下面是根据所需宽高进行计算采样比例的计算方法:
|
|
获得采样比例之后就可以根据所需宽高处理较大的图片了,下面是根据所需宽高计算出来的 inSampleSize 对较大位图进行采样:
|
|
在解码过程中使用了 BitmapFactory.decodeResource() 方法,具体如下:
|
|
显然真正解码的方法应该是 decodeResourceStream() 方法,具体如下:
|
|
设置完 inDensity 和 inTargetDensity 之后调用了 decodeStream() 方法,该方法返回完全解码后的 Bitmap 对象,具体如下:
|
|
显然,decodeStream() 方法主要调用了本地方法完成 Bitmap 的解码,跟踪源码发现 nativeDecodeAsset() 和 nativeDecodeStream() 方法都调用了 dodecode() 方法,doDecode 方法关键代码如下:
|
|
上面代码能看到缩放比例的计算,以及 density 与 targetDensity 对 Bitmap 宽高的影响,实际上间接影响了 Bitmap 在所占内存的大小,这个问题会在下文中举例说明,注意 density 与当前 Bitmap 所对应资源文件(图片)的目录有关,如有一张图片位于 drawable-xxhdpi 目录中,其对应的 Bitmap 的 density 为 480dpi,而 targetDensity 就是 DisPlayMetric 的 densityDpi,也就是手机屏幕代表的 density。那么怎么查看 Android 中本地的 native 方法的实现呢,链接如下:
BitmapFactory.cpp,直接搜索 native 方法的方法名即可,可以试一下咯。
Bitmap 内存计算
首先贡献一张大图 6000 x 4000 ,图片接近 12M,【点击获取】 当直接加载这张图片到内存中肯定会发生 OOM,当然通过适当的位图采样缩小图片可避免 OOM,那么 Bitmap 所占内存又如何计算呢,一般情况下这样计算:
|
|
可使用 bitmap.getConfig() 获取 Bitmap 的格式,这里是 ARGB_8888 ,这种 Bitmap 格式下一个像素点占 4 个字节,所以要 x 4,如果将图片放置在 Android 的资源文件夹中,计算方式如下:
$$scale = \frac{targetDensity}{density}$$
$$widthPix = originalWidth \times scale$$
$$heightPix = orignalHeight \times scale$$
$$Bitmap Memory = widthPix \times scale \times heightPix \times scale \times 4$$
上述简单总结了一下 Bitmap 所占内存的计算方式,验证时可使用如下方法获取 Bitmap 所占内存大小:
由于选择的这张图片直接加载会导致 OOM,所以下文的事例中都是先采样压缩,然后在进行 Bitmap 所占内存的计算。
直接采样
这种方式就是直接指定采样比例 inSampleSize 的值,然后先采样然后计算采样后的内存,这里指定 inSampleSize 为200。
- 将该图片放在 drawable-xxhdpi 目录中,此时 drawable-xxhdpi 所代表的 density 为 480(density),我的手机屏幕所代表的 density 是 480(targetDensity),显然,此时 scale 为1,当然首先对图片进行采样,然后将图片加载到内存中, 此时 Bitmap 所占内存内存为:
$$inSampleSize = 200$$
$$scale = \frac{targetDensity}{density} = \frac{480}{480} = 1$$
$$widthPix = orignalScale \times scale = \frac{6000}{200} \times 1 = 30$$
$$heightPix = orignalHeight \times scale =\frac{4000}{200} \times 1 = 20$$
$$BitmapMemory = widthPix \times heightPix \times 4 = 30 \times 20 \times 4 = 2400(Byte)$$
- 将图片放在 drawable-xhdpi 目录中,此时 drawable-xhdpi 所代表的 density 为 320,我的手机屏幕所代表的 density 是 480(targetDensity),将图片加载到内存中,此时 Bitmap 所代表的内存为:
$$inSampleSize = 200$$
$$scale = \frac{targetDensity}{density} = \frac{480}{320}$$
$$widthPix = orignalWidth \times scale = \frac{6000}{200} \times \frac{480}{320} = 45$$
$$heightPix = orignalHeight \times scale = \frac{4000}{200} \times \frac{480}{320} = 30$$
$$BitmapMemory = widthPix \times scale \times heightPix \times scale \times 4 = 45 \times 30 \times 4 = 5400(Byte)$$
计算采样
这种方式就是根据请求的宽高计算合适的 inSampleSize,而不是随意指定 inSampleSize,实际开发中这种方式最常用,这里请求宽高为100x100,具体 inSampleSize 计算在上文中已经说明。
- 将图片放在 drawable-xxhdpi 目录中,此时 drawable-xxhdpi 所代表的 density 为 480,我的手机屏幕所代表的 density 是 480(targetDensity),将图片加载到内存中,此时 Bitmap 所代表的内存为:
$$inSampleSize = \frac{4000}{100} = 40$$
$$scale = \frac{targetDensity}{density} = \frac{480}{480} = 1$$
$$widthPix = orignalWidth \times scale = \frac{6000}{40} \times 1= 150$$
$$heightPix = orignalHeight \times scale = \frac{4000}{40} \times 1 = 100$$
$$BitmapMemory = widthPix \times scale \times heightPix \times scale \times 4 = 60000(Byte)$$
- 将图片放在 drawable-xhdpi 目录中,此时 drawable-xhdpi 所代表的 density 为 320,我的手机屏幕所代表的 density 是 480(targetDensity),将图片加载到内存中,此时 Bitmap 所代表的内存为:
$$inSampleSize = \frac{4000}{100} = 40$$
$$scale = \frac{targetDensity}{density}= \frac{480}{320}$$
$$widthPix = orignalWidth \times scale = \frac{6000}{40} \times \frac{480}{320}=225$$
$$heightPix = orignalHeight \times scale=\frac{4000}{40} \times \frac{480}{320}=150$$
$$BitmapMemory=widthPix \times heightPix \times 4=225 \times 150 \times 4=135000(Byte)$$
位图采样及 Bitmap 在不同情况下所占内存的计算大概过程如上所述。
测试效果
测试效果图参考如下:
drawable-xhdpi | drawable-xxhdpi |
---|---|