Android Bitmap 相关

Android Bitmap 占用内存大小、优化方案等等。

BitmapFactory.Options

BitmapFactory 在解码生成 Bitmap 时,会通过 Options 的各种参数来做控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public static class Options {
public Options() {
inScaled = true;
inPremultiplied = true;
}
// 如果设置了,重用该位图的存储空间
public Bitmap inBitmap;
// 返回的 Bitmap 是否可操作
public boolean inMutable;
// 设置为 true ,不会将 bitmap 加载到内存!解码时返回的 bitmap 是 null
// 但是能通过 Options.out*** 获取图片的大小等
public boolean inJustDecodeBounds;
// 采样率,值为接近 2 的指数幂
public int inSampleSize;
// 色彩模式,默认为 ARGB_8888
public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;

public ColorSpace inPreferredColorSpace = null;
// 透明通道,默认为 true ,表示 bitmap 中添加透明通道
public boolean inPremultiplied;
// 位图的像素密度,即每英寸有多少个像素;通常为资源文件所在目录对应 density
// 也就是说资源文件放到不同的 drawable-**** 目录下,这个值是不一样的
public int inDensity;
// 绘制位图屏幕的密度;通常为显示这张图片手机的 density
// 也就是说不同手机显示时,这个值是不一样的
public int inTargetDensity;
// 正在使用的实际屏幕的像素密度;暂时没发现在哪会用到
public int inScreenDensity;
// 是否缩放
public boolean inScaled;
// 解码后可以拿到原始图片的宽高、类型等属性
public int outWidth;
public int outHeight;
public String outMimeType;
public Bitmap.Config outConfig;
public ColorSpace outColorSpace;
// 解码时临时存储数组
public byte[] inTempStorage;

// 下面几个已经废弃
public boolean inDither;
public boolean inPurgeable;
public boolean inInputShareable;
public boolean inPreferQualityOverSpeed;
public boolean mCancel;
}

重要属性

  • 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
2
3
4
5
6
7
8
9
10
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
RGBA_F16 (6),
HARDWARE (7);
...
}

常见色彩模式及对应描述:

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
2
device/qcom/common/init/init_msm***.c
device/qcom/common/rootdir/etc/init.qcom.***.sh

init_msm8916.c 文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#define VIRTUAL_SIZE "/sys/class/graphics/fb0/virtual_size"
...
void init_msm_properties(unsigned long msm_id,
unsigned long msm_ver, char *board_type)
{
int rc;
unsigned long virtual_size = 0;
...
// 获取分辨率,并取出宽度
rc = read_file2(VIRTUAL_SIZE, str, sizeof(str));
if (rc) {
virtual_size = strtoul(str, NULL, 0);
}

if(virtual_size >= 1080) {
property_set(PROP_LCDDENSITY, "480");
} else if (virtual_size >= 720) {
// For 720x1280 resolution
// 当前分辨率为 720*2880 ,设置密度为 320
property_set(PROP_LCDDENSITY, "320");
} else if (virtual_size >= 480) {
// For 480x854 resolution QRD.
property_set(PROP_LCDDENSITY, "240");
} else
property_set(PROP_LCDDENSITY, "320");
...
}

init.qcom.early_boot.sh 文件为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 从 virtual_size 读取分辨率,并获取宽度值
if [ -f /sys/class/graphics/fb0/virtual_size ]; then
res=`cat /sys/class/graphics/fb0/virtual_size` 2> /dev/null
fb_width=${res%,*}
fi

...

// 根据宽度来设置密度,比如当前显示设备为 720*2880 ,则密度设置为 320
function set_density_by_fb() {
#put default density based on width
if [ -z $fb_width ]; then
setprop ro.sf.lcd_density 320
else
if [ $fb_width -ge 1440 ]; then
setprop ro.sf.lcd_density 560
elif [ $fb_width -ge 1080 ]; then
setprop ro.sf.lcd_density 480
elif [ $fb_width -ge 720 ]; then
setprop ro.sf.lcd_density 320 #for 720X1280 resolution
elif [ $fb_width -ge 480 ]; then
setprop ro.sf.lcd_density 240 #for 480X854 QRD resolution
else
setprop ro.sf.lcd_density 160
fi
fi
}

