原文地址:Android性能优化(五)之细说Bitmap
例如:使用Pixel手机拍摄4048x3036像素(1200W)的照片,如果按ARGB_8888来显示的话,需要48MB的内存空间(4048*3036*4 bytes),这么大的内存消耗极易引发OOM。本篇文章就来说一说这个大胖子。
Android Bitmap内存的管理随着系统的版本迭代也有演进:
1.在Android 2.2(API8)之前,当GC工作时,应用的线程会暂停工作,同步的GC会影响性能。而Android2.3之后,GC变成了并发的,意味着Bitmap没有引用的时候其占有的内存会很快被回收且不会影响主线程性能。
2.在Android 2.3.3(API10)之前,Bitmap的像素数据存放在Native内存,而Bitmap对象本身则存放在Dalvik Heap中,Native内存中的像素数据并不会以可预测的方式进行同步回收,有可能会导致Native内存升高。而在Android3.0之后8.0之前,Bitmap的像素数据也被放在了Dalvik Heap中,这样Bitmap 内存也会随着对象一起被回收。而在Android 8.0 之后,Bitmap的像素数据又被放到 Native 内存中。当然此时Google做了改进,在Native层的Bitmap的像素数据可以做到和Java层的对象一起快速释放。Android 8.0 还新增了硬件位图 Hardware Bitmap,它可以减少图片内存并提升绘制效率。
2.1 Android2.3.3之前
在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收。
备注:只有当确定这个Bitmap不被引用的时候才能调用此方法,否则会有“Canvas: trying to use a recycled bitmap”这个错误。
官方提供了一个使用Recycle的实例:使用引用计数来判断Bitmap是否被展示或缓存,判断能否被回收。
2.2 Android3.0之后
Android3.0之后,并没有强调Bitmap.recycle();而是强调Bitmap的复用:
2.2.1 Save a bitmap for later use
使用LruCache对Bitmap进行缓存,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程。
2.2.2 Use an existing bitmap
Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。不过,使用这个字段有几点限制:
3.1 getByteCount()
getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。
3.2 getAllocationByteCount()
API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer代表存储Bitmap像素数据的字节数组。
return getByteCount();
}
return mBuffer.length;
}
3.3 getByteCount()与getAllocationByteCount()的区别
还记得之前我曾言之凿凿的说:不考虑压缩,只是加载一张Bitmap,那么它占用的内存 = width * height * 一个像素所占的内存。
现在想来实在惭愧:说法也对,但是不全对,没有说明场景,同时也忽略了一个影响项:Density。
4.1 BitmapFactory.decodeResource()
BitmapFactory.java
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) {
//inDensity默认为图片所在文件夹对应的密度
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
if (opts.inTargetDensity == 0 && res != null) {
//inTargetDensity为当前系统密度。
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
BitmapFactory.cpp 此处只列出主要代码。
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
//初始缩放系数
float scale = 1.0f;
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
//缩放系数是当前系数密度/图片所在文件夹对应的密度;
scale = (float) targetDensity / density;
}
}
//原始解码出来的Bitmap;
SkBitmap decodingBitmap;
if (decoder->decode(stream, &decodingBitmap, prefColorType, decodeMode)
!= SkImageDecoder::kSuccess) {
return nullObjectReturn("decoder->decode returned false");
}
//原始解码出来的Bitmap的宽高;
int scaledWidth = decodingBitmap.width();
int scaledHeight = decodingBitmap.height();
//要使用缩放系数进行缩放,缩放后的宽高;
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
//源码解释为因为历史原因;sx、sy基本等于scale。
const float sx = scaledWidth / float(decodingBitmap.width());
const float sy = scaledHeight / float(decodingBitmap.height());
canvas.scale(sx, sy);
canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
此处可以看出:加载一张本地资源图片,那么它占用的内存 = width * height * nTargetDensity/inDensity * nTargetDensity/inDensity * 一个像素所占的内存。
实验:将长为1024、宽为594的一张图片放在xhdpi的文件夹下,使用魅族MX3手机加载。
// 不做处理,默认缩放。
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "width:" + bitmap.getWidth() + ":::height:" + bitmap.getHeight());
Log.i(TAG, "inDensity:" + options.inDensity + ":::inTargetDensity:" + options.inTargetDensity);
Log.i(TAG,"===========================================================================");
// 手动设置inDensity与inTargetDensity,影响缩放比例。
BitmapFactory.Options options_setParams = new BitmapFactory.Options();
options_setParams.inDensity = 320;
options_setParams.inTargetDensity = 320;
Bitmap bitmap_setParams = BitmapFactory.decodeResource(getResources(), R.mipmap.resbitmap, options_setParams);
Log.i(TAG, "bitmap_setParams:ByteCount = " + bitmap_setParams.getByteCount() + ":::bitmap_setParams:AllocationByteCount = " + bitmap_setParams.getAllocationByteCount());
Log.i(TAG, "width:" + bitmap_setParams.getWidth() + ":::height:" + bitmap_setParams.getHeight());
Log.i(TAG, "inDensity:" + options_setParams.inDensity + ":::inTargetDensity:" + options_setParams.inTargetDensity);
输出:
I/lz: bitmap:ByteCount = 4601344:::bitmap:AllocationByteCount = 4601344
I/lz: width:1408:::height:817 // 可以看到此处:Bitmap的宽高被缩放了440/320=1.375倍
I/lz: inDensity:320:::inTargetDensity:440 // 默认资源文件所处文件夹密度与手机系统密度
I/lz: ===========================================================================
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: width:1024:::height:594 // 手动设置了缩放系数为1,Bitmap的宽高都不变
I/lz: inDensity:320:::inTargetDensity:320
可以看出:
4.2 BitmapFactory.decodeFile()
与BitmapFactory.decodeResource()的调用链基本一致,但是少了默认设置density和inTargetDensity(与缩放比例相关)的步骤,也就没有了缩放比例这一说。
除了加载本地资源文件的解码方法会默认使用资源所处文件夹对应密度和手机系统密度进行缩放之外,别的解码方法默认都不会。此时Bitmap默认占用的内存 = width * height * 一个像素所占的内存。这也就是上面4.1开头讲的需要注意场景。
4.3 一个像素占用多大内存?
Bitmap.Config用来描述图片的像素是怎么被存储的?
ARGB_8888: 每个像素4字节. 共32位,默认设置。
Alpha_8: 只保存透明度,共8位,1字节。
ARGB_4444: 共16位,2字节。
RGB_565:共16位,2字节,只存储RGB值。
在上述2.2.2我们谈到了Bitmap的复用,以及复用的限制,Google在《Managing Bitmap Memory》中给出了详细的复用Demo:
BitmapFactory.Options options = new BitmapFactory.Options();
// 图片复用,这个属性必须设置;
options.inMutable = true;
// 手动设置缩放比例,使其取整数,方便计算、观察数据;
options.inDensity = 320;
options.inTargetDensity = 320;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap, options);
// 对象内存地址;
Log.i(TAG, "bitmap = " + bitmap);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
// 使用inBitmap属性,这个属性必须设置;
options.inBitmap = bitmap;
options.inDensity = 320;
// 设置缩放宽高为原始宽高一半;
options.inTargetDensity = 160;
options.inMutable = true;
Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap_reuse, options);
// 复用对象的内存地址;
Log.i(TAG, "bitmapReuse = " + bitmapReuse);
Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
Log.i(TAG, "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());
输出:
I/lz: bitmap = android.graphics.Bitmap@35ac9dd4
I/lz: width:1024:::height:594
I/lz: bitmap:ByteCount = 2433024:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse = android.graphics.Bitmap@35ac9dd4 // 两个对象的内存地址一致
I/lz: width:512:::height:297
I/lz: bitmap:ByteCount = 608256:::bitmap:AllocationByteCount = 2433024
I/lz: bitmapReuse:ByteCount = 608256:::bitmapReuse:AllocationByteCount = 2433024 // ByteCount比AllocationByteCount小
可以看出:
6.1 Bitmap.compress()
质量压缩:
它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的,不会减少图片的像素。进过它压缩的图片文件大小会变小,但是解码成bitmap后占得内存是不变的。
6.2 BitmapFactory.Options.inSampleSize
内存压缩:
备注:inSampleSize值的大小不是随便设、或者越大越好,需要根据实际情况来设置。
以下是设置inSampleSize值的一个示例:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 设置inJustDecodeBounds属性为true,只获取Bitmap原始宽高,不分配内存;
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 计算inSampleSize值;
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 真实加载Bitmap;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
// 宽和高比需要的宽高大的前提下最大的inSampleSize
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
这样使用:mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
备注:
1. Bitmap内存模型
2. Bitmap的内存回收
3. Bitmap占用内存的计算
4. Bitmap的复用
5. Bitmap的压缩
6. Glide
参考: