1. 程式人生 > >Android6.0 Bitmap儲存以及Parcel傳輸原始碼分析

Android6.0 Bitmap儲存以及Parcel傳輸原始碼分析

如果想要對Android Bitmap進行更多的操作,理解好Bitmap的實現將會有非常大的幫助,另外Android在6.0中增加了asm儲存圖片。這篇文章就通過原始碼來分析Android6.0中的Bitmap。本文主要分析Java層與native層的Bitmap,以及Bitmap的儲存和Parcel傳輸。原始碼基於6.0,所以會有一些新的特性。

Bitmap儲存方式以及包含的屬性

計算機裡面圖片都是作為陣列來儲存的,而在Android中Bitmap也是一樣。在Java層的Bitmap陣列儲存為mBuffer。而在native層,Bitmap有四種儲存方式,在Bitmap.h檔案中有個列舉類:

enum class PixelStorageType {
    Invalid,
    External,
    Java,
    Ashmem,
};

Invalid表示圖片已經失效了,一般圖片free掉之後就會是這種狀態。External是外部儲存。Java是表示這個Bitmap對應著Java的Bitmap,此時Bitmap會儲存著Java層Bitmap的儲存陣列的弱引用。而Ashmem則是對應著匿名共享記憶體,表示圖片是儲存在匿名共享記憶體當中。後三種類型在Bitmap中對應著一個union型別:

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;

另外因為圖片是直接儲存在一片記憶體區域,那麼它也可以儲存在匿名共享記憶體當中,這就是Fresco在5.0之前乾的事情,而將圖片放到匿名共享記憶體當中,不會自動GC,應用會更加流暢,因為不在Java堆,也不用關心Java堆大小的限制而導致OOM。

另外還包含幾種屬性:
width, height: 圖片寬度和高度
mDensity: 裝置密度
colorType: 圖片顏色型別,RGB或者gray等,圖片通道數量
rowBytes: 用來表示圖片畫素的位元組數
alphaType: 影象透明度型別,是否有透明度或者沒有透明度
isMutable: 是否易變的

這些屬性在進行Parcel傳輸的時候,都會通過Parcel傳遞,另外也是為了方便圖片操作。

Java層與native層Bitmap

Bitmap的主要實現是在native層,Java層的Bitmap相當於是native層的介面。

Java層Bitmap

Bitmap實際上分為Java層和native層的,Java層包含了一個mBuffer陣列用來儲存畫素,但總的來說Java層只是一個方便Java層應用訪問的介面,最終還是通過native層來儲存圖片內容。在Java層中,我們常用的介面可能是createBitmap,getPixel,setPixel等,但實際上這些函式最終都是呼叫native層介面實現的,下面是Java層Bitmap的建立函式:

private static Bitmap createBitmap(DisplayMetrics display, int width, int height,
        Config config, boolean hasAlpha) {
    if (width <= 0 || height <= 0) {
        throw new IllegalArgumentException("width and height must be > 0");
    }
    Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true); // 這!!!
    if (display != null) {
        bm.mDensity = display.densityDpi;
    }
    bm.setHasAlpha(hasAlpha);
    if (config == Config.ARGB_8888 && !hasAlpha) {
        nativeErase(bm.mFinalizer.mNativeBitmap, 0xff000000);
    }
    // No need to initialize the bitmap to zeroes with other configs;
    // it is backed by a VM byte array which is by definition preinitialized
    // to all zeroes.
    return bm;
}

Bitmap還有很多native方法,具體可以看Bitmap native 方法。我們重點看createBitmap。

另外在Java層與native層對應的標記是mNativeBitmap變數,它儲存的是native層Bitmap的指標地址。這樣在native層通過reinterpret_cast即可得到具體的物件。關於這個,可以看Binder機制的實現Android原始碼代理模式—Binder

native層

既然Bitmap的具體實現都是在native,那麼看一下native層的Bitmap,native層的Bitmap在frameworks/base/core/jni/android/graphics/Bitmap.cpp中,對應的jni註冊部分也在該檔案下。看一下native層Bitmap的建立nativeCreate對應的Bitmap_creator函式:

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

看看Bitmap的建立函式,從一建立開始,Bitmap就是先出現在native層的,Android中2D繪圖是由skia框架實現的,在上述程式碼中就對應著SkBitmap。

