Android4.0之后Parcel传输Bitmap源码分析

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

1
void* mData;

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

2.3源码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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没有限制数据大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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驱动中,而导致如果图片太大出现一些

BINDER TRANSACTION```。大小的限制在Binder。
1
2
3
4
5
6
## 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;

}

1
2
3
而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;

}

1
2
3
4
5
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)。