前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内存大户Bitmap

内存大户Bitmap

作者头像
烧麦程
发布2023-03-08 20:23:46
1.6K0
发布2023-03-08 20:23:46
举报
文章被收录于专栏:半行代码半行代码

前段时间工作中治理了一些 oom,针对内存大户 Bitmap 进行了了一次原理层面的分析。

如何计算Bitmap的内存占用

日常我们提到图片大小的时候,一般都会把关注点放在图片的文件大小。因为一般来说,图片文件越小,内存占用也会越小。但是其实图片文件大小和内存占用大小没有什么直接的必然联系,我们可以通过查看 Android 的 Bitmap 的内存分配,来查看 Bitmap 的内存大小是被哪些因素影响的。

在 Android 的架构里, Bitmap 相关的内容分为下面几个模块:

  • Java:包括 Bitmap、BitmapFactory等类,上层直接使用创建 Bitmap
  • native:包括 android::Bitmap 对象等,负责决定内存分配方式,调用skia
  • sk:包括 SkBitmap, skia 引擎去绘制 Bitmap

这里绘制一个简单的调用时序图方便缕清逻辑:

在Android里,android5-8 和 android8 以上的 Bitmap 内存分配策略是不同的,但是通过源码对比,虽然代码有了比较大的改动,但是调用流程和内存大小的计算方式是基本没有什么大的变化。

解码配置-每像素字节

Bitmap里面,我们可以通过 getByteCount 方法来得到图片内存大小的字节数,它的计算方法则是:

代码语言:javascript
复制
getRowBytes() * getHeight();

而 getRowBytes 是调取了底层逻辑,最终调用到 SkBitmap里:

代码语言:javascript
复制
size_t rowBytes() const { return fRowBytes; }

skkia里面则通过 minRowBytes 计算行字节数:

代码语言:javascript
复制

size_t minRowBytes() const {
        uint64_t minRowBytes = this->minRowBytes64();
        if (!SkTFitsIn<int32_t>(minRowBytes)) {
            return 0;
        }
        return (size_t)minRowBytes;
}

uint64_t minRowBytes64() const {
 return (uint64_t)sk_64_mul(this->width(), this->bytesPerPixel());
}

int bytesPerPixel() const { return fColorInfo.bytesPerPixel(); }

这里我们得到行字节数的计算:

代码语言:javascript
复制
行字节 = 行像素 * 每像素字节数

这里的 fColorInfo 就对应 Option里的 inPreferredConfig。这个代表了图片的解码配置,包括:

  • ALPHA_8 单通道,总共8位,1个字节
  • RGB_565 每像素16为
  • ARGB-4444 每像素16位,(2字节),已经废弃,传的话会被改为 ARGB_8888
  • ARGB_8888 每个像素32位(总共4字节),也就是 argb 四个通过各8位
  • RGBA_F16 每个像素16位,总共8个字节
  • HARDWARE 硬件加速,如果图片只在内存中,使用这个配置最合适

这里我们可以先简单理解为图片内存大小就是

代码语言:javascript
复制
宽 * 高(尺寸) * 每像素字节数

图片尺寸

在上层,我们会通过 BitmapFactory 去创建一个 Bitmap,例如通过

代码语言:javascript
复制
public static Bitmap decodeResource(Resources res, int id)

通过resource里的图片资源创建 Bitmap。类似的函数比较多,但是都会转成stream执行到

代码语言:javascript
复制
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
            @Nullable Options opts)

这里传入的 Options 参数其实就会影响最终图片尺寸的计算。 接着我们继续看 decodeStream的逻辑。这个会执行 native 的nativeDecodeStream函数。进行图片的解码: 解码之前会读取java层传入的配置。其中当 inScale 为ture(默认也是true)的时候:

代码语言:javascript
复制
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;
  }
}

这里读取 inDensity 、inTargetDensity和 inScreenDensity 参数,来确定缩放比例。 这几个参数看着挺抽象的,我们看下传入的具体是什么东西inDensity

代码语言:javascript
复制
final int density = value.density;
if (density == TypedValue.DENSITY_DEFAULT) {
 opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
} else if (density != TypedValue.DENSITY_NONE) {
 opts.inDensity = density;
}

传入源图的density,如果是默认值的话就传160,inTargetDensity

代码语言:javascript
复制
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;

这个其实也是设备的 dpi。这个值具体可以通过

代码语言:javascript
复制
adb shell dumpsys window displays

进行查看。

screenDensity

代码语言:javascript
复制
static int resolveDensity(@Nullable Resources r, int parentDensity) {
 final int densityDpi = r == null ? parentDensity : r.getDisplayMetrics().densityDpi;
 return densityDpi == 0 ? DisplayMetrics.DENSITY_DEFAULT : densityDpi;
}

一般情况下和 inTargetDensity 的一样的。 所以这里计算出来的scale是用来适配屏幕分辨率的。

然后会通过 sampleSize 来计算输出的宽高:

代码语言:javascript
复制
SkISize size = codec->getSampledDimensions(sampleSize);

//skia
SkISize SkSampledCodec::onGetSampledDimensions(int sampleSize) const {
 const SkISize size = this->accountForNativeScaling(&sampleSize);
 return SkISize::Make(get_scaled_dimension(size.width(), sampleSize),
  get_scaled_dimension(size.height(), sampleSize));
}

static inline int get_scaled_dimension(int srcDimension, int sampleSize) {
 if (sampleSize > srcDimension) {
  return 1;
 }
 return srcDimension / sampleSize;
}

这里宽高会变成

代码语言:javascript
复制
初始宽高 / simpleSize

接着会使用上面提到是 scale 进行缩放:

代码语言:javascript
复制
if (scale != 1.0f) {
 willScale = true;
 scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
 scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}

这里可以看到我们最后传给Java层去创建 Bitmap 的尺寸就是一系列计算得到的 scaleWidth * scaleHeight,即:

代码语言:javascript
复制
宽  = 原始宽度 * (targetDensity / density) / sampleSize + 0.5f

Bitmap内存分配

在对应用的内存情况做进一步分析后,了解到了 Bitmap 的内存分配与回收在不同的 Android 版本中又不一样的机制。最近对这块也做了一些了解。 根据 Android 系统版本,可以把分配方式分成几组:

  • Android 3以前:图片数据分配在 native。这个已经是历史了,不关系
  • Android8 以前: 图片数据分配在java堆。 这个虽然也挺旧了,但是应用基本还会支持很大一部分,
  • Android8 及以后:图片数据分配在 native

所以我copy了 2 份源码来分析这部分,一份 Android6 的, 一份 Android 10 的。

创建过程

8.0以上

顺着 8.0 的 BitmapFactory#nativeDecodeStream 往下看,在 native 层代码里面,最终会调用 Bitmap 的构造方法去创建 Bitmap 的 java 层对象:

代码语言:javascript
复制
// now create the java bitmap
return bitmap::createBitmap(env, defaultAllocator.getStorageObjAndReset(),
    bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);


// createBitmap
BitmapWrapper* bitmapWrapper = new BitmapWrapper(bitmap);
jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density,
            isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc);

这里 BitmapWrapper 是对 native Bitmap 的一层包装。这里传递的是它的指针。 这个对应了Java层的构造方法:

代码语言:javascript
复制
Bitmap(long nativeBitmap, int width, int height, int density,
            boolean requestPremultiplied, byte[] ninePatchChunk,
            NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc)

到这里 Bitmap就创建完毕了 这里得到一个简单的指向关系:

接下来看详细的分配逻辑,在 native 层创建 Bitmap 的时候会有预分配的逻辑:

代码语言:javascript
复制
decodingBitmap.tryAllocPixels(decodeAllocator)

这里的 decodingBitmapSkBitmap,可以直接 google SkBitmap 对象的源码

代码语言:javascript
复制
bool SkBitmap::tryAllocPixels(Allocator* allocator) {
    HeapAllocator stdalloc;

    if (nullptr == allocator) {
        allocator = &stdalloc;
    }
    return allocator->allocPixelRef(this);
}

//上面调用的 HeapAllocator#allocPixelRef
// Graphics.cpp
bool HeapAllocator::allocPixelRef(SkBitmap* bitmap) {
    mStorage = android::Bitmap::allocateHeapBitmap(bitmap);
    return !!mStorage;
}

allocateHeapBitmap里面是真正的分配逻辑:

代码语言:javascript
复制
sk_sp<Bitmap> Bitmap::allocateHeapBitmap(const SkImageInfo& info) {
    size_t size;
    if (!computeAllocationSize(info.minRowBytes(), info.height(), &size)) {
        LOG_ALWAYS_FATAL("trying to allocate too large bitmap");
        return nullptr;
    }
    return allocateHeapBitmap(size, info, info.minRowBytes());
}

sk_sp<Bitmap> Bitmap::allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

使用 calloc函数分配需要的size。并且创建 Bitmap,把分配后的指针指向 addr.

8.0以下

