Android跨进程传输大数据解决方案

Android进程间通信有很多种方法 其中跨进程 不同 activity 以及 activityfragment通信 一般都会采用以下方式

val intent = Intent(StartActivity.this,SecondActivity::class.java)
intent.putExtra("key","value")
startActivity(intent)

(以activity举例)

如果我们要传递的值无法直接传递 ,可以 采用序列化和反序列化的方案

val args = Bundle().apply {
   putString(“data”, movieType.toJson())
}
val fragment = Fragment()
fragment.arguments = args

(以fragment举例)

直接序列化相关字段 在数据较小时没有问题 极限大概在不到1m 就会出现问题 处理这个问题让我想起来之前了解到的 putbinder 处理大型图片的方案 其实用来处理这些变量也是可以的

具体方案是先定义一个通用的数据类型

class DataBinder(val binderData: E):Binder()

在传值使用Bundle 的形式

val binder = DataBinder(data)
val bundle = Bundle()
bundle.putBinder(“data”,binder)
val intent = Intent(context, Activity::class.java)
intent.putExtras(bundle)
context.startActivity(intent)

在接收端读取bunder接收数据

intent.extras?.let {
   val binder = it.getBinder(“data”)?:return
   val data =  binder as DataBinder
}

当然这个时候会有个警告

使用抑制即可(当然,无所谓的可以不管他)

@Suppress("UNCHECKED_CAST")

这是因为我们采用了通用的数据类型 如果觉得这样不够优雅 可以针对每一种数据类型写一个 DataBinder

经过测试 这样传值20m 的图片都可以正常运行(传图片检验的方式最简单)

那么为什么 大数据会报错呢? 首先来看一下 如果直接序列化 报错会执行的代码

/**
 * Native implementation of transact() for proxies
*/
public native boolean transactNative(int code, Parcel data, Parcel reply,
            int flags) throws RemoteException;

是一个 native层代码 去搜一下相关代码

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    ......
    status_t err = target->transact(code, *data, reply, flags);
    ......
    if (err == NO_ERROR) {
        //如果匹配成功直接拦截不往下面执行了
        return JNI_TRUE;
    } else if (err == UNKNOWN_TRANSACTION) {
        return JNI_FALSE;
    }
    signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
    return JNI_FALSE;
}

android_util_Binder.cpp

我们打开 signalExceptionForError方法看看里面的内容

void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
        bool canThrowRemoteException, int parcelSize)
{
    switch (err) {
        case FAILED_TRANSACTION: {
            const char* exceptionToThrow;
            char msg[128];
            //parcelSize大于200K就会报错,canThrowRemoteException传递进来的是true
            if (canThrowRemoteException && parcelSize > 200*1024) {
                // bona fide large payload
                exceptionToThrow = "android/os/TransactionTooLargeException";
                snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize);
            } else {
                ..........
            }
            jniThrowException(env, exceptionToThrow, msg);
        } break;
        ........
    }
}

官方 TransactionTooLargeException的文档中描述到: Binder 事务缓冲区有一个有限的固定大小,目前为  1M,由进程所有正在进行的事务共享 而源码中当数据超过 200k 时就会告诉我们“ Transaction too large”只是这个时候没有超出异步事物空闲的缓冲区大小


那么 putbinder 干了什么呢

来看一下执行的流程,快速掠过前面的,直接来看后面

startActivity --> intent.writeToParcel(data, 0) --> out.writeBundle(mExtras) --> val.writeToParcel(this, 0)

public void writeToParcel(Parcel parcel, int flags) {    
    final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0);
     try {
        //这里官方注释已经写的很详细了:
        //将Bundle内容写入Parcel,通常是为了让它通过IBinder连接传递
        super.writeToParcelInner(parcel, flags);
     } finally {
        //把mAllowFds值设置回来
        parcel.restoreAllowFds(oldAllowFds);
     }
}

bundle


再继续往下看

bool Parcel::pushAllowFds(bool allowFds) {    
    const bool origValue = mAllowFds;    
    if (!allowFds) {        
        mAllowFds = false;    
    }    
    return origValue;
}

到这步 出现了一个描述符

 

再往下走 走到了writeArrayMapInternal 这逻辑很简单啊,就是在一个for 循环里给map key value 依次写到parcel

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

再往下就到了 native层的 这里以 bitmap举例

jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ...) {    
    android::Bitmap* androidBitmap = reinterpret_cast(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;
}

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




请使用浏览器的分享功能分享到微信等