Android Bitmap深入介绍(一)---基础

在Android应用开发中,我们经常需要跟图片打交道,而图片一个很麻烦的问题是占用内存非常大,经常导致OOM,了解Bitmap相关信息,不同sdk版本中Android图片处理的变化,以及一些优化处理的方式对我们平时开发中对图片的会非常有帮助。

我觉得需要了解好图片最基础的是需要知道图片基础信息,Bitmap内容,存储信息,以及加载。

像素

Bitmap的存储可以说包括两个部分,像素以及长,宽,颜色等描述信息。像素是Bitmap最占用内存的地方,长宽和像素位数是用来描述图片的,可以通过这些信息计算出图片的像素占用的内存大小。具体到Bitmap的API是下面这几个接口:

1
2
3
public final int getWidth()
public final int getHeight()
public final Config getConfig()

Config是一个枚举类型,表示图片像素类型,总共有下面几种类型:ALPHA_8 (1),RGB_565 (3),ARGB_4444 (4),ARGB_8888 (5);。表示每一个像素图片组成。实际上下面两种方式获取的数值是相等的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int b = 1;
switch (bitmap.getConfig()) {
case ALPHA_8:
b = 1;
break;
case ARGB_4444:
b = 2;
break;
case ARGB_8888:
b = 4;
break;
}
int bytes1 = bitmap.getWidth() * bitmap.getHeight() * b;
int bytes2 = bitmap.getByteCount(); //从api12才有的接口

这是由Bitmap相关参数可以计算出Bitmap所占用的像素数,实际上我们放入drawable里面的图片都是已经知道了图片的长宽以及像素组成的,但是直接在Android外面算出的图片像素数量与通过上面的代码计算会有出入的。因为Android对图片做了缩放,这个跟你将图片放入的drawable位置相关。

我们都知道android资源目录中会有drawable-hdpi, drawable-xhdpi,drawable-xxhdpi等目录。这里每个目录都会对应一个density。下面看BitmapFactory.decodeResource方法加载Bitmap的例子:

1
2
3
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test4, options);
Log.i(LOGTAG, "options: " + options.inDensity + "," + options.inTargetDensity);

decodeResource就是Android内部对Resource的加载方式,这里就不从源码上面一步一步介绍了,它最终会调用decodeResourceStream方法,直接看decodeResourceStream:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts) {
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}

options.inDensity表示图片自身默认的像素密度,TypedValue会有一个density,对应着图片来自于哪个drawable目录,因为每一个drawable目录(drawable-hdpi,drawable-xhdpi,drawable-xxhdpi)都对应着一种屏幕,而屏幕就有density,TypedValue的density对应着DisplayMetrics的densityDpi,densityDpi表示每英尺的像素数。options.inTargetDensity是当前手机屏幕对应的densityDpi,最终的像素数是:

1
bytes = 原始图片宽*(options.inDensity/options.inTargetDensity)*原始图片长*(options.inDensity/options.inTargetDensity)*每个像素点位数

存储与传输

Android图片在不同的sdk版本中存储的地方是不一样的。在2.3及2.3以前,图片像素是存储在native内存中。Android内存分为Java堆和native内存。Android会限制每个应用能使用的最大内存。但是Android对内存的限制是Java堆和native内存的和,把像素数据存放在native区,虚拟机无法自动进行垃圾回收,必须手动使用bitmap.recycle()导致很容易内存泄漏。因为Android的设备monitor也只能够看到Java堆的内存变化,这样其实也不方便调试Bitmap内存。比如在应用中新创建一个图片,根本无法在monitor中看到内存变化。

从3.0开始Android将图片保存在Java堆中,新加载一张图片的时候,也能够立刻从monitor反映出来。另外Java的垃圾回收机制也能够自动回收。然后在4.0后,图片又有了一些变化,那就是在parcel传输的时候,当图片很大时,它会使用ashmem来进行图片的传输,具体可以看我这篇文章Android4.0之后Parcel传输Bitmap源码分析。在6.0的时候,图片的存储又有了很大的变化,底层已经明显增加了将图片保存ashmem的接口了,具体可以可以看我这篇文章Android6.0 Bitmap存储以及Parcel传输源码分析

BitmapFactory

BitmapFactory是用来加载图片的,这个类主要分为三种图片的加载,先把它的API拿出来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static Bitmap decodeResourceStream(Resources res, TypedValue value,
InputStream is, Rect pad, Options opts)
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeResource(Resources res, int id)
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts) {
public static Bitmap decodeByteArray(byte[] data, int offset, int length) {
public static Bitmap decodeFile(String pathName, Options opts)
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeStream(InputStream is)
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd)

我们直接看BitmapFactory提供的nativeDecode接口:

1
2
3
4
5
6
7
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts);
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
Rect padding, Options opts);
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,
int length, Options opts);

BitmapFactory对File的decode都会转换为InputStream采用nativeDecodeStream来decode,对Resource的decode会采用decodeAsset,而如果FileDesciptor可以转换为native的fd,会通过nativeDecodeFileDescriptor来decode,另外ByteArray会直接采用nativeDecodeByteArray来decode。需要注意的是,对Resource的decode,BitmapFactory会设置Option的相关参数,最终进行相应的缩放,图片的大小会跟原图有所区别。 具体的内容建议去看看BitmapFactory,了解每种方式的区别,才能够更好地使用接口,选择的时候采用更有效率的方法。

BitmapFactory.Options

下面看一下Options类,我们在加载的时候,可以通过这个参数对图片进行一些处理,前面已经说了inDensity和inTargetDensity。下面看看其他的参数。

inPurgeable

这个参数的用途是当需要使用Bitmap的时候再加载Bitmap,不需要的时候回收Bitmap。在4.1中,使用inPurgeable,加载图片后内存基本不会增高,而不使用inPurgeable加载图片后内存会有明显的增加。

inSampleSize

这是表示采样大小,长和宽会对应乘以1/inSampleSize。用于将图片缩小加载出来的,以免站占用太大内存,适合缩略图。

inJustDecodeBounds

这个设置了true后,用于获取图片的宽度长度信息。下面是个例子:

1
2
3
4
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bmp = BitmapFactory.decodeFile(path, options);
// options.outWidth 和 options.outHeight就能够获取结果

inBitmap

在Android 3.0开始引入了inBitmap设置,通过设置这个参数,在图片加载的时候可以使用之前已经创建了的Bitmap,以便节省内存,避免再次创建一个Bitmap。在Android4.4,新增了允许inBitmap设置的图片与需要加载的图片的大小不同的情况,只要inBitmap的图片比当前需要加载的图片大就好了。

inPreferredConfig

用于配置图片解码方式,对应的类型Bitmap.Config。如果非null,则会使用它来解码图片

总结

这篇先主要介绍了Bitmap相关基本的信息,Bitmap,BitmapFactory和Options类,以及bitmap的存储。