彻底搞懂Bitmap的内存计算(二)
前言
占用内存 = 图片宽度/inSampleSize*inTargetDensity/inDensity*图片高度/inSampleSize**inTargetDensity/inDensity*每个像素所占的内存
接下来我们结合代码一步步分析,代码基于android的API 29。
正文
本文主要分为两个部分,第一部分底层计算每个像素所占的大小,主要介绍底层怎么计算每个像素所占内存的大小,第二部分主要介绍应用层的参数如:inSampleSize、inTargetDensity、inDensity以及图片宽高怎么影响最终生成的Bitmap的大小
底层计算每个像素所占的大小
看下Bitmap的getAllocationByteCount()函数,这个函数返回Bitmap所占内存的大小如下:
public final int getAllocationByteCount() {
if (mRecycled) {
Log.w(TAG, "Called getAllocationByteCount() on a recycle()'d bitmap! "
+ "This is undefined behavior!");
return 0;
}
return nativeGetAllocationByteCount(mNativePtr);
}
走到了nativeGetAllocationByteCount,是个native方法
作为一个有追求的程序员,怎么能被这个小小的困难吓倒,继续往下肝。找到frameworks/base/libs/hwui/jni/Bitmap.cpp,映射到了如下代码:
static jint Bitmap_getAllocationByteCount(JNIEnv* env, jobject, jlong bitmapPtr) {
LocalScopedBitmap bitmapHandle(bitmapPtr);
return static_cast<jint>(bitmapHandle->getAllocationByteCount());
}
继续往下跟到LocalScopedBitmap#getAllocationByteCount(),如下
size_t getAllocationByteCount() const {
if (mBitmap) {
return mBitmap->getAllocationByteCount();
}
return mAllocationSize;
}
走到Bitmap#getAllocationByteCount(),如下:
size_t Bitmap::getAllocationByteCount() const {
switch (mPixelStorageType) {
case PixelStorageType::Heap:
return mPixelStorage.heap.size;
case PixelStorageType::Ashmem:
return mPixelStorage.ashmem.size;
default:
return rowBytes() * height();
}
}
这里可以看到不同的像素存储类型,计算逻辑是不一样的,但是对于最终的结果影响不大,这里简单提一下Bitmap像素数据在内存的存放位置:2.3之前的像素存储需要的内存是在native上分配的,并且生命周期不太可控,可能需要用户自己回收。 2.3-7.1之间,Bitmap的像素存储在Dalvik的Java堆上对应PixelStorageType::Heap,当然,4.4之前的甚至能在匿名共享内存上分配(Fresco采用)对应PixelStorageType::Ashmem,而8.0之后的像素内存又重新回到native上去分配,不需要用户主动回收,8.0之后图像资源的管理更加优秀,极大降低了OOM。本文基于API29,也就是Android 10,因此对应的是默认逻辑rowBytes() * height(),也就是每行占的内存乘以高度,继续跟到了external/skia/include/core/SkPixelRef.h#rowBytes(),如下:
size_t rowBytes() const { return fRowBytes; }
看下fRowBytes在哪里赋值:
void SkPixelRef::android_only_reset(int width, int height, size_t rowBytes) {
fWidth = width;
fHeight = height;
fRowBytes = rowBytes;
this->notifyPixelsChanged(); }
再看下android_only_reset的调用链路,
void Bitmap::reconfigure(const SkImageInfo& newInfo, size_t rowBytes)
{
mInfo = validateAlpha(newInfo);
// TODO: Skia intends for SkPixelRef to be immutable, but this method
// modifies it. Find another way to support reusing the same pixel memory.
this->android_only_reset(mInfo.width(), mInfo.height(), rowBytes);
}
再看下 reconfigure 的调用链路,如下:
void Bitmap::reconfigure(const SkImageInfo& info) {
reconfigure(info, info.minRowBytes());
}
发现rowBytes()最终是 SkImageInfo 中的 minRowBytes() 计算的,继续跟踪external/skia/include/core/SkImageInfo.h:
size_t minRowBytes() const {
uint64_t minRowBytes = this->minRowBytes64();
if (!SkTFitsIn<int32_t>(minRowBytes)) {
return 0;
}
return (size_t)minRowBytes;
}
继续肝到了minRowBytes64(),如下:
uint64_t minRowBytes64() const {
return (uint64_t)sk_64_mul(this->width(), this->bytesPerPixel());
}
意思就是图片宽度乘以每个像素占的字节,看下bytesPerPixel(),如下:
int bytesPerPixel() const {
return fColorInfo.bytesPerPixel();
}
到了SkColorInfo#bytesPerPixel()如下:
int SkColorInfo::bytesPerPixel() const {
return SkColorTypeBytesPerPixel(fColorType);
}
继续SkColorTypeBytesPerPixel 如下:
int SkColorTypeBytesPerPixel(SkColorType ct) {
switch (ct) {
case kUnknown_SkColorType: return 0;
case kAlpha_8_SkColorType: return 1;
case kRGB_565_SkColorType: return 2;
case kARGB_4444_SkColorType: return 2;
case kRGBA_8888_SkColorType: return 4;
case kBGRA_8888_SkColorType: return 4;
case kRGB_888x_SkColorType: return 4;
case kRGBA_1010102_SkColorType: return 4;
case kRGB_101010x_SkColorType: return 4;
case kBGRA_1010102_SkColorType: return 4;
case kBGR_101010x_SkColorType: return 4;
case kGray_8_SkColorType: return 1;
case kRGBA_F16Norm_SkColorType: return 8;
case kRGBA_F16_SkColorType: return 8;
case kRGBA_F32_SkColorType: return 16;
case kR8G8_unorm_SkColorType: return 2;
case kA16_unorm_SkColorType: return 2;
case kR16G16_unorm_SkColorType: return 4;
case kA16_float_SkColorType: return 2;
case kR16G16_float_SkColorType: return 4;
case kR16G16B16A16_unorm_SkColorType: return 8;
}
SkUNREACHABLE; }
到这里,豁然开朗,其实底层对于每个像素所占的字节是和颜色相关的,和应用层的Bitmap.Config是一一对应,列了一张常用的几个对应表格如下:
应用层名称 | 底层名称 | 位数 | 所占内存 |
---|---|---|---|
ALPHA_8 | kAlpha_8_SkColorType | 8 | 1 |
RGB_565 | kRGB_565_SkColorType | 16 | 2 |
ARGB_4444 | kARGB_4444_SkColorType | 16 | 2 |
ARGB_8888 | kBGRA_8888_SkColorType | 32 | 4 |
到此我们知道了 Bitmap.Config中的颜色空间 是如何影响底层计算单个像素内存的逻辑。
应用层参数影响底层Bitmap的尺寸
先上公式:
占用内存 = 图片宽度/inSampleSize*inTargetDensity/inDensity*图片高度/inSampleSize**inTargetDensity/inDensity*每个像素所占的内存
从如下代码出发:
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.banner);
最终到了BitmapFactory#decodeResourceStream(),如下:
@Nullable
public static Bitmap decodeResourceStream(@Nullable Resources res, @Nullable TypedValue value,
@Nullable InputStream is, @Nullable Rect pad, @Nullable Options opts) {
validate(opts);
if (opts == null) {
opts = new Options();
}
if (opts.inDensity == 0 && value != null) {
//注释1
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
opts.inDensity = density;
}
}
//注释2
if (opts.inTargetDensity == 0 && res != null) {
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
}
return decodeStream(is, pad, opts);
}
这里面出现了计算公式里面的inDensity和inTargetDensity,从注释2中可以看到 inTargetDensity 值为res.getDisplayMetrics().densityDpi,和设备相关,我手里的手机是1920*1080的值为480,从注释1看出opts.inDensity的值来自于value.density,这个value是从如下代码里面计算传值的:
public static Bitmap decodeResource(Resources res, int id, Options opts) {
validate(opts);
Bitmap bm = null;
InputStream is = null;
try {
final TypedValue value = new TypedValue();
//注释1
is = res.openRawResource(id, value);
bm = decodeResourceStream(res, value, is, null, opts);
} catch (Exception e) {
/* do nothing.
If the exception happened on open, bm will be null.
If it happened on close, bm is still valid.
*/
} finally {
try {
if (is != null) is.close();
} catch (IOException e) {
// Ignore
}
}
if (bm == null && opts != null && opts.inBitmap != null) {
throw new IllegalArgumentException("Problem decoding into existing bitmap");
}
return bm;
}
看注释1,进入Resources#openRawResource(),最终跟到
@NonNull
public InputStream openRawResource(@RawRes int id, TypedValue value)
throws NotFoundException {
return mResourcesImpl.openRawResource(id, value);
}
openRawResource会根据资源缩放的位置对TypedValue的inDensity赋值,这里不深究,有时间单开一篇去讲,具体结果如下图:
回过头了看BitmpaFactory的逻辑,最终会走到如下代码:
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
Rect padding, Options opts, long inBitmapHandle, long colorSpaceHandle);
又是个native方法,最终跟到frameworks/base/libs/hwui/jni/BitmapFactory.cpp#doDecode函数,代码较多,精简部分,只留下关键代码如下代码:
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream,
jobject padding, jobject options, jlong inBitmapHandle,
jlong colorSpaceHandle) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// Correct a non-positive sampleSize. sampleSize defaults to zero within the
// options object, which is strange.
//注释1 修正sampleSize
if (sampleSize <= 0) {
sampleSize = 1;
}
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
//注释2获取density 因为图片是放在drawable文件夹下面,density等于160
const int density = env->GetIntField(options, gOptions_densityFieldID);
//注释3 获取targetDensity targetDensity等于160
const int targetDensity = env->GetIntField(options, gOptions_targetDensityFieldID);
//注释4 screenDensity等于0
const int screenDensity = env->GetIntField(options, gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
// 注释5 scale 等于3
scale = (float) targetDensity / density;
}
}
//注释6 根据Options.inSampleSize技术输出的尺寸
SkISize size = codec->getSampledDimensions(sampleSize);
//注释7 根据前面的density和targetDensity计算出的scale,再次计算缩放的宽高
if (scale != 1.0f) {
willScale = true;
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
if (options != NULL) {
jstring mimeType = getMimeTypeAsJavaString(env, codec->getEncodedFormat());
if (env->ExceptionCheck()) {
return nullObjectReturn("OOM in getMimeTypeAsJavaString()");
}
// 注释8 往java写入最终的宽高
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
jint configID = GraphicsJNI::colorTypeToLegacyBitmapConfig(decodeColorType);
if (isHardware) {
configID = GraphicsJNI::kHardware_LegacyBitmapConfig;
}
jobject config = env->CallStaticObjectMethod(gBitmapConfig_class,
gBitmapConfig_nativeToConfigMethodID, configID);
env->SetObjectField(options, gOptions_outConfigFieldID, config);
env->SetObjectField(options, gOptions_outColorSpaceFieldID,
GraphicsJNI::getColorSpace(env, decodeColorSpace.get(), decodeColorType));
if (onlyDecodeSize) {
return nullptr;
}
}
return bitmap;搞
}
注释1处获取修正 Options.inSampleSize,注释2注释3注释4注释5通过获取的Options的targetDensity、density计算出scale,注释6根据Options.inSampleSize技术输出的尺寸,注释7注释8通过之前计算的scale计算出输出Bitmap最终的宽高,输出Bitmap尺寸的宽高计算尺寸公式:
输出BitMap宽*高 = 图片宽度/inSampleSize*inTargetDensity/inDensity*图片高度/inSampleSize**inTargetDensity/inDensity
加上上一节底层计算每个像素的大小,最后得到公式如下:
占用内存 = 图片宽度/inSampleSize*inTargetDensity/inDensity*图片高度/inSampleSize**inTargetDensity/inDensity*每个像素所占的内存
至此Bitmap占用内存计算从上层到底层算是撸了个遍
总结
本文承接上文,时间跨度达两年多之久,主要是设计到Native测的代码晦涩难懂,费时费力,加上换了新工作,All in 业务,时间和精力有限,能在新的一年的年初补上欠账,走出舒适区,尝试自己未曾经历过的事情,也算是一份新年礼物,大家共勉,程序员永不为奴,奥利给
转载自:https://juejin.cn/post/7050079624235073550