8.0以下的 decode 里面最后会使用 JavaAllocator 分配图片像素:

代码语言:javascript
复制
// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
 bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);

分配的逻辑放在了 SkImageDecoder 里面:

代码语言:javascript
复制
SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
// ...
decoder->decode(
    stream,
    &decodingBitmap,
    prefColorType, decodeMode) != SkImageDecoder::kSuccess
)

// skia
SkImageDecoder::Result SkImageDecoder::decode(SkStream* stream, SkBitmap* bm, SkColorType pref,
                                              Mode mode) {
    // we reset this to false before calling onDecode
    fShouldCancelDecode = false;
    // assign this, for use by getPrefColorType(), in case fUsePrefTable is false
    fDefaultPref = pref;

    // pass a temporary bitmap, so that if we return false, we are assured of
    // leaving the caller's bitmap untouched.
    SkBitmap tmp;
    const Result result = this->onDecode(stream, &tmp, mode);
    if (kFailure != result) {
        bm->swap(tmp);
    }
    return result;
}

这里调用 onDecode 函数,onDecode是一个模板方法,实际上调用子类 SkPNGImageDecoder 的 onDecode:

代码语言:javascript
复制
// SkPNGImageDecoder
SkImageDecoder::Result SkPNGImageDecoder::onDecode(SkStream* sk_stream, SkBitmap* decodedBitmap, Mode mode) {
    //...

    if (!this->allocPixelRef(decodedBitmap,
  kIndex_8_SkColorType == colorType ? colorTable : NULL)) {
        return kFailure;
    }

    //...
}

这里使用的就是 JavaAllocator。 和 10.0 的代码一样,我们先看 createBitmap 之后的逻辑。也会调用 Java Bitmap 的构造方法:

代码语言:javascript
复制
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
            boolean isMutable, boolean requestPremultiplied,
            byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets)

和 Android 10 相比,这里多传入了一个 byte 数组叫buffer:

代码语言:javascript
复制
/**
* Backing buffer for the Bitmap.
*/
private byte[] mBuffer;

mBuffer = buffer;
mNativePtr = nativeBitmap;

这里的 mBuffer 就存储了 Bitmap 的像素内容,所以在 Android6 上对象间关系是这样:

接下来在 allocateJavaPixelRef里面看一下具体的内存分配流程:

代码语言:javascript
复制
android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
 // 省略...

    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size);
    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj);
    
 android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable);
    wrapper->getSkBitmap(bitmap);
    bitmap->lockPixels();
    return wrapper;
}

这里 byte 数组是通过 VMRuntimenewNonMovableArray分配的,然后通过 addressOf把地址传递给 android::Bitmap。

Bitmap内存释放

现在我们继续看一下 Bitmap 的内存释放机制。 Bitmap 在 Java 层提供了 recycle方法来释放内存。我们同样也通过 Android 10 和 Android 6的源码进行分析。

8.0以上

Android 8以上的 recycle 方法逻辑如下:

代码语言:javascript
复制
public void recycle() {
    if (!mRecycled) {
        nativeRecycle(mNativePtr);
        mNinePatchChunk = null;
        mRecycled = true;
    }
}

这里直接调了 native 层的 nativeRecycle 方法,传入的是 mNativePtr,即 native 层 BitmapWrapper指针。nativeRecycle的代码如下:

代码语言:javascript
复制
static void Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    bitmap->freePixels();
}

这里调了 LocalScopedBitmapfreePixelsLocalScopeBitmap则是代理了 BitmapWrapper这个类。

代码语言:javascript
复制
void freePixels() {
 mInfo = mBitmap->info();
 mHasHardwareMipMap = mBitmap->hasHardwareMipMap();
 mAllocationSize = mBitmap->getAllocationByteCount();
 mRowBytes = mBitmap->rowBytes();
 mGenerationId = mBitmap->getGenerationID();
 mIsHardware = mBitmap->isHardware();
 mBitmap.reset();
}

最后会调用 bitmap 指针的 reset, 那么最后会执行 Bitmap 的析构函数:

代码语言:javascript
复制
// hwui/Bitmap.cpp
Bitmap::~Bitmap() {
    switch (mPixelStorageType) {
        case PixelStorageType::Heap:
            free(mPixelStorage.heap.address);
         break;
        // 省略...
    }
}

这里释放了图片的内存数据。 但是如果没有手动调用 recycle , Bitmap 会释放内存吗,其实也是会的。这里要从 Java 层的 Bitmap 说起。 在 Bitmap 的构造方法里,有如下代码:

