跨进程传输大文件

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层,驱动层,只有搞清楚了它的来龙去脉,我们才能够避免再犯类似的错误。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android进程数据传输可以通过使用Binder机制实现。Binder是一种进程通信的机制,它允许一个进程调用另一个进程的方法,同时也支持进程传输数据。下面是一个简单的演示: 1.定义一个AIDL接口文件,例如IMyAidlInterface.aidl,其中包含需要进程调用的方法: ```aidl interface IMyAidl { int add(int a, int b); } ``` 2.在服务端实现该接口: ```java public class MyService extends Service { private final IMyAidlInterface.Stub mBinder = new IMyAidlInterface.Stub() { @Override public int add(int a, int b) throws RemoteException { return a + b; } }; @Nullable @Override public IBinder onBind(Intent intent) { return mBinder; } } ``` 3.在客户端绑定服务并调用接口方法: ```java private ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { IMyAidlInterface myAidlInterface = IMyAidlInterface.Stub.asInterface(iBinder); try { int result = myAidlInterface.add(1, 2); Log.d(TAG, "result: " + result); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName componentName) { } }; Intent intent = new Intent(); intent.setComponent(new ComponentName("com.example.myapp", "com.example.myapp.MyService")); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); ``` 在上述代码中,客户端通过bindService()方法绑定服务,然后通过IMyAidlInterface接口调用服务端的add()方法,最后得到结果并打印。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值