Android4.0 Bitmap Parcel传输源码分析

Posted _houzhi

tags:

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

很久之前就看到有网友遇到用Parcel传Bitmap的时候,会遇到因为图片太大而报错,都在讨论传输Bitmap的时候的大小限制,但是实际上应该只有在4.0之前会有限制,4.0之后图片传输的方式有变化,它采用了Blob来传输,最终会使用ashmem来传递占用内存大的数据。下面分别介绍4.0前后Parcel对图片传输的异同。

Parcel写入读取

先简单介绍一下Parcel的写入读取模式,Parcel是android中跨进程数据传递的中介,跨进程数据使用Parcel传递效率会比Serializable。Parcel提供了很多接口,比如writeInt,writeFloat,writeString,readInt,readFloat,readString等等,用这些接口可以读取写入数据.而实际上,Parcel里面有一个mData变量:

void* mData;

这个变量是一个指针类型,那些写入的接口都是将数据写入到这个指针变量指向的区域,读取也是从mData中读,写入和读取的数据相互对应。然后再将这个mData传入到Binder,或者是从Binder中读取出来。

2.3源码

在2.3中,Android Parcel传输图片是有大小限制的,实际上的限制应该是Binder对传输的数据大小的限制。Bitmap会对应的native层Parcel传输函数是Bitmap_writeToParcel,先看源码是怎么传输的:

static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
                                     const SkBitmap* bitmap,
                                     jboolean isMutable, jint density,
                                     jobject parcel) 
    if (parcel == NULL) 
        SkDebugf("------- writeToParcel null parcel\\n");
        return false;
    

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

    p->writeInt32(isMutable);
    p->writeInt32(bitmap->config());
    p->writeInt32(bitmap->width());
    p->writeInt32(bitmap->height());
    p->writeInt32(bitmap->rowBytes());
    p->writeInt32(density); //这些都是写入到Parcel的mData

    if (bitmap->getConfig() == SkBitmap::kIndex8_Config) 
        SkColorTable* ctable = bitmap->getColorTable();
        if (ctable != NULL) 
            int count = ctable->count();
            p->writeInt32(count);
            memcpy(p->writeInplace(count * sizeof(SkPMColor)),
                   ctable->lockColors(), count * sizeof(SkPMColor)); 
            ctable->unlockColors(false);
         else 
            p->writeInt32(0);   // indicate no ctable
        
    

    size_t size = bitmap->getSize();
    bitmap->lockPixels();
    memcpy(p->writeInplace(size), bitmap->getPixels(), size); //这个地方传输像素数据
    bitmap->unlockPixels();
    return true;

而Parcel的writeInplace方法很简单,就是根据传进去的位置,然后返回一个地址,这个地址是Parcel数据的地址,相当于当前应该写入的位置。得到地址后,再用memcpy把像素拷贝到Parcel中(mData)。这样相当于直接把数据拷贝到Parcel中。而Parcel传输数据如果大于当前的容量,会通过growData来增大容量,这个最大不要溢出整数的最大值,或者有存储空间可以分配,相当于正常情况下在Parcel没有限制数据大小:

void* Parcel::writeInplace(size_t len)

    const size_t padded = PAD_SIZE(len);

    // sanity check for integer overflow
    if (mDataPos+padded < mDataPos)  //不能超过整数大小
        return NULL;
    

    if ((mDataPos+padded) <= mDataCapacity) 
restart_write:  //不断增大容量
        //printf("Writing %ld bytes, padded to %ld\\n", len, padded);
        uint8_t* const data = mData+mDataPos;

         // Need to pad at end?
        if (padded != len) 

            //printf("Applying pad mask: %p to %p\\n", (void*)mask[padded-len],
            //    *reinterpret_cast<void**>(data+padded-4));
             *reinterpret_cast<uint32_t*>(data+padded-4) &= mask[padded-len];
        

        finishWrite(padded);
        return data;
    

    status_t err = growData(padded);
    if (err == NO_ERROR) goto restart_write;
    return NULL;

这样方式传输,会把像素数组全部传输到Binder驱动中,而导致如果图片太大出现一些FAILED BINDER TRANSACTION。大小的限制在Binder。

4.0源码

在4.0的源码中,Android的Parcel传输Bitmap的时候,会采用Blob来传输,Blob是用来传递占用内存很大的对象的,这是在native层的接口,如果4.0在Java层使用Parcel传递未提供的接口的数据的话,可以考虑用writeByteArray,在5.0中Java层增加了Blob接口。

先看Bitmap_writeTOParcel源码:

static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
                                     const SkBitmap* bitmap,
                                     jboolean isMutable, jint density,
                                     jobject parcel) 
    if (parcel == NULL) 
        SkDebugf("------- writeToParcel null parcel\\n");
        return false;
    

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

    ...前面部分是传输图片相关的一些特性,比如宽度,高度,颜色等等,与2.3一致

    size_t size = bitmap->getSize();
    //这里开始用blob传输
    android::Parcel::WritableBlob blob;
    android::status_t status = p->writeBlob(size, &blob);
    if (status) 
        doThrowRE(env, "Could not write bitmap to parcel blob.");
        return false;
    

    bitmap->lockPixels();
    const void* pSrc =  bitmap->getPixels(); //把像素copy到blob的指针,也就是blob里面
    if (pSrc == NULL) 
        memset(blob.data(), 0, size);
     else 
        memcpy(blob.data(), pSrc, size);
    
    bitmap->unlockPixels();

    blob.release();
    return true;

