Android Bitmap
占用内存大小、优化方案等等。
BitmapFactory.Options
BitmapFactory
在解码生成 Bitmap
时,会通过 Options
的各种参数来做控制。
1 | public static class Options { |
重要属性
inJustDecodeBounds
设置为true
时,不会为图片分配内存,解码时返回为null
没有bitmap
,但是会设置Options.out***
相关属性;即将被解码图片的属性读出来,而不生成Bitmap
也不加载到内存。
在被读取图片尺寸很大或者存储大小非常大时,通常通过该属性预读取图片相关参数,避免直接加载引起OOM
。inSampleSize
采样率,值为最接近接近 2 的指数幂。即采样后得到的图片大小为原始宽高的1/inSampleSize
。比如原始图片大小为100*100
,采样率为 4,最终生成大小为25*25
,整个像素大小减小到1/16
。常用于压缩大尺寸图片。inPreferredConfig
色彩模式,默认为ARGB_8888
,即 4 字节存储。可以在解码前通过设置Config
来决定使用几字节存储。常用于压缩图片存储空间,采用RGB_565
模式。inMutable
是否可操作标记。比如返回的Bitmap
会通过Canvas
重新绘制,必须设置为true
,否则会抛出异常Immutable bitmap passed to Canvas constructor
。inScaled, inDensity, inTargetDensity
表示图片是否被缩放;如果设置为缩放,缩放比例为scale = inTargetDensity/inDensity
,其中inTargetDensity
表示当前显示图片手机的密度,inDensity
为资源文件所在drawable-
目录对应的密度;通常只有从资源文件中加载Bitmap
时,这几个字段才会生效;如果是直接从Asserts
或者本地文件等等中加载,是不会有影响的。outWidth, outHeight
表示解码一张图片时,获取图片原始的宽、高、类型等信息。
色彩模式 Bitmap.Config
色彩模式,表示 Bitmap
由哪些颜色组成,并占用几位内存:
1 | public enum Config { |
常见色彩模式及对应描述:
Config | 占用内存字节(byte) | 描述 |
---|---|---|
ALPHA_8 | 1 | 单透明通道,只由透明度组成 |
RGB_565 | 2 | 由 RGB 三色组成,分别占 5, 6, 5 位 |
ARGB_4444 | 4 | 已废弃 |
ARGB_8888 | 4 | 24 位真彩色,由透明度 RGB 四部分组成 |
RGBA_F16 | 8 | Android8.0新增(更丰富的色彩表现HDR) |
HARDWARE | Special | Android 8.0 新增 (Bitmap直接存储在graphic memory) |
密度 Density
当前设备密度
当前设备密度主要由显示屏的参数等决定,通常显示设备 LCD
的规格书中,会明确指出屏幕尺寸( 3.5
寸)、分辨率 320*480
、能显示多少种颜色 65K colors
等等,但并没有给出具体的 dpi, density, densityDpi
;这些密度相关值都是在软件代码中设置的,而软件的主要依据是屏幕分辨率的宽度。Android
中通常是使用 ro.sf.lcd_density
属性来记录当前显示屏对应的密度值的,可以在下列任意一个文件中设置(高通平台):
1 | device/qcom/common/init/init_msm***.c |
以 init_msm8916.c
文件为例:
1 |
|
以 init.qcom.early_boot.sh
文件为例:
1 | // 从 virtual_size 读取分辨率,并获取宽度值 |
在 DisplayMetrics
中,默认的 density, densityDpi
都是根据这个属性来设置默认值的:
1 | public class DisplayMetrics { |
结论:
Android
系统会根据显示设备分辨率的宽度,来设置对应的lcd_density
属性,DisplayMetrics
中会根据这个属性值来设置系统的density, densityDpi
,而这两个值决定了资源文件中位图缩放的比例。
资源文件对应密度
资源文件存放在不同的目录 mdpi, hdpi
等,对应的密度信息如下:
DensityDpi | res | Density |
---|---|---|
120dpi | ldpi | 0.75 |
160dpi | mdpi | 1 |
240dpi | hdpi | 1.5 |
320dpi | xhdpi | 2 |
480dpi | xxhdpi | 3 |
640dpi | xxxhdpi | 4 |
参考:Android 官网-支持不同的像素密度 ,这些资源目录对应的 DensityDpi
的值是固定的,源码中以常量方式存在:
1 | public class DisplayMetrics { |
其中 160dpi
为标准密度,即 density=1
; Density
的计算方式是当前 DensityDpi
和标准密度的比值:比如 XXHIGH
,其 Density=480/160=3
。dp
与 px
的换算公式为: px = dp * Density
;因此资源位图如果没有以正确的尺寸放到对应合适的 drawable-
目录下,会出现位图缩放,导致失真、模糊等等。
比如一张 48*48
的位图,放于 drawable-mdpi
中,它在 densityDpi=160
的设备中将会无缩放直接显示;但是它在 densityDpi=320
的设备中,在同样大小的 ImageView
中显示,则需要将图片放大到 2 倍,如果图片细节处理不好,放大后可能会出现模糊;这种情况下,需要 UI
工程师提供一张对应的 96*96
位图放到 drawable-xhdpi
目录下就不会缩放了。对于 48*48
的位图,在不同密度的屏幕上正确显示,需要的尺寸及存放目录如下:
分辨率 | drawable 目录 |
---|---|
36x36 | ldpi |
48x48 | mdpi |
72x72 | hdpi |
96x96 | xhdpi |
144x144 | xxhdpi |
192x192 | xxxhdpi |
Bitmap
的内存
内存计算
获取 Bitmap
占用内存的大小,对应方法如下:
1 | // Bitmap.java |
其中,如果设置了 BitmapFactory.Options.inBitmap
即采用 Bitmap
内存复用时, getByteCount
表示新增内存空间大小;否则两者相等,都表示 Bitmap
实际占用内存空间大小。
计算方式:Bitmap内存 ≈ 像素数据总大小 = 横向像素值 * 纵向像素值 * 每个像素的内存
,其中:
- 每个像素占用内存
由色彩模式决定,即Bitmap.Config
;在BitmapFactory
解码时,可以指定Options.inPreferredConfig
来设置对应的色彩模式。默认为ARGB_8888
即 4 字节,通常可以使用RGB_565
即 2 字节来压缩。 - 横向像素值/纵向像素值
大致的计算方式为dstWidth = srcWidth*scale
;其中缩放比例scale = inTargetDensity / inDensity
。如果需要更精确的计算,参考BitmapFactory.cpp::doDecode
源码:
1 | // BitmapFactory.cpp::doDecode |
这个缩放只对通过资源文件
drawable-
*生成Bitmap
时有效,即通过 `BitmapFactory.decodeResource相关代码,才会使用缩放;其他方法解码时不会使用缩放,即
scale` 为 1 。
资源文件占用的内存
加载资源文件在内存当中占用的大小取决于以下几点:
- 色彩模式
- 采样率
- 资源文件存放的目录
inDensity
,即hdpi,xxhdpi,mdpi
等 - 目标显示屏幕
inTargetDensity
比如一张 720x1080
大小的图片,放在 drawable-xhdpi
目录下(即 inDensity = 320
),使用 BitmapFactory.decodeResource
来解码生成 Bitmap
,分别在不同手机上占用内存空间大小:
- 在
720x1080
手机inTargetDensity = 320
上加载,图片不会被压缩 - 在
480x800
手机inTargetDensity = 240
上加载,缩放率scale=240/320=3/4
- 在
1080x1920
手机inTargetDensity = 480
上加载,缩放率scale=480/320=3/2
Bitmap
内存回收
Android 2.3.3(API 10)
及以下的系统
在 2.3 以下的系统中,Bitmap
的像素数据是存储在native
中,Bitmap
对象是存储在Java
堆中的,所以在回收Bitmap
时,需要回收两个部分的空间:native
和Java
堆。即先调用recycle()
释放native
中Bitmap
的像素数据,再对Bitmap
对象置null
,保证GC
对Bitmap
对象的回收。Android 3.0(API 11)
及以上的系统
在 3.0 以上的系统中,Bitmap
的像素数据和对象本身都是存储在Java
堆中的,无需主动调用recycle()
,只需将对象置null
,由GC
自动管理。
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
的内存就可以了。
加载大图片
根据计算公式:Bitmap
占用内存的大小主要由图片分辨率等决定(和图片文件大小有几兆无关,它只是占用 ROM
,不是 RAM
),因此加载大图片前可以先读取图片的宽高等信息,再计算采样率,最后再加载缩放到指定大小。
为了避免 OOM
异常,最好在解析每张图片的时候都先检查一下图片的大小,以下几个因素是我们需要考虑的:
- 预估一下加载整张图片所需占用的内存
- 为了加载这一张图片你所愿意提供多少内存
- 用于展示这张图片的控件的实际大小
- 当前设备的屏幕尺寸和分辨率
需要注意的是,如果使用 InputStream
解码获取 Bitmap
,因为流的特性,只能读取一次;所以如果多次读取同一张图,可以使用文件描述符等来实现。
获取 ImageView
如果 ImageView
设置为自适应或者匹配屏幕大小,在没有完成加载前,读取到的宽高为 0 ,因此可以先获取当前手机屏幕分辨率,给出一个预估值。
获取图片宽高分辨率
BitmapFactory.Options.inJustDecodeBounds
设置为 true
,解码时仅读取图片宽高等信息,不会将图片加载到内存。
1 | BitmapFactory.Options options = new BitmapFactory.Options(); |
计算采样率 inSampleSize
根据给定的宽高信息,计算被加载图片最合适的采样率。
1 | private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { |
指定采样率初步加载图片
根据计算出来的采样率,初步加载图片。
1 | options.inSampleSize = calculateInSampleSize(options, size.x, size.y); |
缩放到指定大小
根据给定的 Bitmap
,以及指定的大小,使用 createScaledBitmap
来创建缩放后的 Bitmap
。
1 | Bitmap detectBitmap = Bitmap.createScaledBitmap(src, dstX, dstY, false); |
设置 inMutable
的几种方式
BitmapFactory.Options.inMutable = true
Bitmap.copy(null, true)
后续
Matrix
矩阵
大图小用用采样,小图大用用矩阵
BitmapRegionDecoder
BitmapRegionDecoder
分区域加载:public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) {...}
指定区域加载图片,不用通过采样率压缩。