高效的加载Bitmap避免内存泄露
我们在使用bitmap时,经常会遇到内存溢出等情况,这是因为图片太大或者android系统对单个应用施加的内存限制等原因造成的,所以,高效的使用bitmap就显得尤为重要,对他效率的优化也是如此。
Bitmap基本加载方式
BitmapFactory提供的解析Bitmap的静态工厂方法有以下五种:1
2
3
4
5Bitmap decodeFile(...)
Bitmap decodeResource(...)
Bitmap decodeByteArray(...)
Bitmap decodeStream(...)
Bitmap decodeFileDescriptor(...)
其中常用的三个:decodeFile、decodeResource、decodeStream。decodeFile和decodeResource其实最终都是调用decodeStream方法来解析Bitmap,decodeStream的内部则是调用两个native方法解析Bitmap的:1
2nativeDecodeAsset()
nativeDecodeStream()
这两个native方法只是对应decodeFile和decodeResource、decodeStream来解析的,像decodeByteArray、decodeFileDescriptor也有专门的native方法负责解析Bitmap。
接下来就是看看这两个方法在解析Bitmap时究竟有什么区别decodeFile、decodeResource,查看后发现它们调用路径如下:1
2decodeFile->decodeStream
decodeResource->decodeResourceStream->decodeStream
decodeResource在解析时多调用了一个decodeResourceStream方法,而这个decodeResourceStream方法代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public 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进行处理了,在得到opts.inDensity属性的前提下,如果我们没有对该属性设定值,那么将opts.inDensity=DisplayMetrics.DENSITY_DEFAULT;赋定这个默认的Density值,这个默认值为160,为标准的dpi比例,即在Density=160的设备上1dp=1px,这个方法中还有这么一行:1
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
对opts.inTargetDensity进行了赋值,该值为当前设备的densityDpi值,所以说在decodeResourceStream方法中主要做了两件事:1
21、对opts.inDensity赋值,没有则赋默认值160
2、对opts.inTargetDensity赋值,没有则赋当前设备的densityDpi值
之后重点来了,之后参数将传入decodeStream方法,该方法中在调用native方法进行解析Bitmap后会调用这个方法setDensityFromOptions(bm, opts);:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
if (outputBitmap == null || opts == null) return;
final int density = opts.inDensity;
if (density != 0) {
outputBitmap.setDensity(density);
final int targetDensity = opts.inTargetDensity;
if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
return;
}
byte[] np = outputBitmap.getNinePatchChunk();
final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
if (opts.inScaled || isNinePatch) {
outputBitmap.setDensity(targetDensity);
}
} else if (opts.inBitmap != null) {
// bitmap was reused, ensure density is reset
outputBitmap.setDensity(Bitmap.getDefaultDensity());
}
}
该方法主要就是把刚刚赋值过的两个属性inDensity和inTargetDensity给Bitmap进行赋值,不过并不是直接赋给Bitmap就完了,中间有个判断,当inDensity的值与inTargetDensity或与设备的屏幕Density不相等时,则将应用inTargetDensity的值,如果相等则应用inDensity的值。
1 | 所以总结来说,setDensityFromOptions方法就是把inTargetDensity的值赋给Bitmap,不过前提是opts.inScaled = true; |
进过上面的分析,可以得出这样一个结论:
在不配置Options的情况下:
1、decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图。
2、decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大。
BitmapFactory.decodeResource加载的图片可能会经过缩放,该缩放目前是放在java层做的,效率比较低,而且需要消耗java层的内存。因此,如果大量使用该接口加载图片,容易导致OOM错误。
BitmapFactory.decodeStream不会对所加载的图片进行缩放,相比之下占用内存少,效率更高,但是decodeStream直接拿的图片来读取字节码了,不会根据机器的各种分辨率来自动适应,所以使用了decodeStream的时候,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。
这两个接口各有用处,如果对性能要求较高,则应该使用decodeStream;如果对性能要求不高,且需要 Android 自带的图片自适应缩放功能,则可以使用 decodeResource。
部分摘自:Android性能优化之Bitmap的内存优化。
高效加载一
加载Bitmap的时候,尽量不要使用setImageBitmap或setImageResource,decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。
首先建议通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间。
其次读取时加上BitmapFactory的Option参数,可以更有效减少加载的内存,从而更有效的阻止抛out of Memory异常。
设置Options.inPreferredConfig值来降低内存消耗 //如把默认值ARGB_8888改为RGB_565,节约一半内存
设置Options.inPurgeable和inInputShareable让系统能及时回收内存
设置Options.inSampleSize 对大图片进行压缩
此外需要注意的是:decodeStream直接拿的图片来读取字节码了,不会根据机器的各种分辨率来自动适应,使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。
以最省内存的方式读取本地资源的图片:1
2
3
4
5
6
7
8
9public static Bitmap readBitMap(Context context, int resId) {
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inPreferredConfig = Bitmap.Config.RGB_565;
opt.inPurgeable = true;
opt.inInputShareable = true;
//获取资源图片
InputStream in = context.getResources().openRawResource(resId);
return BitmapFactory.decodeStream(in, null, opt);
}
关于Options的参数,建议阅读笔记第四部分:Options的部分成员。
高效加载二
按需加载,很多时候ImageView并没有原始图片那么大,所以没必要加载原始大小的图片。采用BitmapFactory.Options来加载所需尺寸的图片。通过BitmapFactory.Options来缩放图片,主要是用到了它的inSampleSize参数,即采样率。inSampleSize应该为2的倍数,如果不是系统会向下取整并选择一个最接近2的指数来代替;缩放比例为1/(inSampleSize的二次方)。按需加载虽然可以减少内存的使用,但是可能会有图片失真。
通过采样率高效地加载图片,代码示例: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
37public static Bitmap decodeBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calcuateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
//结合了方式一
options.inPurgeable = true;
options.inInputShareable = true;
options.inPreferredConfig = Bitmap.Config.RGB_565;
return BitmapFactory.decodeResource(res, resId, options);
}
//获取采样率
private static int calcuateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int width = options.outWidth;//注意这里width是以像素为单位的
int height = options.outHeight;//注意这里height是以像素为单位的
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
// 显示图片
Bitmap bitmap = DecodeBitmap.decodeBitmapFromResource(getResources(), R.mipmap.haimei2, 400, 400);
imageView.setImageBitmap(bitmap);
当inJustDecodeBounds参数为true时,BitmapFactory只会解析图片的原始宽/高信息,并不会真正的加载图片,所以这个操作是轻量级的。需要注意这时候BitmapFactory获取的图片宽/高信息和图片的位置与程序运行的设备有关。
通过BitmapFactory.Options对象来加载一张缩放后的图片,对FileInputStream的缩放存在问题,因为FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属相,导致第二次decodeStream时得到的是null。所以一般通过文件流来得到对应的文件描述符,通过BitmapFactory.decodeFileDescriptor()来加载一张缩放后的图片。
关于解决decodeStream加载的问题的另一种解决方案,建议阅读:Bitmap的高效加载
在decodeBitmapFromResource和decodeSampledBitmapFromFileDescriptor方法中可以结合高效加载一中的方法,配置Options的其他参数,进行更高效的加载。
Options的部分成员
1.public boolean inJustDecodeBounds
- true,只是会解析图片的原始高度opt.outHeight、原始宽度信息opt.outWidth,并不会真正的加载图片(不会分配内存空间,避免内存溢出OOM),是一个轻量级的操作。
- false,将申请分配内存,获取得到Bitmap对象。
2.public int inSampleSize
默认或最小值为1,一般大于1,为2的幂,若不为2的幂,将会向下取整选择最接近的2的幂。该参数设置图片的缩放大小(压缩比)。
3.public boolean inPurgeable
- true,系统内存不足时,可以被回收
- false,不能被回收
4.public boolean inInputShareable
设置是否深拷贝,与 inPurgeable 搭配使用。
- inPurgeable - false ,该参数没有意义,被忽略。
- inPurgeable - true,该参数为true,可以共享一个引用给输入数据(流、数组等);false-深拷贝
5.public inPreferredConfig
表示图片解码时使用的颜色模式,也就是图片中每个像素颜色的表示方式。
参数inpreferredconfig的可选值有四个,分别为ALPHA_8,RGB_565,ARGB_4444,ARGB_8888。它们的含义列举如下。
参数取值 | 含义 |
---|---|
ALPHA_8 | 图片中每个像素用一个字节(8位)存储,该字节存储的是图片8位的透明度值 |
RGB_565 | 图片中每个像素用两个字节(16位)存储,两个字节中高5位表示红色通道,中间6位表示绿色通道,低5位表示蓝色通道 |
ARGB_4444 | 图片中每个像素用两个字节(16位)存储,Alpha,R,G,B四个通道每个通道用4位表示 |
ARGB_8888 | 图片中每个像素用四个字节(32位)存储,Alpha,R,G,B四个通道每个通道用8位表示 |
Bitmap内存占用:拿一张1024X1024像素的图片来说,假定采用ARGB8888格式存储,那么它占用的内存为1024X1024X4,即4MB。
在对图片的要求不高的时候,我们可以使用RGB_565可以提高性能。
参考链接:
BitmapFactory.Options避免 内存溢出 OutOfMemoryError的优化方法
温故而知新 - 一些解决OOM的方法
获取缩略图
android Options.inPurgeable的意思
android内存优化
Android 之 Bitmap
Bitmap的分析与使用.md
Android图片加载解析之Bitmap