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層還沒有提供將圖片儲存在匿名共享記憶體裡面。