前段时间工作中治理了一些 oom,针对内存大户 Bitmap 进行了了一次原理层面的分析。
1
如何计算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 方法来得到图片内存大小的字节数,它的计算方法则是:
getRowBytes() * getHeight();
而 getRowBytes 是调取了底层逻辑,最终调用到 SkBitmap里:
size_t rowBytes() const { return fRowBytes; }
skkia里面则通过 minRowBytes 计算行字节数:
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(); }
这里我们得到行字节数的计算:
行字节 = 行像素 * 每像素字节数
这里的 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 硬件加速,如果图片只在内存中,使用这个配置最合适。
这里我们可以先简单理解为图片内存大小就是:
宽 * 高(尺寸) * 每像素字节数
图片尺寸
在上层,我们会通过 BitmapFactory 去创建一个 Bitmap,例如通过
public static Bitmap decodeResource(Resources res, int id)
通过resource里的图片资源创建 Bitmap。类似的函数比较多,但是都会转成stream执行到
public static Bitmap decodeStream(@Nullable InputStream is, @Nullable Rect outPadding,
@Nullable Options opts)
这里传入的 Options 参数其实就会影响最终图片尺寸的计算。接着我们继续看 decodeStream的逻辑。这个会执行 native 的nativeDecodeStream函数。进行图片的解码:解码之前会读取java层传入的配置。其中当 inScale 为ture(默认也是true)的时候:
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
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
opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
这个其实也是设备的 dpi。这个值具体可以通过
adb shell dumpsys window displays
进行查看。
screenDensity
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 来计算输出的宽高:
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;
}
这里宽高会变成:
初始宽高 / simpleSize
接着会使用上面提到是 scale 进行缩放:
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,即:
宽 = 原始宽度 * (targetDensity / density) / sampleSize + 0.5f
2
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 层对象:
// 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层的构造方法:
Bitmap(long nativeBitmap, int width, int height, int density,
boolean requestPremultiplied, byte[] ninePatchChunk,
NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc)
到这里 Bitmap就创建完毕了 这里得到一个简单的指向关系:
接下来看详细的分配逻辑,在 native 层创建 Bitmap 的时候会有预分配的逻辑:
decodingBitmap.tryAllocPixels(decodeAllocator)
这里的 decodingBitmap是 SkBitmap,可以直接 google SkBitmap 对象的源码:
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里面是真正的分配逻辑:
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 分配图片像素:
// now create the java bitmap
return GraphicsJNI::createBitmap(env, javaAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
分配的逻辑放在了 SkImageDecoder 里面:
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:
// 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 的构造方法:
Bitmap(long nativeBitmap, byte[] buffer, int width, int height, int density,
boolean isMutable, boolean requestPremultiplied,
byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets)
和 Android 10 相比,这里多传入了一个 byte 数组叫buffer:
/**
* Backing buffer for the Bitmap.
*/
private byte[] mBuffer;
mBuffer = buffer;
mNativePtr = nativeBitmap;
这里的 mBuffer 就存储了 Bitmap 的像素内容,所以在 Android6 上对象间关系是这样:
接下来在 allocateJavaPixelRef里面看一下具体的内存分配流程:
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 数组是通过 VMRuntime 的 newNonMovableArray分配的,然后通过 addressOf把地址传递给 android::Bitmap。
3
Bitmap内存释放
现在我们继续看一下 Bitmap 的内存释放机制。Bitmap 在 Java 层提供了 recycle方法来释放内存。我们同样也通过 Android 10 和 Android 6的源码进行分析。
8.0以上
Android 8以上的 recycle 方法逻辑如下:
public void recycle() {
if (!mRecycled) {
nativeRecycle(mNativePtr);
mNinePatchChunk = null;
mRecycled = true;
}
}
这里直接调了 native 层的 nativeRecycle 方法,传入的是 mNativePtr,即 native 层 BitmapWrapper指针。nativeRecycle的代码如下:
static void Bitmap_recycle(JNIEnv* env, jobject, jlong bitmapHandle) {
LocalScopedBitmap bitmap(bitmapHandle);
bitmap->freePixels();
}
这里调了 LocalScopedBitmap的 freePixels,LocalScopeBitmap则是代理了 BitmapWrapper这个类。
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 的析构函数:
// hwui/Bitmap.cpp
Bitmap::~Bitmap() {
switch (mPixelStorageType) {
case PixelStorageType::Heap:
free(mPixelStorage.heap.address);
break;
// 省略...
}
}
这里释放了图片的内存数据。但是如果没有手动调用 recycle , Bitmap 会释放内存吗,其实也是会的。这里要从 Java 层的 Bitmap 说起。在 Bitmap 的构造方法里,有如下代码:
NativeAllocationRegistry registry;
registry = NativeAllocationRegistry.createMalloced(
Bitmap.class.getClassLoader(), nativeGetNativeFinalizer(), allocationByteCount);
registry.registerNativeAllocation(this, nativeBitmap);
这样,当Bitmap被Android虚拟机回收的时候,会自动调用 nativeGetNativeFinalizer。关于 NativeAllocationRegistry的细节,我们不做深入讨论。
// 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:
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是:
mFinalizer.mNativeBitmap
这里也是在 Bitmap 创建的时候把 native 的 Bitmap 传给了 BitmapFinalizer对象。继续看 Bitmap#freePixels:
void Bitmap::freePixels() {
AutoMutex _lock(mLock);
if (mPinnedRefCount == 0) {
doFreePixels();
mPixelStorageType = PixelStorageType::Invalid;
}
}
这里的 doFreePixels 也和 8 以上类似,不过走的是 PixelStorageType::Java 的分支:
// 省略其他代码...
case PixelStorageType::Java:
JNIEnv* env = jniEnv();
env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
break;
这里会把 jweakRef 给回收。这个引用指向的的就是存储了图片像素数据的 Java byte 数组。在 8 以下没有 NativeAllocationRegistry的时候,会依赖 Java 对象的 finalize进行内存回收。
@Override
public void finalize() {
try {
super.finalize();
} catch (Throwable t) {
// Ignore
} finally {
setNativeAllocationByteCount(0);
nativeDestructor(mNativeBitmap);
mNativeBitmap = 0;
}
}
这里会调用 nativeDestructor,即 Bitmap_destructor:
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 的析构函数:
Bitmap::~Bitmap() {
doFreePixels();
}
这里和 recycle一样,最后也会通过 doFreePixels 一样回收图片像素内存。整理流程如下:
4
总结
阅读到这里,我们总结几个有用的结论:
• Android Bitmap 内存占用和图片的尺寸,质量强相关,日常治理大图的时候要对这些参数适当做降级方案。
• Android8以下图片分配在 Java 堆内,容易 OOM,可以通过一些 hook 方案把内存移到堆外。并且虽然 Bitmap 有自己兜底的内存释放机制,但是主动及时调用 recycle也不是坏事。
• Android8 以上虽然 Bitmap 内存分配在 native 部分,可以避免 Java 层的 OOM,但是虚拟内存不足的 OOM 还是可能会引发的,所以大图还是需要治理的。
转自:内存大户Bitmap