Android Bitmap理解

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android Bitmap理解相关的知识,希望对你有一定的参考价值。

参考技术A

参考:
android Bitmap 详解:关于 Bitamp 你所要知道的一切

Android Bitmap(位图)详解

图片是由大量且有限个数的像素点组成。把一张图片通过bitmap的方式创建到内存中,实际上就是在内存中创建了一个叫做Bitmap的对象,然后把 图片所有像素 解码后的数据存放在Bitmap对象里面,Bitmap就拥有了图片的宽高,透明度,颜色值等数据。所以Bitmap的创建是通过BitmapFactory.decodeXxx()。

Config是Bitmap类中的枚举类。像素由ARGB四个颜色通道组成。Config描述位图中像素的存储方式。 这里的存储方式,无非就是对颜色通道和用多大的容器(bit)来存储的排列组合。所以config会影响图片透明度,占用内存大小,保存成文件的大小,图片质量。
Config的字母表示该配置存储的像素的颜色通道,数字表示对应通道的数据用多少位来存储。

ALPHA_8:表示只存储alpha通道,使用8bit(1字节)的内存(容器)来存储一个像素。
RGB_565:表示存储RGB三个通道,分别使用5bit,6bit,5bit的内存(容器)来存储一个像素。
ARGB_4444:表示存储ARGB四个通道,每个通道都是以4bit的内存(容器)来存储一个像素。
ARGB_8888:表示存储ARGB四个通道,每个通道都是以8bit的内存(容器)来存储一个像素。

所以,ARGB_8888配置占用内存最大,图片质量最高。
图片压缩的一个思路就是降低图片的配置。

总内存 = 宽的像素数 × 高的像素数 × 每个像素点占用的大小
注:
1 byte = 8 bit
1 KB = 1024 byte

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层还没有提供将图片保存在匿名共享内存里面。

以上是关于Android Bitmap理解的主要内容,如果未能解决你的问题,请参考以下文章

Android:安卓学习笔记之Bitmap的简单理解和使用

Android自学日记关于Bitmap的理解和使用-不完整版

Android6.0 Bitmap存储以及Parcel传输

Android6.0 Bitmap存储以及Parcel传输源码分析

Android开发:对View DrawingCache的理解

Android 根据目标宽度,将bitmap等比缩放。