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