Android跨进程传递大内存数据

背景

在主进程Activity 中选择或者编辑一张背景图产生一个bitmap 对象,要传递给 B进程,因为要尽量保证清晰度,所以这个bitmap还有可能比较大,所以必然会涉及到跨进程传输大型bitmap 的问题。

有哪些方案

跨进程传递大图,我们能想到哪些方案呢?

文件写入磁盘

最容易想到的方案就是先给图片保存到文件,给路径跨进程传过去,对方再从文件给图片decode出来,这个方案可行的,但是不够高效。

走系统IPC方式

另一种方案就是通过跨进程通信的方式,就是不走文件,直接走内存,这个肯定会快不少。跨进程通信有哪些方式呢?

首先Binder性能是可以,用起来也方便,但是有大小的限制,传的数据量大了就会抛异常。Socket或者管道性能不太好,涉及到至少两次拷贝。共享内存性能还不错,可以考虑,关键看怎么实现。
我们来看,通过Binder传图,一个是通过Intent传图,还一个可以通过Binder调用传图。这两个不是一回事?你可能会有疑问,那么我们具体来看下这两种有什么区别。

先看常规Intent传图,我们塞进去一个超大的图。
这个

Intent intent = new Intent(this, SecondActivity.class);
intent.putExtra("bitmap", mBitmap);
startActivity(intent);

bitmap 是一张很大的图片,正常传大图 抛出如下异常:

2019-07-27 20:40:38.981 19460-19460/io.github.brijoe.ipcbigbitmapdemo E/AndroidRuntime: FATAL EXCEPTION: main
    Process: io.github.brijoe.ipcbigbitmapdemo, PID: 19460
		 ...
     Caused by: android.os.TransactionTooLargeException: data parcel size 14293996 bytes
       ...

那我们看下系统对这个异常是怎么解释的

理解TransactionTooLargeException

 

The Binder transaction failed because it was too large. During a remote procedure call, the arguments and the return value of the call are transferred as Parcel objects stored in the Binder transaction buffer. If the arguments or the return value are too large to fit in the transaction buffer, then the call will fail and TransactionTooLargeException will be thrown. The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process. Consequently this exception can be thrown when there are many transactions in progress even when most of the individual transactions are of moderate size.

上面这段来自官方文档的描述,大致有这么几层意思:

  1. 此异常发生在Binder IPC调用过程中.
  2. 客户端发送请求数据过大 服务端返回数据过大 都会触发 此异常。
  3. binder调用的缓冲buffer大小当前为1M ,由当前进程共享。因此,如果同时有多个调用,就算单个调用过程传输的数据不大,也有可能触发此异常。
  4. 可以将数据打碎分片成小数据来避免此异常。

很显然 由于我们传递的bitmap过大,导致了缓冲区超限,所以触发了此异常。那我们有没有办法能规避这个异常呢?我们来继续看。

神奇的putBinder 方法

再看Binder调用传图,是往Intent里塞了个Binder对象,等到另一个组件启动之后,读出这个Binder对象,调用它的getBitmap函数拿到Bitmap。
注意下,putBinder 方式 只在Android 4.3 (api 18)及以上才有,所以我们做下判断。

发送端:

 private void ipcBitmapBinder() {
        Intent intent = new Intent(this, SecondActivity.class);
        Bundle bundle = new Bundle();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            bundle.putBinder("bitmap", new ImageBinder(mBitmap));
        }
        intent.putExtras(bundle);
        startActivity(intent);
    }

接收端

Bundle bundle = getIntent().getExtras();
        if (bundle != null) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
                ImageBinder imageBinder = (ImageBinder) bundle.getBinder("bitmap");
                Bitmap bitmap = imageBinder.getBitmap();
                mTv.setText(String.format(("bitmap大小为%dkB"), bitmap.getByteCount() / 1024));
                mIv.setImageBitmap(bitmap);
            }

        }

ImageBinder 是个继承自Binder的类,提供获取图片的方式。

class ImageBinder extends Binder {
    private Bitmap bitmap;

    public ImageBinder(Bitmap bitmap) {
        this.bitmap = bitmap;
    }

    Bitmap getBitmap() {
        return bitmap;
    }
}

点击启动按钮,Activity 正常跳转。我们将10几兆的图片通过跨进程的方式传过去了,避免了写磁盘的窘境。

       

源码分析

这两个实现上有什么区别么?我们来看一下源码,就从startActivity开始吧,

int startActivity(..., Intent intent, ...) {    
    Parcel data = Parcel.obtain();
    ......  
    intent.writeToParcel(data, 0);    
    ......
    mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0);  
    ......
}

我们重点关注Bitmap是怎么传输的,这里给Intent写到Parcel了,通过下面这个writeToParcel函数,其实就是给Intent里的Bundle写到Parcel了,

public void writeToParcel(Parcel out, int flags) {    
    ......    
    out.writeBundle(mExtras);
}

继续往下走,看Bundle怎么写到Parcel的,原来是调到了Bundle的writeToParcel函数,

public final void writeBundle(Bundle val) {    
    val.writeToParcel(this, 0);
}

继续往下走,又调到了writeToParcelInner,

public void writeToParcel(Parcel parcel, int flags) {    
    final boolean oldAllowFds = parcel.pushAllowFds(mAllowFds); 
    super.writeToParcelInner(parcel, flags);    
    parcel.restoreAllowFds(oldAllowFds);
}

这个pushAllowFds是啥呢?就是说如果Bundle里不允许带描述符,那Bundle写到Parcel里的时候,Parcel也不许带描述符了。

bool Parcel::pushAllowFds(bool allowFds) {    
    const bool origValue = mAllowFds;    
    if (!allowFds) {        
        mAllowFds = false;    
    }    
    return origValue;
}
我们再看writeToParcelInner函数,大家耐心一点,马上就要接近真相了,
void writeToParcelInner(Parcel parcel, int flags) {    
    ......    
    parcel.writeArrayMapInternal(mMap);
}

这里调到了Parcel的writeArrayMapInternal函数,Bundle其实核心就是一个ArrayMap。写Bundle就是写这个ArrayMap。我们看这个Map是怎么写到Parcel的。

void writeArrayMapInternal(ArrayMap<String, Object> val) {      
    final int N = val.size();   
    writeInt(N);    
    for (int i = 0; i < N; i++) {        
        writeString(val.keyAt(i));        
        writeValue(val.valueAt(i));    
    }
}

这逻辑很简单啊,就是在一个for循环里给map的key和value依次写到parcel。我们看writeValue是怎么写的,里面会根据value的不同类型采取不同的写法,

public final void writeValue(Object v) {    
    ......     
    else if (v instanceof Parcelable) {        
        writeInt(VAL_PARCELABLE);        
        writeParcelable((Parcelable) v, 0);    
    }     
    ......
}

因为Bitmap是Parcelable的,所以我们只关注这个分支,这又调到了Bitmap的writeToParcel函数,

void writeParcelable(Parcelable p, int parcelableFlags) {    
    writeParcelableCreator(p);    
    p.writeToParcel(this, parcelableFlags);
}

我们继续看Bitmap的writeToParcel,这又进入了native层,

public void writeToParcel(Parcel p, int flags) {    
    nativeWriteToParcel(mFinalizer.mNativeBitmap, ...);
}

我们看native层是怎么实现的,

jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ...) {    
    android::Bitmap* androidBitmap = reinterpret_cast<Bitmap*>(bitmapHandle);    
    androidBitmap->getSkBitmap(&bitmap);  
     
    // 往parcel里写Bitmap的各种配置参数    
     
    int fd = androidBitmap->getAshmemFd();    
    if (fd >= 0 && !isMutable && p->allowFds()) {        
        status = p->writeDupImmutableBlobFileDescriptor(fd);        
        return JNI_TRUE;    
    }    
     
    android::Parcel::WritableBlob blob;    
    status = p->writeBlob(size, mutableCopy, &blob);    
    const void* pSrc =  bitmap.getPixels();    
    memcpy(blob.data(), pSrc, size);
}

这里首先拿到native层的Bitmap对象,叫androidBitmap,然后拿到对应的SkBitmap。先看bitmap里带不带ashmemFd,如果带,并且这个Bitmap不能改,并且Parcel是允许带fd的话,就给fd写到parcel里,然后返回。否则的话继续往下,先有个WriteBlob对象,通过writeBlob函数给这个blob在parcel里分配了一块空间,然后给bitmap拷贝到这块空间里。我们看这个writeBlob函数,

status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob) {    
    if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {        
        status = writeInt32(BLOB_INPLACE);        
        void* ptr = writeInplace(len);        
        outBlob->init(-1, ptr, len, false);        
        return NO_ERROR;    
    }    
    int fd = ashmem_create_region("Parcel Blob", len);    
    void* ptr = mmap(NULL, len, ..., MAP_SHARED, fd, 0);    
    ......    
    status = writeFileDescriptor(fd, true);    
    outBlob->init(fd, ptr, len, mutableCopy);    
    return NO_ERROR;
}

这个writeBlob函数,首先看如果不允许带fd,或者这个数据小于16K,就直接在parcel的缓冲区里分配一块空间来保存这个数据。不然的话呢,就另外开辟一个ashmem,映射出一块内存,数据就保存在ashmem的内存里,parcel里只写个fd就好了,这样就算数据量很大,parcel自己的缓冲区也不用很大。

为何putBinder 就不会抛异常?

bitmap的传输原理咱们清楚了,但是还有一个问题没有解决,为什么intent带大图会异常,但是binder调用带大图就没事呢?肯定是因为intent带bitmap的时候,bitmap直接拷到parcel缓冲区了,没有利用这个ashmem。为什么呢?

咱们注意到,只可能是这个allowFds没打开,咱们研究一下。

startActivity的时候会调到execStartActivity,这里会调到prepareToLeaveProcess,里面会禁用intent的allowFds,sendBroadcast也会这样,bindService也一样哈。

public ActivityResult execStartActivity(..., Intent intent, ...) {    
    ......    
    intent.prepareToLeaveProcess();    
    ActivityManagerNative.getDefault().startActivity(...);
}

至于为什么应用组件通信的时候要专门禁用这个fd,暂时没有找到特别可靠的解释,感觉应该是为了安全性考虑。

总结

Intent普通传大图方式为啥会抛异常而putBinder 为啥可以?
较大的bitmap直接通过Intent传递 是因为Intent启动组件时,系统禁掉了文件描述符fd,bitmap无法利用共享内存,只能采用拷贝到缓冲区的方式,导致缓冲区超限,触发异常;putBinder 的方式,避免了intent 禁用描述符的影响,bitmap 写parcel时的fd 默认是true,可以利用到共享内存,所以能高效传输图片。

 

转载:https://blog.csdn.net/ylyg050518/article/details/97671874

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值