阅读 1193

理解Android Bitmap

基于android-6.0.1_r80源代码分析

通过下面三个章节基本可以扫清Bitmap盲区。文章没有覆盖到的一方面是Bitmap用法,这部分建议阅读Glide库源代码。一些Color的概念,例如premultiplied / Dither,需要具备一定CG物理基础,不管怎样先读下去。

Bitmap对象创建

Bitmap java层构造函数是通过nativejni call过来的,逻辑在Bitmap_creator方法中。

// /home/yuxiang/repo_aosp/android-6.0.1_r79/frameworks/base/core/jni/android/graphics/Bitmap.cpp
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable) {
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
    if (NULL != jColors) {
        size_t n = env->GetArrayLength(jColors);
        if (n < SkAbs32(stride) * (size_t)height) {
            doThrowAIOOBE(env);
            return NULL;
        }
    }

    // ARGB_4444 is a deprecated format, convert automatically to 8888
    if (colorType == kARGB_4444_SkColorType) {
        colorType = kN32_SkColorType;
    }

    SkBitmap bitmap;
    bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType));

    Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
    if (!nativeBitmap) {
        return NULL;
    }

    if (jColors != NULL) {
        GraphicsJNI::SetPixels(env, jColors, offset, stride,
                0, 0, width, height, bitmap);
    }

    return GraphicsJNI::createBitmap(env, nativeBitmap,
            getPremulBitmapCreateFlags(isMutable));
}复制代码

legacyBitmapConfigToColorTypeBitmap.Config.ARGB_8888转成skia域的颜色类型kBGRA_8888_SkColorType,颜色类型定义在SkImageInfo.h中,kARGB_4444_SkColorType会强转成kN32_SkColorType,它就是kBGRA_8888_SkColorType,不必纠结。

// /home/yuxiang/repo_aosp/android-6.0.1_r79/external/skia/include/core/SkImageInfo.h
enum SkColorType {
    kUnknown_SkColorType,
    kAlpha_8_SkColorType,
    kRGB_565_SkColorType,
    kARGB_4444_SkColorType,
    kRGBA_8888_SkColorType,
    kBGRA_8888_SkColorType,
    kIndex_8_SkColorType,
    kGray_8_SkColorType,

    kLastEnum_SkColorType = kGray_8_SkColorType,

#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A)
    kN32_SkColorType = kBGRA_8888_SkColorType,
#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A)
    kN32_SkColorType = kRGBA_8888_SkColorType,
#else
    #error "SK_*32_SHFIT values must correspond to BGRA or RGBA byte order"
#endif
};复制代码

接着,根据宽、高、颜色类型等创建SkBitmap,注意kPremul_SkAlphaType描述是alpha采用premultiplied处理的方式,CG处理alpha存在premultiplied和unpremultiplied两两种方式。

public:
    SkImageInfo()
        : fWidth(0)
        , fHeight(0)
        , fColorType(kUnknown_SkColorType)
        , fAlphaType(kUnknown_SkAlphaType)
        , fProfileType(kLinear_SkColorProfileType)
    {}

    static SkImageInfo Make(int width, int height, SkColorType ct, SkAlphaType at,
                            SkColorProfileType pt = kLinear_SkColorProfileType) {
        return SkImageInfo(width, height, ct, at, pt);
    }复制代码

Make创建SkImageInfo对象,fWidth的赋值是一个关键点,后面Java层通过getAllocationByteCount获取Bitmap内存占用中会用到它计算一行像素占用空间。allocateJavaPixelRef是通过JNI调用VMRuntime实例的newNonMovableArray方法分配内存。

int register_android_graphics_Graphics(JNIEnv* env)
{
    jmethodID m;
    jclass c;
    ...
    gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m));
    gVMRuntime_newNonMovableArray = env->GetMethodID(gVMRuntime_class, "newNonMovableArray",
                                                     "(Ljava/lang/Class;I)Ljava/lang/Object;");
    ...
}复制代码

env->CallObjectMethod(gVMRuntime, gVMRuntime_newNonMovableArray, gByte_class, size)拿到虚拟机分配Heap对象,env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj)拿到分配对象的地址,调用native层构造函数new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable)

