理解Android Bitmap

Cc養魚人發表於2019-03-04

基於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物件建立

相關文章