而對於Java儲存型別的Bitmap的建立是由GraphicsJNI的allocateJavaPixelRef完成的,allocateJavaPixelRef是從Java層分配畫素陣列,看看allocateJavaPixelRef的原始碼

android::Bitmap* GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
    const SkImageInfo& info = bitmap->info();
    if (info.fColorType == kUnknown_SkColorType) {
        doThrowIAE(env, "unknown bitmap configuration");
        return NULL;
    }

    size_t size;
    if (!computeAllocationSize(*bitmap, &size)) {
        return NULL;
    }

    // we must respect the rowBytes value already set on the bitmap instead of
    // attempting to compute our own.
    const size_t rowBytes = bitmap->rowBytes();
    // 在這裡分配
    jbyteArray arrayObj = (jbyteArray) env->CallObjectMethod(gVMRuntime,
                                                             gVMRuntime_newNonMovableArray,
                                                             gByte_class, size); //在這建立Java層Array
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }
    SkASSERT(arrayObj);
    jbyte* addr = (jbyte*) env->CallLongMethod(gVMRuntime, gVMRuntime_addressOf, arrayObj); //獲取地址
    if (env->ExceptionCheck() != 0) {
        return NULL;
    }
    SkASSERT(addr);
    android::Bitmap* wrapper = new android::Bitmap(env, arrayObj, (void*) addr,
            info, rowBytes, ctable); //建立native層物件, 在Bitmap建構函式中mPixelStorage中儲存了jweak引用。
    wrapper->getSkBitmap(bitmap); // 在這裡會將mPixelStorage的弱引用轉換為強引用
    // since we're already allocated, we lockPixels right away
    // HeapAllocator behaves this way too
    bitmap->lockPixels();

    return wrapper;
}

可以看到,native層是通過JNI方法,在Java層建立一個數組物件的,這個陣列是對應在Java層的Bitmap物件的buffer陣列,所以影象還是儲存在Java堆的。而在native層這裡它是通過weak指標來引用的,在需要的時候會轉換為strong指標,用完之後又去掉strong指標,這樣這個陣列物件還是能夠被Java堆自動回收。可以看一下native層的Bitmap建構函式:

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);//建立對Java層物件的弱引用
    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();
}

裡面jstrongRef一開始是賦值為null的,但是在bitmap的getSkBitmap方法會使用weakRef給他賦值:

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(); //refPixelRefLocked
    outBitmap->setHasHardwareMipMap(hasHardwareMipMap());
}
void Bitmap::pinPixelsLocked() {  //refPixelRefLocked會呼叫這個方法
    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;
    }
    }
}

在native層隨時新增刪除一個強引用,這樣有利於更好地配合Java堆的垃圾回收。圖片的陣列可能會是非常耗記憶體的。

在建立了native層的Bitmap後,再用GraphicsJNI的createBitmap建立Java層的Bitmap物件:

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);//建立Java層Bitmap物件
    hasException(env); // For the side effect of logging.
    return obj;
}

在建立過程中,將剛剛建立的Java層Array和native層的bitmap指標也都會傳給Java層Bitmap的建構函式。

另外對於External儲存型別的Bitmap,它的建立如下:

Bitmap::Bitmap(void* address, void* context, FreeFunc freeFunc,
            const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
        : mPixelStorageType(PixelStorageType::External) {
    mPixelStorage.external.address = address;
    mPixelStorage.external.context = context;
    mPixelStorage.external.freeFunc = freeFunc;
    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();
}

而Ashmem則是儲存一個fd,以及asm地址和大小:

Bitmap::Bitmap(void* address, int fd,
            const SkImageInfo& info, size_t rowBytes, SkColorTable* ctable)
        : mPixelStorageType(PixelStorageType::Ashmem) {
    mPixelStorage.ashmem.address = address;
    mPixelStorage.ashmem.fd = fd;
    mPixelStorage.ashmem.size = ashmem_get_size_region(fd);
    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();
}

native層Bitmap會針對不同的儲存型別,做不同的處理。

Parcel傳遞

首先在Java層Bitmap實現了Parcelable介面,所以他是能夠通過Parcel來傳遞的,看看Bitmap的parcelable部分的原始碼:

public final class Bitmap implements Parcelable {
    ...
    /**
     * Write the bitmap and its pixels to the parcel. The bitmap can be
     * rebuilt from the parcel by calling CREATOR.createFromParcel().
     * @param p    Parcel object to write the bitmap data into
     */
    public void writeToParcel(Parcel p, int flags) {
        checkRecycled("Can't parcel a recycled bitmap");
        if (!nativeWriteToParcel(mFinalizer.mNativeBitmap, mIsMutable, mDensity, p)) {
            throw new RuntimeException("native writeToParcel failed");
        }
    }
    public static final Parcelable.Creator<Bitmap> CREATOR
              = new Parcelable.Creator<Bitmap>() {


        public Bitmap More ...createFromParcel(Parcel p) {
            Bitmap bm = nativeCreateFromParcel(p);
            if (bm == null) {
                 throw new RuntimeException("Failed to unparcel Bitmap");
            }
            return bm;
        }
        public Bitmap[] More ...newArray(int size) {
            return new Bitmap[size];
        }
    };
    ...
}

寫入和讀取分別呼叫了nativeWriteToParcel,nativeCreateFromParcel。先看看nativeWriteToParcel對應的native層方法Bitmap_writeToParcel:

static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
                                     jlong bitmapHandle,
                                     jboolean isMutable, jint density,
                                     jobject parcel) {

    //根據handle建立native層圖片,寫入圖片相關的一些附加資訊,width,height,colorType,density等等。
    if (parcel == NULL) {
        SkDebugf("------- writeToParcel null parcel\n");
        return JNI_FALSE;
    }

    android::Parcel* p = android::parcelForJavaObject(env, parcel);
    SkBitmap bitmap;

    android::Bitmap* androidBitmap = reinterpret_cast<Bitmap*>(bitmapHandle);
    androidBitmap->getSkBitmap(&bitmap);

    p->writeInt32(isMutable);
    p->writeInt32(bitmap.colorType());
    p->writeInt32(bitmap.alphaType());
    p->writeInt32(bitmap.width());
    p->writeInt32(bitmap.height());
    p->writeInt32(bitmap.rowBytes());
    p->writeInt32(density);

    if (bitmap.colorType() == kIndex_8_SkColorType) {
        SkColorTable* ctable = bitmap.getColorTable();
        if (ctable != NULL) {
            int count = ctable->count();
            p->writeInt32(count);
            memcpy(p->writeInplace(count * sizeof(SkPMColor)),
                   ctable->readColors(), count * sizeof(SkPMColor));
        } else {
            p->writeInt32(0);   // indicate no ctable
        }
    }
    // 關鍵看這部分傳輸程式碼!!!!
    // Transfer the underlying ashmem region if we have one and it's immutable.
    android::status_t status;
    int fd = androidBitmap->getAshmemFd(); //獲取匿名共享記憶體,如果是圖片是在匿名共享記憶體
    if (fd >= 0 && !isMutable && p->allowFds()) { //如果成功獲取,並且圖片不是mutable,同時允許fd(mAllowFds預設為True)
        status = p->writeDupImmutableBlobFileDescriptor(fd); //最終會直接把檔案fd傳過去
        if (status) {
            doThrowRE(env, "Could not write bitmap blob file descriptor.");
            return JNI_FALSE;
        }
        return JNI_TRUE;
    }
    // 如果不能通過fd傳遞,則傳輸Blob資料,也就是相當於直接把畫素資料傳遞過去。
    // Copy the bitmap to a new blob.
    bool mutableCopy = isMutable;

    size_t size = bitmap.getSize();
    android::Parcel::WritableBlob blob;
    status = p->writeBlob(size, mutableCopy, &blob);
    if (status) {
        doThrowRE(env, "Could not copy bitmap to parcel blob.");
        return JNI_FALSE;
    }

    bitmap.lockPixels();
    const void* pSrc =  bitmap.getPixels();
    if (pSrc == NULL) {
        memset(blob.data(), 0, size);
    } else {
        memcpy(blob.data(), pSrc, size);
    }
    bitmap.unlockPixels();

    blob.release();
    return JNI_TRUE;
}

從原始碼可以知道,如果是匿名共享記憶體儲存,那麼writeToParcel會通過匿名共享記憶體的方式將匿名共享檔案傳遞過去,看看writeDupFileDescriptor方法:

