彻底搞懂Bitmap的内存计算(二)

·  阅读 627

前言

看了一下近乎彻底搞懂Bitmap的内存计算(一)的发布时间是2019年9月,打死我都没想到第二篇会拖到两年半之后,不管怎样现在补上,在上篇文章中我们总结了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方法

image.png 作为一个有追求的程序员,怎么能被这个小小的困难吓倒,继续往下肝。找到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_8kAlpha_8_SkColorType81
RGB_565kRGB_565_SkColorType162
ARGB_4444kARGB_4444_SkColorType162
ARGB_8888kBGRA_8888_SkColorType324

到此我们知道了 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赋值,这里不深究,有时间单开一篇去讲,具体结果如下图: image.png 回过头了看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 业务,时间和精力有限,能在新的一年的年初补上欠账,走出舒适区,尝试自己未曾经历过的事情,也算是一份新年礼物,大家共勉,程序员永不为奴,奥利给

分类:
Android
标签:
分类:
Android
标签: