日常我们提到图片大小的时候,一般都会把关注点放在图片的文件大小。因为一般来说,图片文件越小,内存占用也会越小。但是其实图片文件大小和内存占用大小没有什么直接的必然联系,我们可以通过查看 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( InputStream is, Rect outPadding,
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( 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 还是可能会引发的,所以大图还是需要治理的。
Android 技术提升知识点归整
Android 性能调优系列:https://qr18.cn/FVlo89
Android 车载学习指南:https://qr18.cn/F05ZCM
Android Framework核心知识点笔记:https://qr18.cn/AQpN4J
Android 音视频学习笔记:https://qr18.cn/Ei3VPD
Jetpack全家桶(含Compose):https://qr18.cn/A0gajp
Kotlin 入门到精进:https://qr18.cn/CdjtAF
Flutter 基础到进阶实战:https://qr18.cn/DIvKma
Android 八大知识体系:https://qr18.cn/CyxarU
Android 中高级面试题锦:https://qr18.cn/CKV8OZ
后续如有新知识点,将会持续更新,尽请期待……