status_t Parcel::writeDupFileDescriptor(int fd)
{
    int dupFd = dup(fd);
    if (dupFd < 0) {
        return -errno;
    }
    status_t err = writeFileDescriptor(dupFd, true /*takeOwnership*/);
    if (err) {
        close(dupFd);
    }
    return err;
}

如果是儲存的陣列資料,那麼會直接將畫素資料轉換為Blob來傳遞。這是在6.0的原始碼中是如此的,在5.0的原始碼中,還沒有增加這些東西,5.0的原始碼中只有普通的將畫素儲存區域memcopy來傳。Android在3.0中增加了inBitmap,在4.4增加了不同大小的圖片使用inBitmap。

而nativeCreateFromParcel對應了native層的Bitmap_createFromParcel,在6.0的原始碼裡面原始碼如下(去掉了DEBUG_PARCEL):

static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) {
    ......
    // 一開始讀取圖片相關的一些資訊,比如說width, height, density, colorType等等,並存於SkImageInfo中。並且對ColorType的相關處理,這些佔用的記憶體都很小,關鍵看畫素的傳遞

    SkColorTable* ctable = NULL;
    if (colorType == kIndex_8_SkColorType) {
        int count = p->readInt32();
        if (count < 0 || count > 256) {
            // The data is corrupt, since SkColorTable enforces a value between 0 and 256,
            // inclusive.
            return NULL;
        }
        if (count > 0) {
            size_t size = count * sizeof(SkPMColor);
            const SkPMColor* src = (const SkPMColor*)p->readInplace(size);
            if (src == NULL) {
                return NULL;
            }
            ctable = new SkColorTable(src, count);
        }
    }

    // Read the bitmap blob.
    size_t size = bitmap->getSize();
    android::Parcel::ReadableBlob blob;
    android::status_t status = p->readBlob(size, &blob); //這裡對應writeDupFileDescriptor
    if (status) {
        SkSafeUnref(ctable);
        doThrowRE(env, "Could not read bitmap blob.");
        return NULL;
    }
     // 關鍵看這部分傳輸程式碼!!!!
    // Map the bitmap in place from the ashmem region if possible otherwise copy.
    Bitmap* nativeBitmap;
    if (blob.fd() >= 0 && (blob.isMutable() || !isMutable) && (size >= ASHMEM_BITMAP_MIN_SIZE)) {

        // Dup the file descriptor so we can keep a reference to it after the Parcel
        // is disposed.
        int dupFd = dup(blob.fd());
        if (dupFd < 0) {
            blob.release();
            SkSafeUnref(ctable);
            doThrowRE(env, "Could not allocate dup blob fd.");
            return NULL;
        }

        // Map the pixels in place and take ownership of the ashmem region.
        nativeBitmap = GraphicsJNI::mapAshmemPixelRef(env, bitmap.get(),
                ctable, dupFd, const_cast<void*>(blob.data()), !isMutable);
        SkSafeUnref(ctable);
        if (!nativeBitmap) {
            close(dupFd);
            blob.release();
            doThrowRE(env, "Could not allocate ashmem pixel ref.");
            return NULL;
        }

        // Clear the blob handle, don't release it.
        blob.clear();
    } else {

        // Copy the pixels into a new buffer.
        nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, bitmap.get(), ctable);
        SkSafeUnref(ctable);
        if (!nativeBitmap) {
            blob.release();
            doThrowRE(env, "Could not allocate java pixel ref.");
            return NULL;
        }
        bitmap->lockPixels();
        memcpy(bitmap->getPixels(), blob.data(), size);
        bitmap->unlockPixels();

        // Release the blob handle.
        blob.release();
    }

    return GraphicsJNI::createBitmap(env, nativeBitmap,
            getPremulBitmapCreateFlags(isMutable), NULL, NULL, density);
}

這個是與writeToParcel相互對應的,如果是asm則直接讀取檔案fd,如果是資料,則傳對應資料。

總結

上面就是Bitmap在Java層與native的表現,Bitmap的操作基本都是在native層,Java層與native層通過一個handle相互對應。在6.0Bitmap總共有四種儲存形式,也增加了asm的儲存。在進行Parcel傳輸的時候,針對asm,Parcel傳輸的fd,這樣能夠減少很多記憶體的消耗。在Android6.0內部,很多圖片也開始儲存在asm裡面了。不過在Java層還沒有提供將圖片儲存在匿名共享記憶體裡面。