// /home/yuxiang/repo_aosp/android-6.0.1_r79/frameworks/base/core/jni/android/graphics/Bitmap.cpp
Bitmap::Bitmap(JNIEnv* env, jbyteArray storageObj, void* address,
            const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
        : mPixelStorageType(PixelStorageType::Java) {
    env->GetJavaVM(&mPixelStorage.java.jvm);
    mPixelStorage.java.jweakRef = env->NewWeakGlobalRef(storageObj);
    mPixelStorage.java.jstrongRef = nullptr;
    mPixelRef.reset(new WrappedPixelRef(this, address, info, rowBytes, ctable));
    // Note: this will trigger a call to onStrongRefDestroyed(), but
    // we want the pixel ref to have a ref count of 0 at this point
    mPixelRef->unref();
}

void Bitmap::getSkBitmap(SkBitmap* outBitmap) {
    assertValid();
    android::AutoMutex _lock(mLock);
    // Safe because mPixelRef is a WrappedPixelRef type, otherwise rowBytes()
    // would require locking the pixels first.
    outBitmap->setInfo(mPixelRef->info(), mPixelRef->rowBytes());
    outBitmap->setPixelRef(refPixelRefLocked())->unref();
    outBitmap->setHasHardwareMipMap(hasHardwareMipMap());
}

void Bitmap::pinPixelsLocked() {
    switch (mPixelStorageType) {
    case PixelStorageType::Invalid:
        LOG_ALWAYS_FATAL("Cannot pin invalid pixels!");
        break;
    case PixelStorageType::External:
    case PixelStorageType::Ashmem:
        // Nothing to do
        break;
    case PixelStorageType::Java: {
        JNIEnv* env = jniEnv();
        if (!mPixelStorage.java.jstrongRef) {
            mPixelStorage.java.jstrongRef = reinterpret_cast<jbyteArray>(
                    env->NewGlobalRef(mPixelStorage.java.jweakRef));
            if (!mPixelStorage.java.jstrongRef) {
                LOG_ALWAYS_FATAL("Failed to acquire strong reference to pixels");
            }
        }
        break;
    }
    }
}

// /home/yuxiang/repo_aosp/android-6.0.1_r79/frameworks/base/core/jni/android/graphics/Bitmap.h
std::unique_ptr<WrappedPixelRef> mPixelRef;
PixelStorageType mPixelStorageType;

union {
    struct {
        void* address;
        void* context;
        FreeFunc freeFunc;
    } external;
    struct {
        void* address;
        int fd;
        size_t size;
    } ashmem;
    struct {
        JavaVM* jvm;
        jweak jweakRef;
        jbyteArray jstrongRef;
    } java;
} mPixelStorage;复制代码

native层的Bitmap构造函数,mPixelStorage保存前面创建Heap对象的弱引用,mPixelRef指向WrappedPixelRefoutBitmap拿到mPixelRef强引用对象,这里理解为拿到SkBitmap对象。Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef完成Bitmap Heap分配,创建nativeBitmapSkBitmap对象,最后自然是创建JavaBitmap对象,把该包的包上。native层是通过JNI方法,在Java层创建一个数组对象的,这个数组是对应在Java层的Bitmap对象的buffer数组,所以pixels还是保存在Java堆。而在native层这里它是通过weak指针来引用的,在需要的时候会转换为strong指针,用完之后又去掉strong指针,这样这个数组对象还是能够被Java堆自动回收。里面jstrongRef一开始是赋值为null的,但是在bitmap的getSkBitmap方法会使用weakRef给他赋值。

// /home/yuxiang/repo_aosp/android-6.0.1_r79/frameworks/base/core/jni/android/graphics/Bitmap.cpp
jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
    bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
    // The caller needs to have already set the alpha type properly, so the
    // native SkBitmap stays in sync with the Java Bitmap.
    assert_premultiplied(bitmap->info(), isPremultiplied);

    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    hasException(env); // For the side effect of logging.
    return obj;
}复制代码