代码语言:javascript
复制
NativeAllocationRegistry registry;
registry = NativeAllocationRegistry.createMalloced(
 Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
registry.registerNativeAllocation(this, nativeBitmap);

这样,当Bitmap被Android虚拟机回收的时候,会自动调用 nativeGetNativeFinalizer。关于 NativeAllocationRegistry的细节,我们不做深入讨论。

代码语言:javascript
复制
// nativeGetNativeFinalizer
static void Bitmap_destruct(BitmapWrapper* bitmap) {
    delete bitmap;
}

static jlong Bitmap_getNativeFinalizer(JNIEnv*, jobject) {
    return static_cast<jlong>(reinterpret_cast<uintptr_t>(&Bitmap_destruct));
}

这里会调用 bitmap 的 delete,自然也会调 Bitmap 的析构函数,清理图片的像素内存。 我们把 8 以上的 Bitmap 内存回收整理一个结构图:

6.0

分析完 Android 10 的代码,我们继续了解下 8 以下是怎么回收 Bitmap 的。 同样先看 recycle:

代码语言:javascript
复制
public void recycle() {
    if (!mRecycled && mFinalizer.mNativeBitmap != 0) {
        if (nativeRecycle(mFinalizer.mNativeBitmap)) {
            mBuffer = null;
            mNinePatchChunk = null;
        }
        mRecycled = true;
    }
}

nativeRecycle 里面调用 android/graphics/Bitmap.cpp 的 Bitmap_recycle方法,这里的逻辑和 8 以上是一样的。只是这里传入的 bitmapHandle

代码语言:javascript
复制
mFinalizer.mNativeBitmap

这里也是在 Bitmap 创建的时候把 native 的 Bitmap 传给了 BitmapFinalizer对象。 继续看 Bitmap#freePixels:

代码语言:javascript
复制
void Bitmap::freePixels() {
    AutoMutex _lock(mLock);
    if (mPinnedRefCount == 0) {
        doFreePixels();
        mPixelStorageType = PixelStorageType::Invalid;
    }
}

这里的 doFreePixels 也和 8 以上类似,不过走的是 PixelStorageType::Java 的分支:

代码语言:javascript
复制
// 省略其他代码...
case PixelStorageType::Java:
 JNIEnv* env = jniEnv();
 env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
 break;

这里会把 jweakRef 给回收。这个引用指向的的就是存储了图片像素数据的 Java byte 数组。 在 8 以下没有 NativeAllocationRegistry的时候,会依赖 Java 对象的 finalize进行内存回收。

代码语言:javascript
复制
@Override
public void finalize() {
    try {
        super.finalize();
 } catch (Throwable t) {
  // Ignore
 } finally {
  setNativeAllocationByteCount(0);
  nativeDestructor(mNativeBitmap);
  mNativeBitmap = 0;
 }
}

这里会调用 nativeDestructor,即 Bitmap_destructor:

代码语言:javascript
复制
static void Bitmap_destructor(JNIEnv* env, jobject, jlong bitmapHandle) {
    LocalScopedBitmap bitmap(bitmapHandle);
    bitmap->detachFromJava();
}

void Bitmap::detachFromJava() {
    bool disposeSelf;
    {
        android::AutoMutex _lock(mLock);
        mAttachedToJava = false;
        disposeSelf = shouldDisposeSelfLocked();
    }
    if (disposeSelf) {
        delete this;
    }
}

这里最后会调用 delete this,即调用 Bitmap 的析构函数:

代码语言:javascript
复制
Bitmap::~Bitmap() {
    doFreePixels();
}

这里和 recycle一样,最后也会通过 doFreePixels 一样回收图片像素内存。 整理流程如下:

总结

阅读到这里,我们总结几个有用的结论:

  • Android Bitmap 内存占用和图片的尺寸,质量强相关,日常治理大图的时候要对这些参数适当做降级方案。
  • Android8以下图片分配在 Java 堆内,容易 OOM,可以通过一些 hook 方案把内存移到堆外。并且虽然 Bitmap 有自己兜底的内存释放机制,但是主动及时调用 recycle也不是坏事。
  • Android8 以上虽然 Bitmap 内存分配在 native 部分,可以避免 Java 层的 OOM,但是虚拟内存不足的 OOM 还是可能会引发的,所以大图还是需要治理的。
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-02-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 半行代码 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 如何计算Bitmap的内存占用
    • 解码配置-每像素字节
      • 图片尺寸
      • Bitmap内存分配
        • 创建过程
        • Bitmap内存释放
          • 8.0以上
            • 6.0
            • 总结
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档