Bitmap 创建示例
Bitmap的创建方式有很多种,例如在XML中定义bitmap、通过bitmapDrawable生成drawable,又或者通过BitmapFactory来创建Bitmap,可以根据实际使用情况来选择,示例如下:
private void bitmapSample() {
/** 1. xml形式
* <bitmap xmlns:android="http://schemas.android.com/apk/res/android"
* android:src="@mipmap/ic_avatar_default"></bitmap>
*/
//2.Drawable
Resources resources = getResources();
Drawable bitmapDrawable = resources.getDrawable(R.mipmap.ic_avatar_default,null);
//3.Bitmap直接创建
Bitmap bitmap = Bitmap.createBitmap(200,200,Bitmap.Config.ARGB_8888);
//4.通过BitmapFactory创建Bitmap
Bitmap bm = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_avatar_default);
}
Bitmap源码解析
Bitmap是一种位图,可以被序列化,且不能被继承,Bitmap的创建和像素数据都是在native层,那么先来看看Bitmap这个类都有些成员变量和属性:
public final class Bitmap implements Parcelable {
public static final int DENSITY_NONE = 0;
private static final long NATIVE_ALLOCATION_SIZE = 32;
private final long mNativePtr; //native指针,这个指针很重要,指向的是Bitmap对应的native层对象
private final boolean mIsMutable; //标记这个bitmap是否可变,如果不可变,那么需要创建一个同样大小、分辨率的bitmap时,可以返回原始bitmap对象
private boolean mRequestPremultiplied;
private byte[] mNinePatchChunk; //.9图数组
private NinePatch.InsetStruct mNinePatchInsets;
private int mWidth; //bitmap宽高
private int mHeight;
private boolean mRecycled; //是否被回收空间。
private ColorSpace mColorSpace; //记录Bitmap颜色
public int mDensity = getDefaultDensity();
private static volatile int sDefaultDensity = -1;
public static volatile int sPreloadTracingNumInstantiatedBitmaps;
public static volatile long sPreloadTracingTotalBitmapsSize;
//Bitmap私有构造函数,必须传入一个nativeBitmap指针,指向一个native层的bitmap,根据该bitmap创建一个Java层的Bitmap
Bitmap(long nativeBitmap, int width, int height, int density, boolean isMutable, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) {
if (nativeBitmap == 0) {
throw new RuntimeException("internal error: native bitmap is 0");
}
//...成员变量赋值操作
}
//内部枚举,用于计算Bitmap大小,不同类型的bitmap占用内存大小不一样。
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
RGBA_F16 (6),
HARDWARE (7);
}
//Bitmap压缩类型,有JPEG\PNG\WEBP三种,不同的压缩格式,压缩后大小也不同。
public enum CompressFormat {
JPEG (0),
PNG (1),
WEBP (2);
CompressFormat(int nativeInt) {
this.nativeInt = nativeInt;
}
final int nativeInt;
}
//... 省略很多createBitmap()方法,创建bitmap的最终实现都是在native层,Java层只是做一些条件判断以及参数设置等等操作。
Bitmap native层创建源码
由于Bitmap是final的,且构造函数私有,那么就无法直接通过new Bitmap()去创建Bitmap,但是可以通过BitmapFactory去创建Bitmap,BitmapFactory提供了很多方法去创建Bitmap,例如:
BitmapFactory.decodeResource(Resources res, int id);
BitmapFactory.decodeFile(String fileName);
BitmapFactory.decodeStream(InputStream is);
...
//这些方法最终都是调用native的方法去创建Bitmap
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.Options
创建Bitmap时可以传入一个BitmapFactory.Options对象来设置Bitmap的属性,这个Options合理使用,可以极大的提高Bitmap的性能,来看下这个类的具体代码:
public class BitmapFactory {
//Options是BitmapFactory的静态内部类
public static class Options {
public Options() {
inScaled = true;
inPremultiplied = true;
}
/**
* 如果设置了inBitmap,那么解码bitmap输入流时,会尝试去复用这个inBitmap,如果无法复用这个inBitmap,那么就会抛出一个IllegalArgumentException;
* 因此这个复用的inBitmap必须是可变的,同时,得到的Bitmap也会是可变的(一般从resource中解码得到的bitmap是一个不可变的bitmap)
*/
public Bitmap inBitmap;
/**
* 设置获取到的bitmap是否可变,可以用来在通过BitmapFactory加载bitmap时设置想要的属性。
*/
public boolean inMutable;
/**
* 设置为true,那么解码器会返回null,但是会将宽高写入Options的outWidth和outHeight供调用者查询,而无需将Bitmap加载入内存,可以显著的提高性能
*/
public boolean inJustDecodeBounds;
/**
* 缩放比例,通过设置inSampleSize可以请求解码器去返回一个缩小后的image去节省内存,例如设置为2,那么返回的bitmap的宽高是原始bitmap的1/2,像素点是原始bitmap的1/4.
* 如果设置inSampleSize < 1,解码器会重设为1.另外,inSampleSize应设置为2的倍数,否则解码器会四舍五入,取离2的幂最近的数。
*/
public int inSampleSize;
//图片默认配置是ARGB_8888
public Bitmap.Config inPreferredConfig = Bitmap.Config.ARGB_8888;
//图片的密度,跟图片放在哪个目录下有关,mipmap-hdpi,mipmap-mdpi,mipmap-xhdpi...
public int inDensity;
//图片的目标密度,如果没有设置,默认就是屏幕密度
public int inTargetDensity;
//屏幕的密度
public int inScreenDensity;
//如果设置了inScaled,那么返回的bitmap会缩放至适配inTargetDensity,而不是依赖于系统的分辨率。
public boolean inScaled;
//bitmap的最终宽高,如果inJustDecodeBounds设置为false,当bitmap发生缩放时,这个值就是bitmap的宽高。如果设置为true,那么无需计算缩放,这两个值就是返回bitmap的宽高。如果解码发生
// 错误,那么就设置为-1。
public int outWidth;
public int outHeight;
}
}
在创建bitmap时设置的Options属性会一路传入到native层,现在再来看看具体的创建过程;选择其中的decodeStream()来看看具体的创建过程:
public static Bitmap decodeStream(InputStream is) {
//decodeStream(InputStream is)内部调用了decodeStream(InputStream is, Rect outPadding,Options opts)
return decodeStream(is, null, null);
}
再看看decodeStream(InputStream is, Rect outPadding,Options opts)的源码:
/**
* outPadding:如果设置了outPadding,相当于对原始Bitmap进行一个裁剪,返回Rect包裹的Bitmap
* opts : bitmap属性设置,例如设置inJustDecodeBounds = true,允许计算bitmap大小,但是不将bitmap加载进内存。
*
*/
public static Bitmap decodeStream(InputStream is, Rect outPadding,Options opts){
if (is == null) {
return null;
}
//验证opt是否合法
validate(opts);
Bitmap bm = null;
Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
try {
//根据输入流类型调用下面的不同方法去创建Bitmap。
if (is instanceof AssetManager.AssetInputStream) {
final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
bm = nativeDecodeAsset(asset, outPadding, opts);
} else {
//这里最终就会调用nativeDecodeStream()
bm = decodeStreamInternal(is, outPadding, opts);
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
setDensityFromOptions(bm, opts);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
}
return bm;
}
decodeStreamInternal():
private static Bitmap decodeStreamInternal(InputStream is,Rect outPadding, Options opts) {
byte [] tempStorage = null;
if (opts != null) tempStorage = opts.inTempStorage;
if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE]; //创建一个buffer用于decode,默认是16K。
return nativeDecodeStream(is, tempStorage, outPadding, opts); //最后就调用BitmapFactory的native方法去创建bitmap
}
BitmapFactory.cpp的源码在:
/frameworks/base/core/jni/android/graphics/BitmapFactory.cpp
现在来看看BitmapFactory.cpp的源码:
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage, jobject padding, jobject options) {
//定义一个bitmap对象
jobject bitmap = NULL;
//创建一个输入流适配器
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));
if (stream.get()) {
std::unique_ptr<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Create(stream.release(), SkCodec::MinBufferedBytesNeeded()));
SkASSERT(bufferedStream.get() != NULL);
// 创建bitmap的代码在doDecode()里面
bitmap = doDecode(env, bufferedStream.release(), padding, options);
}
return bitmap;
}
再看看doDecode()的代码:
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
std::unique_ptr<SkStreamRewindable> streamDeleter(stream);
//从Java代码的Bitmap.java中我们知道,创建Bitmap需要如下的一些成员变量,现在首先定义默认的一些配置,例如缩放比例、是否只是decode,是否是不可变的等等:
int sampleSize = 1;
bool onlyDecodeSize = false;
SkColorType prefColorType = kN32_SkColorType;
bool isHardware = false;
bool isMutable = false;
float scale = 1.0f;
bool requireUnpremultiplied = false;
//这个就是需要返回给Java层的bitmap对象。
jobject javaBitmap = NULL;
//当options不为空时,从option里面取出各种配置设置给上面定义的各个变量
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
//若sampleSize <= 0,会默认重设为1
if (sampleSize <= 0) {
sampleSize = 1;
}
//获取并设置onlyDecodeSize,对应Options中的inJustDecodeBounds。
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
...
//这里很重要,先从options中获取scale的值,如果为true,那么就分别再获取inDensity和inTargetDensity的值以及inScreenDensity的值,用于计算scale的值。
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 等于 要加载图片的density / 图片实际存放目录的density的比值
scale = (float) targetDensity / density;
}
}
...
isHardware = GraphicsJNI::isHardwareConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
//这个gOptions_bitmapFieldID指的是BitmapOptions的inBitmap,如果这个值被设置了,那么decode方法就会去重复使用已存在的bitmap,这样能够提高性能,减少内存的分配和删除。
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
....
}
//Bitmap不能既是可变又是Hardware的。
if (isMutable && isHardware) {
doThrowIAE(env, "Bitmaps with Config.HARWARE are always immutable");
return nullObjectReturn("Cannot create mutable hardware bitmap");
}
// 创建解码器
NinePatchPeeker peeker;
std::unique_ptr<SkAndroidCodec> codec(SkAndroidCodec::NewFromStream(streamDeleter.release(), &peeker));
if (!codec.get()) {
return nullObjectReturn("SkAndroidCodec::NewFromStream returned null");
}
//.9图不能被decode成RGB565格式,因为会抖动
294 if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
295 prefColorType = kN32_SkColorType;
296 }
297
298 //根据采样率创建SkISize
299 SkISize size = codec->getSampledDimensions(sampleSize);
300
301 int scaledWidth = size.width();
302 int scaledHeight = size.height();
303 bool willScale = false;
304
305
306 if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
307 willScale = true;
//从流中解码,并获得具体的宽高,再根据sampleSize得到缩放后的宽高。
308 scaledWidth = codec->getInfo().width() / sampleSize;
309 scaledHeight = codec->getInfo().height() / sampleSize;
310 }
311
312 // 设置解码color类型
313 SkColorType decodeColorType = codec->computeOutputColorType(prefColorType);
314 sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(decodeColorType, prefColorSpace);
316
317 // 如果设置了inJustDecodeBounds,那么这里就返回Bitmap的宽高,而不会创建具体的Bitmap。
318 if (options != NULL) {
319 jstring mimeType = encodedFormatToString(env, (SkEncodedImageFormat)codec->getEncodedFormat());
321 if (env->ExceptionCheck()) {
322 return nullObjectReturn("OOM in encodedFormatToString()");
323 }
324 env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
325 env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
326 env->SetObjectField(options, gOptions_mimeFieldID, mimeType); .....
//如果仅仅只是需要计算宽高,那么就直接返回一个空指针,不需要创建一个新的Bitmap,那么就可以不用将bitmap加载到内存就能够计算得到它的宽高
339 if (onlyDecodeSize) {
340 return nullptr;
341 }
342 }
343
344 // 很重要,图片宽高的计算:
// 如果scale不为1,那么就进行相应的缩放,可见图片的宽= 实际宽 * inTargetDensity/inDensity + 0.5f,高同理
345 if (scale != 1.0f) {
346 willScale = true;
347 scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
348 scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
349 }
350
....
//....调用HeapAllocator去分配空间
//根据输入流解码得到的bitmap
SkBitmap decodingBitmap;
if (!decodingBitmap.setInfo(bitmapInfo) || !decodingBitmap.tryAllocPixels(decodeAllocator)) {
return nullptr;
}
//.9图处理
jbyteArray ninePatchChunk = NULL;
432 if (peeker.mPatch != NULL) {
433 if (willScale) {
434 scaleNinePatchChunk(peeker.mPatch, scale, scaledWidth, scaledHeight);
435 }
436
437 size_t ninePatchArraySize = peeker.mPatch->serializedSize();
438 ninePatchChunk = env->NewByteArray(ninePatchArraySize);
439 if (ninePatchChunk == NULL) {
440 return nullObjectReturn("ninePatchChunk == null");
441 }
442
443 jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
444 if (array == NULL) {
445 return nullObjectReturn("primitive array == null");
446 }
447
//为mNinePatchChunk分配空间
448 memcpy(array, peeker.mPatch, peeker.mPatchSize);
449 env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
450 }
451
452 jobject ninePatchInsets = NULL;
453 if (peeker.mHasInsets) {
454 ninePatchInsets = env->NewObject(gInsetStruct_class, gInsetStruct_constructorMethodID,
455 peeker.mOpticalInsets[0], peeker.mOpticalInsets[1],
456 peeker.mOpticalInsets[2], peeker.mOpticalInsets[3],
457 peeker.mOutlineInsets[0], peeker.mOutlineInsets[1],
458 peeker.mOutlineInsets[2], peeker.mOutlineInsets[3],
459 peeker.mOutlineRadius, peeker.mOutlineAlpha, scale);
460 if (ninePatchInsets == NULL) {
461 return nullObjectReturn("nine patch insets == null");
462 }
463 if (javaBitmap != NULL) {
464 env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID, ninePatchInsets);
465 }
466 }
//定义SkBitmap对象,native层存储的Bitmap对象
468 SkBitmap outputBitmap;
469 if (willScale) {
475 const float sx = scaledWidth / float(decodingBitmap.width());
476 const float sy = scaledHeight / float(decodingBitmap.height());
477
479 SkBitmap::Allocator* outputAllocator;
480 if (javaBitmap != nullptr) {
481 outputAllocator = &recyclingAllocator;
482 } else {
483 outputAllocator = &defaultAllocator;
484 }
485
486 SkColorType scaledColorType = decodingBitmap.colorType();
490 outputBitmap.setInfo(bitmapInfo.makeWH(scaledWidth, scaledHeight).makeColorType(scaledColorType));
492 if (!outputBitmap.tryAllocPixels(outputAllocator)) {
496 return nullObjectReturn("allocation failed for scaled bitmap");
497 }
498
499 SkPaint paint;
503 paint.setBlendMode(SkBlendMode::kSrc);
504 paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
505
//调用SKCanvas将bitmap画出来,最终会调用Graphics的draw()方法。
506 SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
507 canvas.scale(sx, sy);
508 canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
509 } else {
510 outputBitmap.swap(decodingBitmap);
511 }
//处理边距
513 if (padding) {
514 if (peeker.mPatch != NULL) {
515 GraphicsJNI::set_jrect(env, padding,
516 peeker.mPatch->paddingLeft, peeker.mPatch->paddingTop,
517 peeker.mPatch->paddingRight, peeker.mPatch->paddingBottom);
518 } else {
519 GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
520 }
521 }
522
//设置outBitmap的属性
524 if (outputBitmap.pixelRef() == NULL) {
525 return nullObjectReturn("Got null SkPixelRef");
526 }
527
528 if (!isMutable && javaBitmap == NULL) {
530 outputBitmap.setImmutable();
531 }
532
533 bool isPremultiplied = !requireUnpremultiplied;
534 if (javaBitmap != nullptr) {
535 bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied);
536 outputBitmap.notifyPixelsChanged();
538 return javaBitmap;
539 }
540
541 int bitmapCreateFlags = 0x0;
542 if (isMutable) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Mutable;
543 if (isPremultiplied) bitmapCreateFlags |= android::bitmap::kBitmapCreateFlag_Premultiplied;
544
545 if (isHardware) {
546 sk_sp<Bitmap> hardwareBitmap = Bitmap::allocateHardwareBitmap(outputBitmap);
547 if (!hardwareBitmap.get()) {
548 return nullObjectReturn("Failed to allocate a hardware bitmap");
549 }
550 return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags,ninePatchChunk, ninePatchInsets, -1);
552 }
//这里调用createBitmap()生成Java的Bitmap对象,其实native已经创建了真正的Bitmap,
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
557}
createBitmap()生成bitmap对象:
jobject createBitmap(JNIEnv* env, Bitmap* bitmap,int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,int density) {
bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
assert_premultiplied(bitmap->info(), isPremultiplied);
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
//调用Bitmap的构造函数创建Bitmap对象,bitmap对象存储于native,Java持有的是native的一个BitmapWrapper,将BitmapWrapper赋给Java的mNativePtr。
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
isMutable, isPremultiplied, ninePatchChunk, ninePatchInsets);
if (env->ExceptionCheck() != 0) {
ALOGE("*** Uncaught exception returned from Java call!\n");
env->ExceptionDescribe();
}
return obj;
}
Bitmap分析
从上面的创建过程可知,Bitmap无法直接在Java层通过new Bitmap()创建出来,而是需要通过BitmapFactory的decodeFile()或者decodeStream()等等方法去创建,而这些方法内部调用的是native
层的decodeStream()方法,创建的实际bitmap会是保存在native,Java层根据拿到的是native层的BitmapWrapper引用,用于访问native层bitmap。
Bitmap内存计算
Bitmap是内存中的大户,那么现在来看看一个Bitmap的内存是如何计算的,从上面的Bitmap源码可以看到,Bitmap定义了一个枚举,用于表示Bitmap的格式用来表示Bitmap一个像素点的存储格式,如下:
public enum Config {
ALPHA_8 (1),
RGB_565 (3),
@Deprecated
ARGB_4444 (4),
ARGB_8888 (5),
RGBA_F16 (6),
HARDWARE (7);
}
那么bitmap的大小如何计算?想要知道一个bitmap所占用内存大小,可以通过getAllocationByteCount()去获取:
public final int getAllocationByteCount() {
if (mRecycled) {
//如果bitmap已经被回收,那么就直接返回0
return 0;
}
//否则根据mNativePtr引用去native中计算内存大小,mNativePtr是native bitmap的引用。
return nativeGetAllocationByteCount(mNativePtr);
}
Bitmap.cpp中计算内存的方法:
static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) {
//根据bitmapPtr找到native存储的bitmap对象
LocalScopedBitmap bitmapHandle(bitmapPtr);
return static_cast<jint>(bitmapHandle->getAllocationByteCount());
注意getByteCount()和getAllocationByteCount()的区别:
getByteCount() & getAllocationByteCount():
不使用inBitmap时两者相等,当使用了inBitmap时,且inBitmap的图片内存比新创建图片所需要的内存大,那么getByteCount()返回新创建的图片所需要的内存,getAllocationByteCount()返回被复用的bitmap的内存。
现在来看下上面各个配置下像素点的内存计算方法:
ALPHA_8:
每个像素只有一个透明度,即只保存透明度,8位存储,用1个字节即可表示一个像素点;
RGB_565:
每个像素点需要用Red\greeen\blue三个色素值表示,red占5位,green占6位,blue占5位,即一个像素点占16位,即2个字节;
ARGB_4444:
已经废弃(质量不好),每个像素点需要用alpha、Red、greeen、blue四个色素值表示,alpha、red、green、blue各占4位,即一个像素点占16位,即2个字节;
ARGB_8888:
每个像素点需要用alpha、Red、greeen、blue四个色素值表示,alpha、red、green、blue各占8位,即一个像素点占32位,即4个字节;
RGBA_F16:
每个像素点需要用alpha、Red、greeen、blue四个色素值表示,alpha、red、green、blue各占16位,即一个像素点占64位,即8个字节;
HARDWARE:
图片存储于GPU内存中,数据格式还是ARGB_8888.
图片的内存计算:
图片所占内存 = 宽 * 高 * 一个像素点占用字节。
从上面创建bitmap可知,bitmap的宽高计算如下:
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
scale = inTargetDensity / inDensity
由此得出图片内存计算公式:
bitmapMemory = scaledWidth * scaledHeight * 一个像素点的大小 = (原始with * targetDensity / inDensity + 0.5f) * (原始height * targetDensity / inDensity + 0.5f)
那这样计算的结果是否正确呢?来验证一下:
在mipmap-xxhdpi(inDensity = 480)下放一张144 * 144的图片,然后去计算大小,再获取大小打印出来,运行于分辨率480(inTargetDEnsity)的华为手机上,图片默认配置为ARGB_8888,
private void caculateBitmap(){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_bitmap);
System.out.println("bitmap size: "+bitmap.getAllocationByteCount()); //打印结果为 :82944B
}
然后套用上面的公式来计算:
(144 * 480 / 480 + 0.5)* ( 144 * 480 / 480 + 0.5) * 4 = int(144.5) * int(144.5) * 4 = 144 * 144 * 4 = 82944B
因此可以验证得到,我们上面的内存计算公式是对的。
Bitmap内存优化
Bitmap内存优化可以从三方面入手:
- Bitmap复用
- Bitmap格式选择以及合理缩放
- Bitmap回收
Bitmap复用
bitmap复用有两种手段:
- 使用LruCache和DiskLruCache对bitmap进行缓存;
- 使用Options.inBitmap,可以复用一个已存在的bitmap的内存,那么不需要重新在Native去申请内存、释放内存的操作,可以显著的提高性能,但是这里要注意一下版本兼容的问题,参考官方例子Managing Bitmap Memory
Bitmap内存管理版本变化:
- Android 2.2(API 8) 以及之前版本,当进行GC时,app线程会暂停,所以这样会降低性能,Android 2.3 增加了并行GC,当bitmap不再被引用时,可以并行的去进行回收
- Android 2.3.3(API 10) 以及之前版本,bitmap的像素数据存储在native内存中,和bitmap对象本身分离,有可能会导致application超过了它的内存限制引起crash
- Android 3.0 (API11) 到 Android7.0(API 25) ,像素数据以及bitmap对象存储在虚拟机堆中,所以当使用这些版本时,要尤其注重bitmap的回收
- Android 8.0 (API 26)以及之后版本,又将bitmap数据存储在native的堆中,且当bitmap没有再被引用时,GC会自动去回收这个像素数据的内存空间,一般不需要手动的去recycle(),除非确认已经不再需要该 bitmap了,那么可以调用recycle去回收,bitmap不会立刻回收,而会被标记成“dead",此后再调用该bitmap去获取像素数据则会抛出异常
Android3.0之后提供了BitmapFactory.Options.inBitmap去设置复用bitmap,当设置了inBitmap,那么解码方法就会尝试去复用该bitmap的内存,这样能够显著的提高性能,不用重复去申请释放内存,但是使用inBitmap也有限制,在Android4.4之前,只有复用bitmap和待创建bitmap的大小相等时才能复用;复用bitmap必须设置成mutable的,创建的bitmap也会是mutable,即使通常从resource解码出bitmap是immutable的bitmap,复用示例:
private void reUseBitmap() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
options.inDensity = 480;
options.inTargetDensity = 480;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_avatar_default,options);
iv1.setImageBitmap(bitmap);
recycleBitmap.setIsDisplayed(true);
cache.put(String.valueOf(R.mipmap.ic_avatar_default),bitmap);
recycleBitmap.setIsCache(true);
System.out.println("--:bitmap:"+bitmap); //android.graphics.Bitmap@924e1c2
System.out.println("--bitmap.getByteCount:"+bitmap.getByteCount()); //32400
System.out.println("--bitmap.getAllocationByteCount"+bitmap.getAllocationByteCount()); //32400
//由于初始bitmap没有复用其他的bitmap,所以它的getByteCount()和getAllocationByteCount()大小一样。
BitmapFactory.Options reUse = new BitmapFactory.Options();
reUse.inBitmap = cache.get(String.valueOf(R.mipmap.ic_avatar_default));
reUse.inMutable = true;
reUse.inDensity = 480;
reUse.inTargetDensity = 240; //这里targetDensity设置为原始bitmap的一半,那么根据上面的内存计算公式,这个内存大小应该是原来的1/4
Bitmap reUseBitmap = BitmapFactory.decodeResource(getResources(),R.mipmap.ic_bitmap,reUse);
iv2.setImageBitmap(reUseBitmap);
System.out.println("--reUseBitmap.getByteCount:"+reUseBitmap.getByteCount()); //8100 ,当前bitmap所需要的内存大小
System.out.println("--reUseBitmap.getAllocationByteCount"+reUseBitmap.getAllocationByteCount()); //32400,当前bitmap实际的内存大小,即复用bitmap的大小,的确为上面的4倍。
System.out.println("--:reUseBitmap:"+reUseBitmap); //android.graphics.Bitmap@924e1c2
}
从上面打印可知,两个bitmap的内存地址一致,说明第二个bitmap的确是复用了第一个的内存地址,而且要注意复用之后,显示原来bitmap的ImageView也会显示复用后的bitmap,且复用的内存空间应该大于需要创建的新Bitmap,否则会有IllegalArgumentException异常。
可见,我们可以通过LruCache或者DiskLruCache对bitmap进行缓存,或者使用in Bitmap对bitmap进行复用,减少底层再进行内存申请、释放以及解码等等过程,可以显著的提高性能。
Bitmap合理缩放
由上面的结论得知,bitmap所占内存由宽高、inDensity、屏幕密度、图片像素格式计算得出,那么想要减少内存占用,就可以考虑从这里入手,屏幕密度是硬件限制,无法改变。那么可以考虑从其他各个方面入手:
-
对图片进行压缩,在创建bitmap时,使用inSampleSize进行缩小,例如ImageView的大小是100 * 100,而图片的大小是200 * 200,那么可以将图片缩小一倍,宽高变小了,占用内存自然也小了;
-
合理使用图片的像素格式,从上面计算知道,ARGB_8888占用内存为4个字节,而RGB_565为2个字节,那么在保证图片质量的情况下,可以考虑采用这个格式,系统默认是ARGB_8888,ARGB_4444已经被废弃了,就不考 虑了;
-
小图需要放大时,可以使用矩阵,例如:
Matrix matrix = new Matrix(); matrix.postScale(2,2,0,0); iv1.setImageMatrix(matrix); iv1.setScaleType(ImageView.ScaleType.MATRIX); iv1.setImageBitmap(bitmap); //对图片使用了放大,但是图片所占内存和原来一样。
Bitmap回收
Android2.3.3以及之前版本,需要使用recycle()去进行回收,因为如果需要展示非常大的bitmap在app上,可能会引起OOM,所以需要及时使用recycle()去回收,回收后就不能再对bitmap操作了,可以通过一个引用计数去对bitmap进行管理,当无引用时就及时回收,Android3.0之后应该关注的是Bitmap的复用问题,native会自动帮助bitmap回收,可以不手动调用recycle(),示例如下:
private int cacheRefCount = 0; //缓存计数
private int displayRefCount = 0; //展示计数
private boolean hasBeenDisplayed = false;
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
displayRefCount++;
hasBeenDisplayed = true;
} else {
displayRefCount--;
}
}
checkState();
}
public void setIsCache(boolean isCache) {
synchronized (this) {
if (isCache) {
cacheRefCount++;
} else {
cacheRefCount--;
}
}
checkState();
}
private synchronized void checkState() {
if (cacheRefCount <= 0 && displayRefCount <= 0 && hasBeenDisplayed && hasValidBitmap()) {
bitmap.recycle();
}
}
private synchronized boolean hasValidBitmap() {
return bitmap != null && !bitmap.isRecycled();
}