前段时间工作中管理了一些 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 办法来得到图片内存巨细的字节数,它的核算办法则是:
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
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。
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 相同收回图片像素内存。
整理流程如下:
总结
阅读到这儿,咱们总结几个有用的定论:
- Android Bitmap 内存占用和图片的尺度,质量强相关,日常管理大图的时分要对这些参数适当做降级计划。
- Android8以下图片分配在 Java 堆内,容易 OOM,能够经过一些 hook 计划把内存移到堆外。并且尽管 Bitmap 有自己兜底的内存开释机制,可是自动及时调用
recycle
也不是坏事。 - Android8 以上尽管 Bitmap 内存分配在 native 部分,能够避免 Java 层的 OOM,可是虚拟内存不足的 OOM 仍是可能会引发的,所以大图仍是需求管理的。