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

怎样核算Bitmap的内存占用

日常咱们说到图片巨细的时分,一般都会把关注点放在图片的文件巨细。由于一般来说,图片文件越小,内存占用也会越小。可是其实图片文件巨细和内存占用巨细没有什么直接的必然联系,咱们能够经过检查 Android 的 Bitmap 的内存分配,来检查 Bitmap 的内存巨细是被哪些因素影响的。

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

  • Java:包含 Bitmap、BitmapFactory等类,上层直接运用创立 Bitmap
  • native:包含 android::Bitmap 目标等,担任决议内存分配办法,调用skia
  • sk:包含 SkBitmap, skia 引擎去制作 Bitmap

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

内存大户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

进行检查。

内存大户Bitmap
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就创立结束了 这儿得到一个简单的指向联系:

内存大户Bitmap

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

decodingBitmap.tryAllocPixels(decodeAllocator)

这儿的 decodingBitmapSkBitmap,能够直接 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 上目标间联系是这样:

内存大户Bitmap
接下来在 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 数组是经过 VMRuntimenewNonMovableArray分配的,然后经过 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();
}

这儿调了 LocalScopedBitmapfreePixelsLocalScopeBitmap则是代理了 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 内存收回整理一个结构图:

内存大户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 相同收回图片像素内存。 整理流程如下:

内存大户Bitmap

总结

阅读到这儿,咱们总结几个有用的定论:

  • Android Bitmap 内存占用和图片的尺度,质量强相关,日常管理大图的时分要对这些参数适当做降级计划。
  • Android8以下图片分配在 Java 堆内,容易 OOM,能够经过一些 hook 计划把内存移到堆外。并且尽管 Bitmap 有自己兜底的内存开释机制,可是自动及时调用 recycle也不是坏事。
  • Android8 以上尽管 Bitmap 内存分配在 native 部分,能够避免 Java 层的 OOM,可是虚拟内存不足的 OOM 仍是可能会引发的,所以大图仍是需求管理的。