Android6.0 Bitmap存储以及Parcel传输

Posted _houzhi

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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层还没有提供将图片保存在匿名共享内存里面。

以上是关于Android6.0 Bitmap存储以及Parcel传输的主要内容,如果未能解决你的问题,请参考以下文章

Android拍照,相册选择图片以及Android6.0权限管理

Android6.0权限管理以及使用权限该注意的地方

Android6.0动态权限申请步骤以及需要注意的一些坑

利用videoView创建播放器,实现展示视频列表和搜索sd卡功能 以及android6.0的动态授权等功能

Android Bitmap深入介绍---基础

聊聊Android6.0 以上系统权限