重点看下这里env->NewObject(gBitmap_class, gBitmap_constructorMethodID,...,参数中有一处bitmap->javaByteArray(),指向的是Heap对象。所以,实际的像素内存只有一份,被不同对象持有,Java层的Bitmapnative层的Btimap

这里顺带说一下JNI生命周期。JNI Local Reference的生命期是在native method的执行期(从 Java 程序切换到 native code环境时开始创建,或者在native method执行时调用JNI function创建),在native method执行完毕切换回Java程序时,所有JNI Local Reference被删除,生命期结束(调用JNI function可以提前结束其生命期)。

JNI 编程中明显的内存泄漏

  1. Native Code 本身的内存泄漏
    JNI 编程首先是一门具体的编程语言,或者 C 语言,或者 C++,或者汇编,或者其它 native 的编程语言。每门编程语言环境都实现了自身的内存管理机制。因此,JNI 程序开发者要遵循 native 语言本身的内存管理机制,避免造成内存泄漏。以 C 语言为例,当用 malloc() 在进程堆中动态分配内存时,JNI 程序在使用完后,应当调用 free() 将内存释放。总之,所有在 native 语言编程中应当注意的内存泄漏规则,在 JNI 编程中依然适应。
    Native 语言本身引入的内存泄漏会造成 native memory 的内存,严重情况下会造成 native memory 的 out of memory。

  2. Global Reference 引入的内存泄漏
    JNI 编程还要同时遵循 JNI 的规范标准,JVM 附加了 JNI 编程特有的内存管理机制。
    JNI 中的 Local Reference 只在 native method 执行时存在,当 native method 执行完后自动失效。这种自动失效,使得对 Local Reference 的使用相对简单,native method 执行完后,它们所引用的 Java 对象的 reference count 会相应减 1。不会造成 Java Heap 中 Java 对象的内存泄漏。
    而 Global Reference 对 Java 对象的引用一直有效,因此它们引用的 Java 对象会一直存在 Java Heap 中。程序员在使用 Global Reference 时,需要仔细维护对 Global Reference 的使用。如果一定要使用 Global Reference,务必确保在不用的时候删除。就像在 C 语言中,调用 malloc() 动态分配一块内存之后,调用 free() 释放一样。否则,Global Reference 引用的 Java 对象将永远停留在 Java Heap 中,造成 Java Heap 的内存泄漏。

更多JNI泄露,参考阅读JNI 编程中潜在的内存泄漏——对 LocalReference 的深入理解

Bitmap对象释放

基于前文JNI Local Reference和Global Reference泄露,可以看到nativeRecycle实际调用native层Bitmap的freePixels方法,DeleteWeakGlobalRef释放Bitmap native层Gloabl引用。逻辑还是很简单的。

// /home/yuxiang/repo_aosp/android-6.0.1_r79/frameworks/base/core/jni/android/graphics/Bitmap.cpp
void Bitmap::freePixels() {
    AutoMutex _lock(mLock);
    if (mPinnedRefCount == 0) {
        doFreePixels();
        mPixelStorageType = PixelStorageType::Invalid;
    }
}

void Bitmap::doFreePixels() {
    switch (mPixelStorageType) {
    case PixelStorageType::Invalid:
        // already free'd, nothing to do
        break;
    case PixelStorageType::External:
        mPixelStorage.external.freeFunc(mPixelStorage.external.address,
                mPixelStorage.external.context);
        break;
    case PixelStorageType::Ashmem:
        munmap(mPixelStorage.ashmem.address, mPixelStorage.ashmem.size);
        close(mPixelStorage.ashmem.fd);
        break;
    case PixelStorageType::Java:
        JNIEnv* env = jniEnv();
        LOG_ALWAYS_FATAL_IF(mPixelStorage.java.jstrongRef,
                "Deleting a bitmap wrapper while there are outstanding strong "
                "references! mPinnedRefCount = %d", mPinnedRefCount);
        env->DeleteWeakGlobalRef(mPixelStorage.java.jweakRef);
        break;
    }

    if (android::uirenderer::Caches::hasInstance()) {
        android::uirenderer::Caches::getInstance().textureCache.releaseTexture(
                mPixelRef->getStableID());
    }
}复制代码

需要注意两点讯息,一是Java层主动call recycle()方法或者Bitmap析构函数都会调用freePixels,移除Global对象引用,这个对象是Heap上存一堆像素的空间。GC时释放掉。二是,JNI不再持有Global Reference,并native函数执行后释放掉,但Java层的Bitmap对象还在,只是它的mBuffermNativePtr是无效地址,没有像素Heap的Bitmap也就几乎不消耗内存了。至于Java层Bitmap对象什么时候释放,生命周期结束自然free掉了。

// /home/yuxiang/repo_aosp/android-6.0.1_r79/art/runtime/jni_internal.cc
static void DeleteWeakGlobalRef(JNIEnv* env, jweak obj) {
  JavaVMExt* vm = down_cast<JNIEnvExt*>(env)->vm;
  Thread* self = down_cast<JNIEnvExt*>(env)->self;
  vm->DeleteWeakGlobalRef(self, obj);
}

// /home/yuxiang/repo_aosp/android-6.0.1_r79/art/runtime/java_vm_ext.cc
void JavaVMExt::DeleteWeakGlobalRef(Thread* self, jweak obj) {
  if (obj == nullptr) {
    return;
  }
  MutexLock mu(self, weak_globals_lock_);
  if (!weak_globals_.Remove(IRT_FIRST_SEGMENT, obj)) {
    LOG(WARNING) << "JNI WARNING: DeleteWeakGlobalRef(" << obj << ") "
                 << "failed to find entry";
  }
}复制代码

通过BitmapFactory创建Bitmap

Bitmap工厂类提供了多种decodeXXX方法创建Bitmap对象,主要是兼容不同的数据源,包括byte数组、文件、FD、Resource对象、InputStream,最终去到native层方法, 如下:

// BitmapFactory.java
private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage, Rect padding, Options opts);
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd, Rect padding, Options opts);
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
private static native Bitmap nativeDecodeByteArray(byte[] data, int offset, int length, Options opts);
private static native boolean nativeIsSeekable(FileDescriptor fd);复制代码