DisplayMetrics 中,默认的 density, densityDpi 都是根据这个属性来设置默认值的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DisplayMetrics {
...
public float density;
public int densityDpi;

@Deprecated
public static int DENSITY_DEVICE = getDeviceDensity();
public static final int DENSITY_DEVICE_STABLE = getDeviceDensity();

public void setToDefaults() {
...
// 默认值
density = DENSITY_DEVICE / (float) DENSITY_DEFAULT;
densityDpi = DENSITY_DEVICE;
scaledDensity = density;
xdpi = DENSITY_DEVICE;
ydpi = DENSITY_DEVICE;
...
}

private static int getDeviceDensity() {
return SystemProperties.getInt("qemu.sf.lcd_density",
SystemProperties.getInt("ro.sf.lcd_density",
DENSITY_DEFAULT));
}

结论: 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class DisplayMetrics {
// res 下的标准密度
public static final int DENSITY_LOW = 120;
public static final int DENSITY_MEDIUM = 160;
public static final int DENSITY_HIGH = 240;
public static final int DENSITY_XHIGH = 320;
public static final int DENSITY_XXHIGH = 480;
public static final int DENSITY_XXXHIGH = 640;
public static final int DENSITY_DEFAULT = DENSITY_MEDIUM;

// 其他密度
public static final int DENSITY_TV = 213;
public static final int DENSITY_260 = 260;
...
}

其中 160dpi 为标准密度,即 density=1Density 的计算方式是当前 DensityDpi 和标准密度的比值:比如 XXHIGH ,其 Density=480/160=3
dppx 的换算公式为: 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
2
3
// Bitmap.java
public final int getByteCount() {...}
public final int getAllocationByteCount() {...}

其中,如果设置了 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
2
3
4
5
6
7
8
// BitmapFactory.cpp::doDecode
// Scale is necessary due to density differences.
if (scale != 1.0f) {
willScale = true;
// 先乘以缩放系数,再加上 0.5 ,最后转为整型
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}

这个缩放只对通过资源文件 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 时,需要回收两个部分的空间: nativeJava 堆。即先调用 recycle() 释放 nativeBitmap 的像素数据,再对 Bitmap 对象置 null ,保证 GCBitmap 对象的回收。
  • 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
2
3
4
5
6
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true; // 不将图片加载到内存,仅读取图片信息
BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor()
, null, options); // 这次解码,返回的 Bitmap 为空
Log.d(TAG, " width = " + options.outWidth +
", height = " + options.outHeight); // outWidth, outHeight 即为图片分辨率

计算采样率 inSampleSize

根据给定的宽高信息,计算被加载图片最合适的采样率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int width = options.outWidth;
final int height = options.outHeight;
int inSampleSize = 1;

if (height > reqHeight || width > reqWidth) {
//使用需要的宽高的最大值来计算比率
final int suitedValue = reqHeight > reqWidth ? reqHeight : reqWidth;
final int heightRatio = Math.round((float) height / (float) suitedValue);
final int widthRatio = Math.round((float) width / (float) suitedValue);

inSampleSize = heightRatio > widthRatio ? heightRatio : widthRatio;//用最大
}

return inSampleSize;
}

指定采样率初步加载图片

根据计算出来的采样率,初步加载图片。

1
2
3
4
5
6
7
8
9
options.inSampleSize = calculateInSampleSize(options, size.x, size.y);
options.inJustDecodeBounds = false; // 设置为 false 加载到内存并生成 Bitmap
options.inMutable = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap display = BitmapFactory.decodeFileDescriptor(
pfd.getFileDescriptor(), null, options);
// 输出实际加载后的图片分辨率
Log.d(TAG, "display.width = " + display.getWidth() +
", display.height = " + display.getHeight());

缩放到指定大小

根据给定的 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) {...} 指定区域加载图片,不用通过采样率压缩。

参考文档

0%