在 Android进程间通信有很多种方法 , 其中跨进程 , 不同 activity , 以及 activity与 fragment通信 , 一般都会采用以下方式 :
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(ArrayMapval) { 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, 可以利用到共享内存,所以能高效传输 较大的数据 。