Earth Guardian

You are not LATE!You are not EARLY!

0%

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=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
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 时,需要回收两个部分的空间: 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
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) {...} 指定区域加载图片,不用通过采样率压缩。

参考文档