来看看nativeDecodeStream方法,该方法中先是创建了bufferedStream对象,接着doDecode返回Bitmap对象。SkStreamRewindable定义在skia库中继承SkStream,它声明了两个方法rewindduplicate,写过网络库的同学一看命名便知是byte操作,前者功能是将文件内部的指针重新指向一个流的开头,后者是创建共享此缓冲区内容的新的字节缓冲区。

// BitmapFactory.cpp
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is, jbyteArray storage,
        jobject padding, jobject options) {

    jobject bitmap = NULL;
    SkAutoTDelete<SkStream> stream(CreateJavaInputStreamAdaptor(env, is, storage));

    if (stream.get()) {
        SkAutoTDelete<SkStreamRewindable> bufferedStream(
                SkFrontBufferedStream::Create(stream.detach(), BYTES_TO_BUFFER));
        SkASSERT(bufferedStream.get() != NULL);
        bitmap = doDecode(env, bufferedStream, padding, options);
    }
    return bitmap;
}复制代码

doDecode先是通过JNI拿到JavaOptions对象里面的属性,outWidth、outHeight、inDensity、inTargetDensity这些。后两者用来计算Bitmap缩放比例,计算公式scale = (float) targetDensity / density

// BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {

    int sampleSize = 1;

    SkImageDecoder::Mode decodeMode = SkImageDecoder::kDecodePixels_Mode;
    SkColorType prefColorType = kN32_SkColorType;

    bool doDither = true;
    bool isMutable = false;
    float scale = 1.0f;
    bool preferQualityOverSpeed = false;
    bool requireUnpremultiplied = false;

    jobject javaBitmap = NULL;

    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        if (optionsJustBounds(env, options)) {
            decodeMode = SkImageDecoder::kDecodeBounds_Mode;
        }

        // initialize these, in case we fail later on
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);

        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        prefColorType = GraphicsJNI::getNativeBitmapColorType(env, jconfig);
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
        preferQualityOverSpeed = env->GetBooleanField(options,
                gOptions_preferQualityOverSpeedFieldID);
        requireUnpremultiplied = !env->GetBooleanField(options, gOptions_premultipliedFieldID);
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);

        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;
            }
        }

    ...
}复制代码

这些参数是提供给图片解码器SkImageDecoder。图片资源无非是压缩格式,SkImageDecoder工厂类根据输入流同步拿到具体压缩格式并创建相应解码器。GetFormatName返回支持的图片格式。
SkImageDecoder实例将Options参数设置下去。如此解压出来的是根据实际尺寸裁剪后的图片。

// BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
    if (decoder == NULL) {
        return nullObjectReturn("SkImageDecoder::Factory returned null");
    }

    decoder->setSampleSize(sampleSize);
    decoder->setDitherImage(doDither);
    decoder->setPreferQualityOverSpeed(preferQualityOverSpeed);
    decoder->setRequireUnpremultipliedColors(requireUnpremultiplied)
    ...
}

// SkImageDecoder_FactoryDefault.cpp
SkImageDecoder* SkImageDecoder::Factory(SkStreamRewindable* stream) {
    return image_decoder_from_stream(stream);
}

// SkImageDecoder_FactoryRegistrar.cpp
SkImageDecoder* image_decoder_from_stream(SkStreamRewindable* stream) {
    SkImageDecoder* codec = NULL;
    const SkImageDecoder_DecodeReg* curr = SkImageDecoder_DecodeReg::Head();
    while (curr) {
        codec = curr->factory()(stream);
        // we rewind here, because we promise later when we call "decode", that
        // the stream will be at its beginning.
        bool rewindSuceeded = stream->rewind();

        // our image decoder's require that rewind is supported so we fail early
        // if we are given a stream that does not support rewinding.
        if (!rewindSuceeded) {
            SkDEBUGF(("Unable to rewind the image stream."));
            SkDELETE(codec);
            return NULL;
        }

        if (codec) {
            return codec;
        }
        curr = curr->next();
    }
    return NULL;
}

// SkImageDecoder.cpp
const char* SkImageDecoder::GetFormatName(Format format) {
    switch (format) {
        case kUnknown_Format:
            return "Unknown Format";
        case kBMP_Format:
            return "BMP";
        case kGIF_Format:
            return "GIF";
        case kICO_Format:
            return "ICO";
        case kPKM_Format:
            return "PKM";
        case kKTX_Format:
            return "KTX";
        case kASTC_Format:
            return "ASTC";
        case kJPEG_Format:
            return "JPEG";
        case kPNG_Format:
            return "PNG";
        case kWBMP_Format:
            return "WBMP";
        case kWEBP_Format:
            return "WEBP";
        default:
            SkDEBUGFAIL("Invalid format type!");
    }
    return "Unknown Format";
}复制代码

解码仅仅完成数据的读取,图片是经过渲染才能呈现在最终屏幕上,这个步骤在canvas.drawBitmap方法中完成。

// BitmapFactory.cpp
static jobject doDecode(JNIEnv* env, SkStreamRewindable* stream, jobject padding, jobject options) {
    ...
    SkBitmap outputBitmap;
    if (willScale) {
        // This is weird so let me explain: we could use the scale parameter
        // directly, but for historical reasons this is how the corresponding
        // Dalvik code has always behaved. We simply recreate the behavior here.
        // The result is slightly different from simply using scale because of
        // the 0.5f rounding bias applied when computing the target image size
        const float sx = scaledWidth / float(decodingBitmap.width());
        const float sy = scaledHeight / float(decodingBitmap.height());

        // TODO: avoid copying when scaled size equals decodingBitmap size
        SkColorType colorType = colorTypeForScaledOutput(decodingBitmap.colorType());
        // FIXME: If the alphaType is kUnpremul and the image has alpha, the
        // colors may not be correct, since Skia does not yet support drawing
        // to/from unpremultiplied bitmaps.
        outputBitmap.setInfo(SkImageInfo::Make(scaledWidth, scaledHeight,
                colorType, decodingBitmap.alphaType()));
        if (!outputBitmap.tryAllocPixels(outputAllocator, NULL)) {
            return nullObjectReturn("allocation failed for scaled bitmap");
        }

        // If outputBitmap's pixels are newly allocated by Java, there is no need
        // to erase to 0, since the pixels were initialized to 0.
        if (outputAllocator != &javaAllocator) {
            outputBitmap.eraseColor(0);
        }

        SkPaint paint;
        paint.setFilterQuality(kLow_SkFilterQuality);

        SkCanvas canvas(outputBitmap);
        canvas.scale(sx, sy);
        canvas.drawARGB(0x00, 0x00, 0x00, 0x00);
        canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
    }

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

最终渲染后的图片数据包在了Bitmap对象中,这部分逻辑重回第一章节Bitmap对象创建

关注下面的标签,发现更多相似文章
评论