Android4.0 Bitmap Parcel传输源码分析

原创 2016年05月29日 11:32:06

很久之前就看到有网友遇到用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)。

用Parcelable接口传递Bitmap

Android中Intent传递对象有两个方法,一个是让对象实现Serializable接口,另一个是让对象实现Parcelable接口,Sample的话Google搜一下就很多了。 大部分情况,S...
  • wike163
  • wike163
  • 2011年08月01日 17:45
  • 7719

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

如果想要对Android Bitmap进行更多的操作,理解好Bitmap的实现将会有非常大的帮助,另外Android在6.0中增加了asm存储图片。这篇文章就通过源码来分析Android6.0中的Bi...
  • xxx_zhi
  • xxx_zhi
  • 2016年05月24日 14:55
  • 6381

Parcel数据传输过程,简要分析Binder流程

Android Binder框架跨进程传输的,基本都是通过Parcel来封装的数据[注1],这个是十分底层的实现了。对于Framework开发来说,除了了解RPC的抽象意义外,对底层的具体通信细节的实...
  • freshui
  • freshui
  • 2017年02月13日 21:32
  • 1868

c印记(十): parcel(数据集,数据容器)实现

一、写在前面的话parcel是Android中的Framework层的一个数据结构,以c++实现,可以理解为数据集或者数据容器,在android将其作为函数的参数以及binder通信等,这里是仿照An...
  • xuanwolanxue
  • xuanwolanxue
  • 2017年03月27日 13:43
  • 319

Android Binder 分析——数据传递者(Parcel)

前面 binder 原理和通信模型中在接口实现部分(Bp 和 Bn)中应该看到很多地方都有使用 parcel。这个 android 专门设计用来跨进程传递数据的,实现在 native,java 层有接...
  • omnispace
  • omnispace
  • 2016年04月15日 09:25
  • 2862

Parcel_进程间数据传递

Parcel,即打包。为什么需要打包呢?是为了序列化。 如果要在进程之间传递一个整数,很简单,直接传就行了;如果要传一个字符串,就稍微复杂了点:需先分配一块可以容纳字符串的内存,然后将字符串复制到内...
  • dipperkun
  • dipperkun
  • 2013年05月11日 20:57
  • 2487

继webpack之后的又一打包神器parcel

序言: 在继grunt、gulp实现自动化构建之后,webpack又引领前端打包潮流,众所周知的是,vue-cli的脚手架就是 基于webpack进行项目打包的。而webpack还在上升的势头的时候,...
  • weixin_39654784
  • weixin_39654784
  • 2017年12月13日 14:23
  • 523

Android进程间通信(一):使用Parcel对象完成进程间通信

Parcel类官方定义:Container for a message (data and object references) that can be sent through an IBinder...
  • happy_horse
  • happy_horse
  • 2016年06月22日 12:50
  • 1388

全新打包工具parcel零配置vuejs开发脚手架

全新打包工具parcel零配置vue开发脚手架 parcel-vue 一个基于parcel打包工具的 vue开发脚手架解决方案,强烈建议使用node8.0以上 项目地址(求star^_...
  • qq_25058625
  • qq_25058625
  • 2018年01月09日 18:51
  • 91

Android 数据Parcel序列化过程源码分析

在Android系统中,所有的服务都必须注册到ServiceManger中,当客户进程需要请求某一服务时,首先从服务管家ServiceManger中查找出该服务,然后通过RPC远程调用的方式使用该服务...
  • yangwen123
  • yangwen123
  • 2013年06月21日 19:41
  • 5690
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:Android4.0 Bitmap Parcel传输源码分析
举报原因:
原因补充:

(最多只允许输入30个字)