而Parcel的writeBlob用来写入Blob,它会根据数据量的大小来判断是否应该使用ashmem来传输。其源码如下:

status_t Parcel::writeBlob(size_t len, WritableBlob* outBlob)

    status_t status;

    if (!mAllowFds || len <= IN_PLACE_BLOB_LIMIT)  //IN_PLACE_BLOB_LIMIT 为40 * 1024
    //如果不允许fd共享内存文件传输,或者长度小于IN_PLACE_BLOB_LIMIT,则按照原来的方式传输
        LOGV("writeBlob: write in place");
        status = writeInt32(0); //未使用asm
        if (status) return status;

        void* ptr = writeInplace(len);
        if (!ptr) return NO_MEMORY;

        outBlob->init(false /*mapped*/, ptr, len); // Blob对应的地址为ptr,其实也是Parcel的地址
        return NO_ERROR;
    
    // 下面的就是通过ashmem(匿名共享内存)来传递数据,
    LOGV("writeBlob: write to ashmem");
    int fd = ashmem_create_region("Parcel Blob", len);
    if (fd < 0) return NO_MEMORY;

    int result = ashmem_set_prot_region(fd, PROT_READ | PROT_WRITE);
    if (result < 0) 
        status = result;
     else 
        void* ptr = ::mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
        if (ptr == MAP_FAILED) 
            status = -errno;
         else 
            result = ashmem_set_prot_region(fd, PROT_READ);
            if (result < 0) 
                status = result;
             else 
                status = writeInt32(1); //标记使用了asm传输
                if (!status) 
                    status = writeFileDescriptor(fd, true /*takeOwnership*/); //传递fd
                    if (!status) 
                        outBlob->init(true /*mapped*/, ptr, len); //如果成功,Blob将会对应asm中的内存位置。
                        return NO_ERROR;
                    
                
            
        
        ::munmap(ptr, len);
    
    ::close(fd);
    return status;

Parcel在4.0后增加Blob数据接口,用来传输占用内存大的数据,Blob传输具体的流程就是:如果数据量不超过IN_PLACE_BLOB_LIMIT或者不允许fd传输,则采用普通的方式,也就是直接将数据拷贝到Parcel里面;如果上面的条件不符合,则会采用asm来传输,也就是创建一个asm区域,然后把fd传入Parcel,把Blob对应的指针位置指向asm区域(通过mmap映射内存,最后直接将数据拷贝到这里面)。
另外需要说的writeBlob只是给Blob赋值了一个指针位置,这个指针或者是Parcel的mData中的某个位置,或者asm区域里面的指针(mmap得到),调用writeBlob的不用关心具体是哪个位置。

如果是普通方式传输,先写入一个0,如果是asm方式先写入一个1,读取的时候根据这个标志来判断是不是asm方式,具体可以看我代码的注释,下面是readBlob源码:

status_t Parcel::readBlob(size_t len, ReadableBlob* outBlob) const

    int32_t useAshmem;
    status_t status = readInt32(&useAshmem); //useAshmem是标志位,如果为0表示不使用ashmem传输
    if (status) return status;

    if (!useAshmem) 
        LOGV("readBlob: read in place");
        const void* ptr = readInplace(len);
        if (!ptr) return BAD_VALUE;

        outBlob->init(false /*mapped*/, const_cast<void*>(ptr), len);
        return NO_ERROR;
    

    LOGV("readBlob: read from ashmem");
    int fd = readFileDescriptor();
    if (fd == int(BAD_TYPE)) return BAD_VALUE;

    void* ptr = ::mmap(NULL, len, PROT_READ, MAP_SHARED, fd, 0);
    if (!ptr) return NO_MEMORY;

    outBlob->init(true /*mapped*/, ptr, len);
    return NO_ERROR;

也就是在4.0之后,如果允许fd传递的话,大数据量会通过ashmem来传递,而数据小的直接通过拷贝的方式,这样对于图片的大小限制也就小了很多。

总结思考

关于遇到Intent传递图片因为大小限制而报错FAILED BINDER TRANSACTION,应该是4.0之前的机器的。4.0之后正常情况下应该不会出错。

其实我在传递图片的时候,都是先保存在sdcard,然后再将图片的路径传递到另外的进程或不同的Activity,我觉得直接传递图片会导致占用内存太大。但是为什么Android内部还是提供了这样一个接口呢?

认真阅读了4.0之后的源码后,我发现从原理上面来看直接传递图片的效率和速度会更高,因为保存在sdcard会经过两次io(起码一次),而直接通过Parcel传递图片,那么直接在内存中操作,速度会高很多。另外我担心的内存占用太大其实多虑了。在4.0之后,如果图片小的话(40kb以内),不会太大影响,如果超过40kb,则会使用asm来传,不会占用到Java的堆内存,而且占用的内存传输完毕后就会释放了。即使是4.0之前,如果图片预计比较小,直接通过Parcel传递应该会好很多(也就是Activity之间直接把Bitmap放入Intent)。

以上是关于Android4.0 Bitmap Parcel传输源码分析的主要内容,如果未能解决你的问题,请参考以下文章

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

桌面小组件AppWidget - RemoteViews for widget update exceeds maximum bitmap memory usage

java.lang.RuntimeException:android.os.TransactionTooLargeException:data parcel size xxx bytes

Android群英传笔记系列二view的绘制

怎么将bitmap的RGB值传给二维数组

Android跨进程传大图思考及实现——附上原理分析