1.出现异常
博主之前在跨进程传输文件的时候遇到过这样的异常,TransactionTooLargeException
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: android.os.TransactionTooLargeException: data parcel size 531824 bytes
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at android.os.BinderProxy.transactNative(Native Method)
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at android.os.BinderProxy.transact(Binder.java:771)
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at android.app.IApplicationThread$Stub$Proxy.scheduleLaunchActivity(IApplicationThread.java:1222)
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at com.android.server.am.ActivityStackSupervisor.realStartActivityLocked(ActivityStackSupervisor.java:1511)
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at com.android.server.am.ActivityStackSupervisor.startSpecificActivityLocked(ActivityStackSupervisor.java:1633)
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at com.android.server.am.ActivityStack.resumeTopActivityInnerLocked(ActivityStack.java:2830)
11-20 14:23:18.733 1000 1387 1664 W ActivityManager: at
从异常的堆栈当中可以看出,是跨进程传输的文件太大,而爆出了这个问题,文件的大小为531824 bytes,约为0.5Mb。
但是查看官方文档,可以看出Binder传输的buffer为1Mb,而我们传输的文件只有0.5Mb根本没有达到系统的限制,我们可以先看下抛出异常的流程。
The Binder transaction buffer has a limited fixed size, currently 1Mb,
https://developer.android.com/reference/android/os/TransactionTooLargeException?hl=zh-cn
2.异常分析
首先从调用栈可以看出异常从native层抛出,抛出异常的地方为android.os.BinderProxy.transactNative,对应的native代码如下。
从注释1出可以看出,当收到FAILED_TRANSACTION信号时,会去判断parcelSize是否大于200Kb,若大于,则会抛出TransactionTooLargeException,所以当我们传输的文件为0.5Mb的时候,此时传输失败,而文件又大于200Kb,因此抛出了这个异常。
static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
......
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
return JNI_FALSE;
}
void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
bool canThrowRemoteException, int parcelSize)
{
switch (err) {
......
//1.接受到FAILED_TRANSACTION
case FAILED_TRANSACTION: {
const char* exceptionToThrow;
char msg[128];
if (canThrowRemoteException && parcelSize > 200*1024) {
// 2.抛出TransactionTooLargeException
exceptionToThrow = "android/os/TransactionTooLargeException";
snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize);
} else {
exceptionToThrow = (canThrowRemoteException)
? "android/os/DeadObjectException"
: "java/lang/RuntimeException";
snprintf(msg, sizeof(msg)-1,
"Transaction failed on small parcel; remote process probably died");
}
jniThrowException(env, exceptionToThrow, msg);
} break;
......
}
}
接着我们再来看下FAILED_TRANSACTION这个信号是怎么产生的。
IPCThreadState::waitForResponse为和驱动层交互并等待驱动层回复的函数,我们可以看到在注释1处,当cmd为BR_FAILED_REPLY时,err为FAILED_TRANSACTION。
binder_transaction为binder驱动层函数,如注释2所示,当binder去申请buffer产生错误,则会返回BR_FAILED_REPLY
status_t IPCThreadState::waitForResponse(Parcel *reply, status_t *acquireResult)
{
......
switch (cmd) {
case BR_TRANSACTION_COMPLETE:
if (!reply && !acquireResult) goto finish;
break;
case BR_DEAD_REPLY:
err = DEAD_OBJECT;
goto finish;
//1.产生FAILED_TRANSACTION
case BR_FAILED_REPLY:
err = FAILED_TRANSACTION;
goto finish;
case BR_ACQUIRE_RESULT:
......
goto finish;
case BR_REPLY:
......
goto finish;
default:
err = executeCommand(cmd);
if (err != NO_ERROR) goto finish;
break;
}
}
......
return err;
}
static void binder_transaction(struct binder_proc *proc,
struct binder_thread *thread,
struct binder_transaction_data *tr, int reply,
binder_size_t extra_buffers_size)
{
......
//2.申请buffer出错,返回BR_FAILED_REPLY
t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
tr->offsets_size, extra_buffers_size,
!reply && (t->flags & TF_ONE_WAY));
if (IS_ERR(t->buffer)) {
/*
* -ESRCH indicates VMA cleared. The target is dying.
*/
return_error_param = PTR_ERR(t->buffer);
return_error = return_error_param == -ESRCH ?
BR_DEAD_REPLY : BR_FAILED_REPLY;
return_error_line = __LINE__;
t->buffer = NULL;
goto err_binder_alloc_buf_failed;
}
......
}
所以到这里我们就知道为什么会抛出TransactionTooLargeException异常了,首先我们在跨进程传输大文件的时候,会触发binder调用,此时binder驱动会去申请buffer,当buffer申请出错则会返回BR_FAILED_REPLY信号给native层,native层收到信号之后会首先判断当前的parcelSize是否大于200Kb,若大于则直接抛出TransactionTooLargeException给应用层。
另外官方文档给出的1Mb的缓存,是指在一个进程当中的所有事务当中所共用的缓存,事务是指client端向servier端发起binder调用,到servier端返回给client端的这整个过程,所以这也就解释在文章开头,博主所遇到的问题,明明发送的文件只有0.5Mb,还是爆出了这个异常。
3.解决方法
3.1解决方法
如下所示为我们常用的传输方式,直接将大文件放到bundle传输,这样很容易会触发这个异常
Bundle bundle = new Bundle();
bundle.putParcelable("Bitmap",mBitmap);
intent.putExtras(bundle);
但是用以下这种方式就可以解决此问题,IFile是一个AIDL接口,里面就只有一个方法getBitmap,远端收到请求之后,可以直接从bundle里面拿到binder对象,然后调用getBitmap方法,从而拿到大文件。
Bundle bundle = new Bundle();
bundle.putBinder("Bitmap", new IFile.Stub() {
@Override
public Bitmap getBitmap() throws RemoteException {
return mBitmap;
}
});
intent.putExtras(bundle);
3.2分析原因
我们来看下为什么会这样
首先我们来看一下startActivity的实现方法
如注释1处所示,intent的数据会被写入到parcel当中,调用intent的writeToParcel方法
然后如注释2所示,接着会调用parcel的writeBundle的方法,最后会调用到Bundle的writeToParcel方法,如注释4所示,会在这里将Bundle里面是否允许传输描述符的flag传递到parcel当中
//IAcitivityManager
@Override
public int startActivity(android.app.IApplicationThread caller, java.lang.String callingPackage, android.content.Intent intent, java.lang.String resolvedType, android.os.IBinder resultTo, java.lang.String resultWho, int requestCode, int flags, android.app.ProfilerInfo profilerInfo, android.os.Bundle options) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeStrongBinder((((caller != null)) ? (caller.asBinder()) : (null)));
_data.writeString(callingPackage);
if ((intent != null)) {
_data.writeInt(1);
//1.将需要传输的数据写到parcel当中
intent.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
......
if (!_status && getDefaultImpl() != null) {
return getDefaultImpl().startActivity(caller, callingPackage, intent, resolvedType, resultTo, resultWho, requestCode, flags, profilerInfo, options);
}
_reply.readException();
_result = _reply.readInt();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
//2.Intent
public void writeToParcel(Parcel out, int flags) {
......
out.writeBundle(mExtras);
}
//3.Bundle
public void writeToParcel(Parcel parcel, int flags) {
//4.是否允许传输描述符
final boolean oldAllowFds = parcel.pushAllowFds((mFlags & FLAG_ALLOW_FDS) != 0);
try {
super.writeToParcelInner(parcel, flags);
} finally {
parcel.restoreAllowFds(oldAllowFds);
}
}
我们再来看下真正将不同类型的数据写入到parcel的实现,如Bitmap
如下注释1所示,若parcel允许携带描述符,且图片有描述符,则直接将描述符写入parcel,否则如注释2所示,调用writeBlob方法。
如注释3所示,若parcel不允许携带或者图片小于16k,则直接将图片存于parcel,否则如注释4所示,将开辟一个共享内存,并将图片存于共享内存,将返回的描述符写入parcel。
static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject,
jlong bitmapHandle,
jboolean isMutable, jint density,
jobject parcel) {
......
android::status_t status;
int fd = bitmapWrapper->bitmap().getAshmemFd();
//1.若图片有描述符,且parcel允许携带,则直接将描述符写入parcel返回
if (fd >= 0 && !isMutable && p->allowFds()) {
status = p->writeDupImmutableBlobFileDescriptor(fd);
if (status) {
doThrowRE(env, "Could not write bitmap blob file descriptor.");
return JNI_FALSE;
}
return JNI_TRUE;
}
bool mutableCopy = isMutable;
size_t size = bitmap.computeByteSize();
android::Parcel::WritableBlob blob;
//2.否则调用parcel的writeBlob方法
status = p->writeBlob(size, mutableCopy, &blob);
if (status) {
doThrowRE(env, "Could not copy bitmap to parcel blob.");
return JNI_FALSE;
}
const void* pSrc = bitmap.getPixels();
if (pSrc == NULL) {
memset(blob.data(), 0, size);
} else {
memcpy(blob.data(), pSrc, size);
}
blob.release();
return JNI_TRUE;
}
status_t Parcel::writeBlob(size_t len, bool mutableCopy, WritableBlob* outBlob)
{
......
//3.若不允许携带描述符或者图片小于16K,则将图片存入parcel
if (!mAllowFds || len <= BLOB_INPLACE_LIMIT) {
ALOGV("writeBlob: write in place");
status = writeInt32(BLOB_INPLACE);
if (status) return status;
void* ptr = writeInplace(len);
if (!ptr) return NO_MEMORY;
outBlob->init(-1, ptr, len, false);
return NO_ERROR;
}
//4.允许携带描述符,且图片大于16K,则开辟共享内存
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(nullptr, len, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (ptr == MAP_FAILED) {
status = -errno;
} else {
if (!mutableCopy) {
result = ashmem_set_prot_region(fd, PROT_READ);
}
if (result < 0) {
status = result;
} else {
status = writeInt32(mutableCopy ? BLOB_ASHMEM_MUTABLE : BLOB_ASHMEM_IMMUTABLE);
if (!status) {
status = writeFileDescriptor(fd, true /*takeOwnership*/);
if (!status) {
outBlob->init(fd, ptr, len, mutableCopy);
return NO_ERROR;
}
}
}
}
::munmap(ptr, len);
}
::close(fd);
return status;
}
由此我们可以知道,使用共享内存的方式,几乎不会触发TransactionTooLargeException异常,而不是用共享内存,传输大文件,则很容易触发此异常。
我们再来看下,activity的启动流程,看是否在当中对携带描述符进行了设置。
如下代码所示,在注释2处,禁止携带描述符,所以当我们直接将图片放进bundle当中进行传输,并没有使用共享内存,因此很容易触发TransactionTooLargeException异常,而我们自己定义的跨进程binder则不会受此影响。
//Instrumentation
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
......
try {
intent.migrateExtraStreamToClipData();
//1.调用prepareToLeaveProcess
intent.prepareToLeaveProcess(who);
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
public void prepareToLeaveProcess(boolean leavingPackage) {
//2.禁止携带描述符
setAllowFds(false);
......
}
4.总结
这个异常看起来简单,但是仔细分析起来还是很复杂的,涉及到应用层,native层,驱动层,只有搞清楚了它的来龙去脉,我们才能够避免再犯类似的错误。