跨進程傳輸大文件

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層,驅動層,只有搞清楚了它的來龍去脈,我們才能夠避免再犯類似的錯誤。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章