Android TransactionTooLargeException 解析浴鸿,思考與監(jiān)控方案

最近公司遇到了一個很有意思的 Crash:android.os.TransactionTooLargeException,這個 Crash 大家可能見到過腾务,錯誤堆棧的信息多種多樣,下面是其中的常見錯誤堆棧信息之一:

#1 main
android.os.TransactionTooLargeException
java.lang.RuntimeException:Adding window failed
android.view.ViewRootImpl.setView(ViewRootImpl.java:515)
......
Caused by:
android.os.TransactionTooLargeException:
android.os.BinderProxy.transact(Native Method)
android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:684)
android.view.ViewRootImpl.setView(ViewRootImpl.java:504)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
android.app.Dialog.show(Dialog.java:307)

這個是什么引起的呢削饵?其實可能是一段很簡單的代碼岩瘦,類似于:

Dialog dialog = XXX;
......
dialog.show();

就是這么一段簡單的 dialog.show() 就會導(dǎo)致崩潰,具體原因我在下面會詳細介紹到窿撬,我們先會通過分析三段 Crash 日志調(diào)用信息來定位原因担钮,然后提出解決辦法。
  轉(zhuǎn)載請注明出處:http://blog.csdn.net/self_study/article/details/60136277
  對技術(shù)感興趣的同鞋加群 544645972 一起交流尤仍。

TransactionTooLargeException 分析與解決

我們來仔細分析一下這個 Exception 的錯誤堆棧信息箫津,由于這里面涉及到了 AIDL 以及 WMS,AMS的相關(guān)知識宰啦,這里列出對應(yīng)相關(guān)的博客苏遥,下面的分析會直接使用到這些內(nèi)容:
android 不能在子線程中更新ui的討論和分析:Activity 打開的過程分析;
java/android 設(shè)計模式學習筆記(9)---代理模式:AMS 的相關(guān)類圖和介紹赡模;
android WindowManager解析與騙取QQ密碼案例分析:界面 window 的創(chuàng)建過程田炭;
java/android 設(shè)計模式學習筆記(8)---橋接模式:WMS 的相關(guān)類圖和介紹;
android IPC通信(下)-AIDL:AIDL 以及 Binder 的相關(guān)介紹漓柑;
Android 動態(tài)代理以及利用動態(tài)代理實現(xiàn) ServiceHook:ServiceHook 的相關(guān)介紹教硫;
Android TransactionTooLargeException 解析,思考與監(jiān)控方案:TransactionTooLargeException 的解析以及監(jiān)控方案辆布。

TransactionTooLargeException StackTrace 分析

我們這里先分析一下上面那段 Exception 的調(diào)用棧瞬矩,這里直接摘取了其中的方法調(diào)用部分:

android.view.ViewRootImpl.setView(ViewRootImpl.java:515)
android.os.BinderProxy.transact(Native Method)
android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:684)
android.view.ViewRootImpl.setView(ViewRootImpl.java:504)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)
android.app.Dialog.show(Dialog.java:307)

從最底下開始,我們一步步分析锋玲,首先第一個是 dialog.show() 函數(shù)景用,這個是我們應(yīng)用層用來顯示一個 Dialog 的方法,很正常惭蹂,對吧伞插,然后下一句:

android.view.WindowManagerImpl.addView(WindowManagerImpl.java:69)

我在博客:android WindowManager解析與騙取QQ密碼案例分析中介紹到,Dialog Window 的創(chuàng)建和 Activity 類似盾碗,也是需要調(diào)用 PolicyManager.makeNewWindow 去創(chuàng)建一個 Window媚污,然后通過 WindowManager 將該 Window 的 DecorView 添加到 Activity 的 Window 中就能顯示出來了,與 Activity 最大的區(qū)別就是 Dialog 的 Window 需要一個 Activity 的句柄廷雅,因為需要依附在 Activity 上面耗美,而 Toast 這種系統(tǒng) Window 則可以直接顯示氢伟,這三種 Window 有著不同的層級范圍,層級大的 Window 會覆蓋在層級小的 Window 之上幽歼,應(yīng)用window的層級范圍是 1~99朵锣,子 Window 的范圍是 1000~1999,系統(tǒng) Window 的范圍是 2000~2999甸私。所以說 Dialog.show() 會調(diào)用相關(guān)的函數(shù)去創(chuàng)建 Window 诚些,而 Dialog 創(chuàng)建 Window 的過程我們可以參考 Activity 創(chuàng)建 Window 的過程,第一步會調(diào)用到 WindowManagerImpl 類中的 addView 函數(shù)去添加上面 new 出來的那個 Window 對象皇型,而 WindowManager 和 Window 類是一個典型的橋接模式诬烹,具體的可以看看我的博客:java/android 設(shè)計模式學習筆記(8)---橋接模式,下面為 uml 類圖:</br>

這里寫圖片描述
</br>
WindowManagerImpl 類持有一個 WindowManagerGlobal 類的引用弃鸦,所有的操作都交給了 WindowManagerGlobal 類绞吁, WindowManagerGlobal 里面會調(diào)用到 ViewRootImpl 類的 setView 方法,而這個函數(shù)里面會調(diào)用 IWindowSession 類唬格,這個 IWindowSession 類的對象 sWindowSession 是通過 IWindowManager 的 openSession 函數(shù)獲取的家破,而 IWindowManager 其實就是 WindowManagerService 在應(yīng)用進程的 Proxy 類對象,它持有了 WMS 的 IBinder 對象购岗,通過 AIDL 調(diào)用到主進程的 WMS 中汰聋,WMS 的 openSession 方法返回的是一個 IWindowSession.Stub 類的對象,但是由于跨進程了喊积,所以系統(tǒng)進程返回的 IWindowSession.Stub 對象在應(yīng)用進程中就對應(yīng)為 IWindowSession 的 IBinder 對象烹困,最后同理需要調(diào)用 IWindowSession.Stub.asInterface 函數(shù)轉(zhuǎn)成 Proxy 對象,具體的代碼如下所示:

@Override
public android.view.IWindowSession openSession(android.view.IWindowSessionCallback callback, com.android.internal.view.IInputMethodClient client, com.android.internal.view.IInputContext inputContext) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    android.view.IWindowSession _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeStrongBinder((((callback != null)) ? (callback.asBinder()) : (null)));
        _data.writeStrongBinder((((client != null)) ? (client.asBinder()) : (null)));
        _data.writeStrongBinder((((inputContext != null)) ? (inputContext.asBinder()) : (null)));
        mRemote.transact(Stub.TRANSACTION_openSession, _data, _reply, 0);
        _reply.readException();
        _result = android.view.IWindowSession.Stub.asInterface(_reply.readStrongBinder());
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}

這樣就轉(zhuǎn)換為了 IWindowSession$Stub$Proxy 對象乾吻,為什么調(diào)用到的是 BinderProxy 類的 transact 方法呢髓梅?android IPC通信(下)-AIDL博客中我已經(jīng)介紹到了,應(yīng)用進程通過 ServiceManager 獲取到的 WMS 的 IBinder 對象其實就是 BinderProxy 對象绎签,這里的 IWindowSession 也是類似的枯饿,所以調(diào)用到了 BinderProxy 對象中的 transact 方法,而這個方法:

public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    Binder.checkParcel(this, code, data, "Unreasonably large binder buffer");
    if (Binder.isTracingEnabled()) { Binder.getTransactionTracker().addTrace(); }
    return transactNative(code, data, reply, flags);
}
....
public native boolean transactNative(int code, Parcel data, Parcel reply,
        int flags) throws RemoteException;

這個方法就調(diào)用到了 native 方法中辜御,全局搜索一下鸭你,這個方法對應(yīng)于 native 的 android_os_BinderProxy_transact 方法,這個方法是關(guān)鍵:

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    if (dataObj == NULL) {
        jniThrowNullPointerException(env, NULL);
        return JNI_FALSE;
    }

    Parcel* data = parcelForJavaObject(env, dataObj);
    if (data == NULL) {
        return JNI_FALSE;
    }
    Parcel* reply = parcelForJavaObject(env, replyObj);
    if (reply == NULL && replyObj != NULL) {
        return JNI_FALSE;
    }

    IBinder* target = (IBinder*)
        env->GetLongField(obj, gBinderProxyOffsets.mObject);
    if (target == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!");
        return JNI_FALSE;
    }

    ALOGV("Java code calling transact on %p in Java object %p with code %" PRId32 "\n",
            target, obj, code);


    bool time_binder_calls;
    int64_t start_millis;
    if (kEnableBinderSample) {
        // Only log the binder call duration for things on the Java-level main thread.
        // But if we don't
        time_binder_calls = should_time_binder_calls();

        if (time_binder_calls) {
            start_millis = uptimeMillis();
        }
    }

    //printf("Transact from Java code to %p sending: ", target); data->print();
    status_t err = target->transact(code, *data, reply, flags);
    //if (reply) printf("Transact from Java code to %p received: ", target); reply->print();

    if (kEnableBinderSample) {
        if (time_binder_calls) {
            conditionally_log_binder_call(start_millis, target, code);
        }
    }

    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;
}

其中調(diào)用到了 signalExceptionForError 方法:

void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
        bool canThrowRemoteException, int parcelSize)
{
    switch (err) {
        case UNKNOWN_ERROR:
            jniThrowException(env, "java/lang/RuntimeException", "Unknown error");
            break;
        ......
        case FAILED_TRANSACTION: {
            ALOGE("!!! FAILED BINDER TRANSACTION !!!  (parcel size = %d)", parcelSize);
            const char* exceptionToThrow;
            char msg[128];
            // TransactionTooLargeException is a checked exception, only throw from certain methods.
            // FIXME: Transaction too large is the most common reason for FAILED_TRANSACTION
            //        but it is not the only one.  The Binder driver can return BR_FAILED_REPLY
            //        for other reasons also, such as if the transaction is malformed or
            //        refers to an FD that has been closed.  We should change the driver
            //        to enable us to distinguish these cases in the future.
            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 {
                // Heuristic: a payload smaller than this threshold "shouldn't" be too
                // big, so it's probably some other, more subtle problem.  In practice
                // it seems to always mean that the remote process died while the binder
                // transaction was already in flight.
                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;
        .......
    }
}

于是我們就看到了關(guān)鍵的一句話:

exceptionToThrow = "android/os/TransactionTooLargeException";
snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize);

沒錯擒权,這就是錯誤的來源,這里判斷如果這個 parcelSize 大于 200K 就會報錯阁谆,而這個 parcelSize 的大小碳抄,對應(yīng)一下,發(fā)現(xiàn)就是 BinderProxy 的第二個參數(shù)场绿,也就是說如果 Percel 對象的大小超過 200K 就會報出這個錯誤剖效,而這個參數(shù)的大小就是應(yīng)用進程傳遞給主進程的參數(shù)大小,而應(yīng)用進程傳遞給主進程的參數(shù)對應(yīng)的就是 Dialog 的相關(guān)參數(shù),比如 Message 或者 Title 等等璧尸,如果這些參數(shù)過大的話咒林,就會出現(xiàn)這個崩潰,解決辦法就是就是將 Dialog 的相關(guān)參數(shù)變小爷光,可是這真的是解決辦法嘛垫竞,不一定,咱們繼續(xù)看蛀序。

同類 Crash

上面的 Dialog.show() 引發(fā)的 Crash 只是冰山一角欢瞪,因為我們知道調(diào)用 WMS 服務(wù)的時候,transact 函數(shù)的參數(shù)如果過大就會崩潰徐裸,那么 AMS遣鼓,PMS呢?答案是肯定的重贺,我們來看看我司的相關(guān)同類 Crash:

PMS 檢查權(quán)限

java.lang.reflect.UndeclaredThrowableException:
$Proxy2.checkPermission(Unknown Source)
......
Caused by:
android.os.TransactionTooLargeException:
android.os.BinderProxy.transactNative(Native Method)
android.os.BinderProxy.transact(Binder.java:504)
android.content.pm.IPackageManager$Stub$Proxy.checkPermission(IPackageManager.java:2169)
java.lang.reflect.Method.invoke(Native Method)
java.lang.reflect.Method.invoke(Method.java:372)
androidx.pluginmgr.hook.PackageManagerHook$HookHandler.invoke(PackageManagerHook.java:99)
java.lang.reflect.Proxy.invoke(Proxy.java:397)
$Proxy2.checkPermission(Unknown Source)
android.app.ApplicationPackageManager.checkPermission(ApplicationPackageManager.java:401)
com.lidroid.xutils.util.DeviceInfoUtils.checkPermissions(DeviceInfoUtils.java:315)

可以看到這個是由 PackageManager.checkPermission 引起的骑祟,而這個會最終會調(diào)用到 ApplicationPackageManager 類的 checkPermission 函數(shù)里面,這個函數(shù):

@Override
public int checkPermission(String permName, String pkgName) {
    try {
        return mPM.checkPermission(permName, pkgName, mContext.getUserId());
    } catch (RemoteException e) {
        throw e.rethrowFromSystemServer();
    }
}

這個函數(shù)調(diào)用到了 mPM 變量的 checkPermission 方法中气笙,這個變量是 IPackageManager 類型曾我,因為我司的插件化框架的緣故,所以這個變量是被修改過的健民,具體的可以看看我的博客:Android 動態(tài)代理以及利用動態(tài)代理實現(xiàn) ServiceHook抒巢,這個變量最終被修改為一個動態(tài)生成類的對象,博客里面我介紹到這個類的名字格式為 $ProxyXXX秉犹,后面的 XXX 為具體的數(shù)字蛉谜,所以緊接著就調(diào)用到了這個動態(tài)生成類的 checkPermission 函數(shù)里面,然后調(diào)用到 InvocationHandler 類的 invoke 方法里面崇堵,對應(yīng)的就是 PackageManagerHook 類的內(nèi)部類 HookHandler 的 invoke 方法型诚,最終會調(diào)用到 IPackageManager 的 Proxy 對象中,對應(yīng)的就是 IPackageManager$Stub$Proxy 這個角色鸳劳,這個角色會調(diào)用 IBinder 對象的狰贯,也就是 BinderProxy 的 transact 方法,最終的調(diào)用過程也就是和上面 WMS 的類似了赏廓。</br>

AMS -> WMS 啟動應(yīng)用或者打開頁面

還有另外的比如:

java.lang.RuntimeException:Adding window failed
android.view.ViewRootImpl.setView(ViewRootImpl.java:559)
......
Caused by:
android.os.TransactionTooLargeException:
android.os.BinderProxy.transact(Native Method)
android.view.IWindowSession$Stub$Proxy.addToDisplay(IWindowSession.java:683)
android.view.ViewRootImpl.setView(ViewRootImpl.java:548)
android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:259)
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3394)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2658)
android.app.ActivityThread.access$800(ActivityThread.java:156)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1355)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:157)
android.app.ActivityThread.main(ActivityThread.java:5883)
java.lang.reflect.Method.invokeNative(Native Method)
java.lang.reflect.Method.invoke(Method.java:515)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:871)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:687)
dalvik.system.NativeStart.main(Native Method)

從最底下開始分析涵紊,NativeStart.main 和 ZygoteInit.main,這個在每個應(yīng)用啟動之前都會執(zhí)行幔摸,因為每個應(yīng)用的進程都是通過 Zygote 進程 fork 出來的摸柄,Zygote進程這里簡單介紹一下:Zygote 服務(wù)進程也叫做孵化進程,在 Linux 的用戶空間既忆,進程 app_process 先會做一些 Zygote 進程啟動的前期工作驱负,如啟動 Runtime 運行時環(huán)境(實例)嗦玖,參數(shù)分解,設(shè)置 startSystemServer 標志跃脊,接著用 runtime.start() 來執(zhí)行 Zygote 服務(wù)的代碼宇挫,其實說簡單點,就是 Zygote 搶了 app_process 這個進程的軀殼酪术,改了名字器瘪,將后面的代碼換成 Zygote 的 main 函數(shù),這樣順利地過度到了 Zygote 服務(wù)進程拼缝。這樣我們在控制臺用 ps 看系統(tǒng)所有進程信息娱局,就不會看到 app_process,取而代之的是 Zygote咧七。而前面 runtime.start()這個函數(shù)實際上是類函數(shù) AndroidRuntime::start()衰齐,在這個函數(shù)中,會新建并啟動一個虛擬機實例來執(zhí)行 com.android.internal.os.ZygoteInit 這個包的 main 函數(shù)继阻。這個 main 函數(shù)中會 fork 一個子進程來啟動 Systemserver耻涛,父進程就作為真正的孵化進程存在了,每當系統(tǒng)要求執(zhí)行一個 Android 應(yīng)用程序瘟檩,Zygote 就會收到 socket 消息 fork 出一個子進程來執(zhí)行該應(yīng)用程序抹缕。因為 Zygote 進程是在系統(tǒng)啟動時產(chǎn)生的,它會完成虛擬機的初始化墨辛,庫的加載卓研,預(yù)置類庫的加載和初始化等操作,而在系統(tǒng)需要一個新的虛擬機實例時可以快速地制造出一個虛擬機出來睹簇。所以這就是應(yīng)用啟動之后會調(diào)用到 ZygoteInit 類的原因奏赘,這個 ZygoteInit.main 接著調(diào)用到了 ZygoteInit$MethodAndArgsCaller.run,這個函數(shù)的調(diào)用過程很有意思太惠,這里需要著重分析一下:<font color="red">ZygoteInit.main -> ZygoteInit.startSystemServer -> ZygoteInit.handleSystemServerProcess -> RuntimeInit.zygoteInit -> RuntimeInit.applicationInit -> RuntimeInit.invokeStaticMain -> 拋出 MethodAndArgsCaller 異常 -> 被 ZygoteInit.main 捕獲 -> MethodAndArgsCaller.run</font>磨淌,為什么要在 RuntimeInit.invokeStaticMain 拋出異常,然后在 ZygoteInit.main 函數(shù)中捕獲它呢凿渊,這個就要涉及到函數(shù)的執(zhí)行模型了梁只,我們知道,程序都是由一個個函數(shù)組成的(除了匯編程序)埃脏,c/c++/java/.. 等高級語言編寫的應(yīng)用程序在執(zhí)行的時候搪锣,他們都擁有自己的棧空間(是一種先進后出的內(nèi)存區(qū)域)剂癌,用于存放函數(shù)的返回地址和函數(shù)的臨時數(shù)據(jù)淤翔,每調(diào)用一個函數(shù)時,就會把函數(shù)的返回地址和相關(guān)數(shù)據(jù)壓入棧中佩谷,當一個函數(shù)執(zhí)行完后旁壮,就會從棧中彈出,cpu 會根據(jù)函數(shù)的返回地址谐檀,執(zhí)行上一個調(diào)用函數(shù)的下一條指令抡谐。 所以,在拋出異常后桐猬,如果異常沒有在當前的函數(shù)中捕獲麦撵,那么當前的函數(shù)執(zhí)行就會異常的退出,從應(yīng)用程序的棧彈出溃肪,并將這個異常傳遞給上一個函數(shù)免胃,直到異常被捕獲處理,否則惫撰,就會引起程序的崩潰羔沙。我們可以回想一下,無論我們寫 C 程序還是 Java 程序厨钻,他們都只有一個入口就是 main 函數(shù)扼雏,當 main 函數(shù)返回退出后就代表整個程序退出了,根據(jù)上面分析的函數(shù)的執(zhí)行模型夯膀,程序的 main 函數(shù)應(yīng)該是每一個應(yīng)用程序最后退出的函數(shù)诗充,應(yīng)該位于棧的底部。同理诱建,Android 應(yīng)用程序的入口是 ActivityThread.main 函數(shù)蝴蜓,所以它也應(yīng)該位于新的進程棧的 ZygoteInit.main 函數(shù)的上面,這樣才能實現(xiàn)直接退出應(yīng)用程序俺猿,但是 Android 每 fork 一個新進程的時候茎匠,它都會先調(diào)用其他的函數(shù)做一些子進程的處理,這樣就造成此時應(yīng)用程序棧的最底部函數(shù)上面不是 ActivityThread.main 函數(shù)辜荠,而是其他函數(shù)汽抚,所以這里通過拋異常的方式啟動 ActivityThread.main 函數(shù)主要是清理應(yīng)用程序棧中 ZygoteInit.main 以上的函數(shù)棧,以實現(xiàn)當 ActivityThread.main 函數(shù)退出時伯病,能直接退出整個應(yīng)用程序造烁。 當 ActivityThread 的 main 退出后,就會退回到 MethodAndArgsCaller.run午笛,而這個函數(shù)直接就退回到 ZygoteInit.main 函數(shù)惭蟋,而 ZygoteInit.main 也無其他的操作,直接退出了函數(shù)药磺,這樣整個應(yīng)用程序?qū)耆顺龈孀椋覀兛纯?google 工程師的注釋也可以看出來:

private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader)
        throws ZygoteInit.MethodAndArgsCaller {
    ......
    /*
     * This throw gets caught in ZygoteInit.main(), which responds
     * by invoking the exception's run() method. This arrangement
     * clears up all the stack frames that were required in setting
     * up the process.
     */
    throw new ZygoteInit.MethodAndArgsCaller(m, argv);
}

是用來清空需要創(chuàng)建一個進程的前期函數(shù)調(diào)用棧的。接著在 ZygoteInit.MethodAndArgsCaller 函數(shù)中通過 method.invoke() 方法調(diào)用到了 ActivityThread.main癌佩,這個函數(shù)熟悉的味道木缝,哈哈哈哈便锨,這就是一個應(yīng)用的 main 函數(shù),打開某個應(yīng)用的時候入口函數(shù)就是這個 main我碟,我們看看這個函數(shù):

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    SamplingProfilerIntegration.start();

    // CloseGuard defaults to true and can be quite spammy.  We
    // disable it here, but selectively enable it later (via
    // StrictMode) on debug builds, but using DropBox, not logs.
    CloseGuard.setEnabled(false);

    Environment.initForCurrentUser();

    // Set the reporter for event logging in libcore
    EventLogger.setReporter(new EventLoggingReporter());

    // Make sure TrustedCertificateStore looks in the right place for CA certificates
    final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
    TrustedCertificateStore.setDefaultUserDirectory(configDir);

    Process.setArgV0("<pre-initialized>");

    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }

    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }

    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();

    throw new RuntimeException("Main thread loop unexpectedly exited");
}

這個函數(shù)里面有一句 Looper.prepareMainLooper()放案,我們知道 Android 系統(tǒng)是事件驅(qū)動的,所以這個 Looper 是用來接收應(yīng)用事件的(這里就不介紹 Looper 矫俺,Handler 以及相關(guān)類了)吱殉,接收到消息之后會調(diào)用 Handler 去處理這些消息,這個 Handler 的名字叫什么呢厘托?就叫 H友雳,哈哈哈,很直白铅匹,我在博客 android 不能在子線程中更新ui的討論和分析中介紹到了這個 H 類押赊,感興趣的可以去了解一下,然后調(diào)用到 H 類的 handleLaunchActivity 方法中(ActivityThread.access$800 這行日志伊群,在使用 Handler 的時候就會打印考杉,應(yīng)該是代表從 Handler 的 Looper 處理消息到了相關(guān)類也就是 ActivityThread 中),這個方法在調(diào)用 startActivity 打開一個頁面時也會調(diào)用舰始,因為第一次打開應(yīng)用的時候也需要打開 HOME 界面崇棠,所以后面的步驟就和 startActivity 一樣了,handleLaunchActivity 函數(shù)會調(diào)用到 handleResumeActivity丸卷,handleResumeActivity 函數(shù)中會創(chuàng)建 Activity 的 PhoneWindow枕稀,并且通過 WMS 添加這個創(chuàng)建的 PhoneWindow,因為步驟和 Dialog.show 的就是一樣的了谜嫉,我這里不重復(fù)分析了萎坷。

疑問以及解決方案

為什么上面這兩個 Exception 會這么詭異呢?一個簡單的 PMS.checkPermission 和啟動應(yīng)用都會崩潰么沐兰?我們來看看 google 官方對于該 TransactionTooLargeException 的介紹:

The Binder transaction failed because it was too large.
During a remote procedure call, the arguments and the return value of the call are transferred as 
Parcel objects stored in the Binder transaction buffer. If the arguments or the return value are too 
large to fit in the transaction buffer, then the call will fail and TransactionTooLargeException 
will be thrown.

The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all 
transactions in progress for the process. Consequently this exception can be thrown when there 
are many transactions in progress even when most of the individual transactions are of moderate size.

There are two possible outcomes when a remote procedure call throws TransactionTooLargeException. 
Either the client was unable to send its request to the service (most likely if the arguments were 
too large to fit in the transaction buffer), or the service was unable to send its response back to 
the client (most likely if the return value was too large to fit in the transaction buffer). It is 
not possible to tell which of these outcomes actually occurred. The client should assume that a 
partial failure occurred.

The key to avoiding TransactionTooLargeException is to keep all transactions relatively small. 
Try to minimize the amount of memory needed to create a Parcel for the arguments and the return 
value of the remote procedure call. Avoid transferring huge arrays of strings or large bitmaps. 
If possible, try to break up big requests into smaller pieces.

If you are implementing a service, it may help to impose size or complexity contraints on the 
queries that clients can perform. For example, if the result set could become large, then don't 
allow the client to request more than a few records at a time. Alternately, instead of returning 
all of the available data all at once, return the essential information first and make the client 
ask for additional information later as needed.

一步步分析一下上面的介紹哆档,一個跨進程調(diào)用,調(diào)用的參數(shù)和返回值是要轉(zhuǎn)換成 Parcel 對象進行傳遞的住闯,而這些 Parcel 對象是存儲在 Binder transaction buffer 里面的瓜浸,如果參數(shù)或者返回值過大,導(dǎo)致這個 buffer 放不下的話比原,程序就會失敗并且拋出 TransactionTooLargeException 異常插佛;這個 Binder transaction buffer 有一個固定的大小 1Mb,而這個空間是提供給一個進程的所有 transaction 使用的量窘,因此甚至當絕大多數(shù)單獨的 transaction 調(diào)用的參數(shù)大小并不大但是數(shù)量很多的時候雇寇,也會拋出這個 Exception;當遠程調(diào)用拋出 TransactionTooLargeException 異常的時候,通常會有兩個可能的結(jié)果锨侯,一個是 Binder Client 無法將請求發(fā)送給 Service(一般是由于傳遞的參數(shù)過大嫩海,Binder transaction buffer 放不下導(dǎo)致的),另一個是 Service 無法將返回值傳遞回 Binder Client(一般是由于返回值過大導(dǎo)致)识腿,一般很難去決斷到底會產(chǎn)生這兩個結(jié)果中的哪一個出革,所以客戶端應(yīng)該去假定它們中的一個會失斣熳场渡讼;避免 TransactionTooLargeException 的關(guān)鍵是讓所有的 transaction 盡可能的小,盡量去縮小遠程調(diào)用 Service 的參數(shù)大小和返回值耳璧,禁止傳遞大數(shù)組成箫,String 字串或者一個大的 Bitmap 對象,如果可以的話旨枯,盡量把大的請求分解成一個個小的調(diào)用蹬昌;如果你在實現(xiàn)一個 Service 服務(wù)者,了解這些會幫助你強制性的規(guī)定 Binder Client 的遠程調(diào)用的大小和制定一些復(fù)雜的約束攀隔,舉個例子皂贩,如果結(jié)果集合可能會變的很大,那么就不允許 Binder Client 在一個時間點內(nèi)請求超過一定數(shù)量昆汹,又或者可以選擇性地當返回值很大的時候明刷,不需要一次性返回所有數(shù)據(jù),可以第一次先返回關(guān)鍵的數(shù)據(jù)满粗,然后如果需要的話讓 Binder Client 再次去請求額外的信息辈末。
  看到這里我們明白了,一個應(yīng)用進程的所有 AIDL 調(diào)用都是共用一個 Binder transaction buffer映皆,而這個 buffer 的大小僅僅只是 1Mb挤聘,當所有的遠程調(diào)用的參數(shù)或者這些調(diào)用返回值的大小加起來超過 1Mb 的話就會拋出 TransactionTooLargeException 異常,所以這也就是我們上面的 WMS捅彻,PMS 都會拋出這個錯誤的原因组去。知道原因,我們就知道初步的處理方法了步淹,就是查看每一個拋出這個異常的地方从隆,修改調(diào)用參數(shù)的大小,或者去查看 AIDL 的 Binder Server 端贤旷,看看是否是返回值的大小超過了一定的限制广料。
  亦或者看看這個答案的描述也可以:What to do on TransactionTooLargeException,它給出了幾種常見的可能會造成這個 exception 的使用方式:

When you get this exception in your application, please analyze your code.

1. Are you exchanging lot of data between your services and application?
2. Using intents to share huge data, (for example, the user selects huge number of files 
from gallery share press share, the URIs of the selected files will be transferred using intents)
3. receiving bitmap files from service
4. waiting for android to respond back with huge data (for example, getInstalledApplications() 
when the user installed lot of applications)
5. using applyBatch() with lot of operations pending

討論與思考

經(jīng)過上面的三種同類型 Crash 的分析幼驶,我們知道了一個應(yīng)用進程會對應(yīng)一個 Binder transaction buffer(如果應(yīng)用有多個進程艾杏,那就是對應(yīng)多個 buffer),如果一個應(yīng)用進程的所有 AIDL 調(diào)用盅藻,這里包括系統(tǒng) Service 和應(yīng)用內(nèi)部跨進程通信的 Client 和 Server 的調(diào)用购桑,在一個時間點內(nèi)這些調(diào)用的參數(shù)和返回值大小如果加起來超過 1Mb畅铭,就會引起 TransactionTooLargeException 錯誤,那么問題來了2K敦!我們在分析第一個 Dialog.show() 引發(fā)的崩潰日志的時候缭贡,跟蹤到 native 層的時候炉擅,明明看到這一段代碼:

// TransactionTooLargeException is a checked exception, only throw from certain methods.
// FIXME: Transaction too large is the most common reason for FAILED_TRANSACTION
//        but it is not the only one.  The Binder driver can return BR_FAILED_REPLY
//        for other reasons also, such as if the transaction is malformed or
//        refers to an FD that has been closed.  We should change the driver
//        to enable us to distinguish these cases in the future.
if (canThrowRemoteException && parcelSize > 200*1024) {
    // bona fide large payload
    exceptionToThrow = "android/os/TransactionTooLargeException";
    snprintf(msg, sizeof(msg)-1, "data parcel size %d bytes", parcelSize);
} 

而這里是檢測了調(diào)用的參數(shù)如果大于 200K 就會報出錯誤,而且這里的大小僅僅只是調(diào)用的參數(shù)大小阳惹,我全局搜索了 Android 的源碼谍失,發(fā)現(xiàn)拋出異常的地方只有這一處:Androidxref search TransactionTooLargeException,所以這就和 google 的官方文檔有出入了莹汤,而且實際的情況更傾向于 google 文檔的介紹快鱼,但是代碼確實擺在這,拋出異常的地方只有這一處纲岭,還是說我的代碼分析出現(xiàn)了問題抹竹,但是 200K 確實是硬編碼寫死的,而且我看了一下我司的代碼止潮,Dialog.show() 函數(shù)確實沒有傳遞大的數(shù)據(jù)窃判,PMS.checkPermission() 函數(shù)也沒有傳遞大的參數(shù),所以不會有參數(shù)超過 200K 的情況出現(xiàn)沽翔,那么實際可能是由于 buffer 已經(jīng)快要滿了兢孝,導(dǎo)致一次小參數(shù)的調(diào)用也會導(dǎo)致拋出這個異常,也就是實際更傾向于 1Mb 的解釋仅偎,可是這就和源碼對應(yīng)不上了跨蟹,這就是我糾結(jié)的地方了,因為這個確實是讓我很困惑橘沥,希望有大神可以知會我一下窗轩,非常感謝~~

最終監(jiān)控方案&&源碼

這個問題的最終處理的方法就是去檢查參數(shù)和返回值的大小,還有不能在短時間內(nèi)有大量的系統(tǒng) Service 調(diào)用座咆,如果是前者比較好處理痢艺,但是如果是后者,就相對比較麻煩介陶,需要去仔細查看工程源碼堤舒,查找每一處可能引發(fā)的地方,能不能有一種方式可以獲取應(yīng)用每次調(diào)用 Service 的參數(shù)大小和調(diào)用的頻率呢哺呜?可以的舌缤,怎么去做呢,這就要用到上一篇博客:Android 動態(tài)代理以及利用動態(tài)代理實現(xiàn) ServiceHook 內(nèi)容了,我們將上篇博客的源碼稍微改造一下就OK了9臁陵吸!怎么獲取調(diào)用系統(tǒng) Service 的參數(shù)大小呢?上面分析源碼的時候我們知道介牙,在 BinderProxy 對象調(diào)用 transact 方法的時候壮虫,第二個參數(shù) Parcel 對象對應(yīng)的就是我們參數(shù),所以我們只需要獲取到這個參數(shù)的大小并通過日志打印出來环础,這樣就能夠?qū)崟r監(jiān)控參數(shù)的大小囚似。怎么獲取調(diào)用的頻率呢?能夠打印大小了喳整,那么只需要查看每次打印大小的日志間隔時間就可以了谆构,如果在短時間內(nèi)有大量的 AIDL 調(diào)用就可以定位問題源碼的所在了。
  比如我們現(xiàn)在就需要監(jiān)控 ClipboardService 每次調(diào)用的參數(shù)大小和頻率框都,怎么做?很簡單呵晨,我們知道 ClipboardService 返回給應(yīng)用進程的 IBinder 對象會轉(zhuǎn)成一個 Proxy 對象魏保,而這個 Proxy 對象會持有上面 IBinder 對象的引用,這個引用名字叫 mRemote摸屠,Proxy 的每次調(diào)用其實就是簡單的 new 兩個 Parcel 對象谓罗,一個是參數(shù),一個是返回值季二,然后調(diào)用 mRemote 對象的 transact 方法將信息寫入到 Binder Driver 中:

@Override
public void setPrimaryClip(android.content.ClipData clip, java.lang.String callingPackage) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        if ((clip != null)) {
            _data.writeInt(1);
            clip.writeToParcel(_data, 0);
        } else {
            _data.writeInt(0);
        }
        _data.writeString(callingPackage);
        mRemote.transact(Stub.TRANSACTION_setPrimaryClip, _data, _reply, 0);
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}

而我們傳遞給 ClipboardService 的參數(shù)就寫進了 _data 那個 Parcel 對象中檩咱,BinderProxy 對象調(diào)用 transact 函數(shù)的時候,這個參數(shù)被放在了第二位胯舷,我們只需要打印第二個參數(shù)的大小不就可以了么刻蚯,我們現(xiàn)在已經(jīng)獲取到了 ClipboardService 在應(yīng)用進程的 Proxy 對象,所以接下來只需要通過反射 mRemote 變量桑嘶,設(shè)置為我們動態(tài)生成類的一個對象炊汹,讓調(diào)用 transact 函數(shù)的時候調(diào)用到我們 InvocationHandler 對象的 invoke 方法中,然后把參數(shù)取出來逃顶,打印第二個參數(shù)的大小即可:

public HookHandler(IBinder base, Class<?> stubClass,
                   InvocationHandler InvocationHandler) {
    mInvocationHandler = InvocationHandler;

    try {
        Method asInterface = stubClass.getDeclaredMethod("asInterface", IBinder.class);
        this.mBase = asInterface.invoke(null, base);

        Class clazz = mBase.getClass();
        Field mRemote = clazz.getDeclaredField("mRemote");
        mRemote.setAccessible(true);
        //新建一個 BinderProxy 的代理對象
        Object binderProxy = Proxy.newProxyInstance(mBase.getClass().getClassLoader(),
                new Class[] {IBinder.class}, new ClipboardHook.TransactionWatcherHook((IBinder) mRemote.get(mBase)));
        mRemote.set(mBase, binderProxy);

    } catch (Exception e) {
        e.printStackTrace();
    }
}
.......
//用來監(jiān)控 TransactionTooLargeException 錯誤
public static class TransactionWatcherHook implements InvocationHandler {

    IBinder binder;
    public TransactionWatcherHook(IBinder binderProxy) {
        binder = binderProxy;
    }

    @Override
    public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
        if (objects.length >= 2 && objects[1] instanceof Parcel) {
            //第二個參數(shù)對應(yīng)為 Parcel 對象
            Log.e(TAG, "clipboard service invoked, transact's parameter size is " + ((Parcel)objects[1]).dataSize() + " byte");
        }
        return method.invoke(binder, objects);
    }
}

這里只貼出來了關(guān)鍵代碼讨便,其他代碼可以去看看Android 動態(tài)代理以及利用動態(tài)代理實現(xiàn) ServiceHook,這樣就成功獲取到了參數(shù)的大小以政,單位為 B 霸褒,我們來看看實際效果:

03-06 17:19:37.031 459-459/com.example.servicehook E/ClipboardHook: clipboardhookhandler invoke
03-06 17:19:37.032 459-459/com.example.servicehook E/ClipboardHook: clipboard service invoked, transact's parameter size is 312 B

增加一個字符之后:

03-06 17:19:40.056 459-459/com.example.servicehook E/ClipboardHook: clipboardhookhandler invoke
03-06 17:19:40.057 459-459/com.example.servicehook E/ClipboardHook: clipboard service invoked, transact's parameter size is 316 B

增加了 4B,也就是一個字盈蛮,所以這個大小的單位為 B废菱,這里簡單計算一下 1Mb 可以復(fù)制多少字符 1024*1024/32 = 32768,感興趣的可以復(fù)制一下這么多字符,看看是不是會崩潰昙啄,哈哈哈哈穆役。
  當然這只是監(jiān)控 ClipboardService 的每次 AIDL 調(diào)用,PMS梳凛,WMS 的監(jiān)控和這里類似耿币,步驟是一樣的,這里就不一一介紹了韧拒。
  轉(zhuǎn)載請注明出處:http://blog.csdn.net/self_study/article/details/60136277
  源碼:https://github.com/zhaozepeng/ServiceHook

引用

https://developer.android.com/reference/android/os/TransactionTooLargeException.html
http://bugly.qq.com/bbs/forum.php?mod=viewthread&tid=865
http://stackoverflow.com/questions/11451393/what-to-do-on-transactiontoolargeexception
http://blog.csdn.net/a123ok/article/details/47961101

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末淹接,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子叛溢,更是在濱河造成了極大的恐慌塑悼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件楷掉,死亡現(xiàn)場離奇詭異厢蒜,居然都是意外死亡,警方通過查閱死者的電腦和手機烹植,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門斑鸦,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人草雕,你說我怎么就攤上這事巷屿。” “怎么了墩虹?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵嘱巾,是天一觀的道長。 經(jīng)常有香客問我诫钓,道長旬昭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任尖坤,我火速辦了婚禮稳懒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘慢味。我一直安慰自己场梆,他們只是感情好,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布纯路。 她就那樣靜靜地躺著或油,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驰唬。 梳的紋絲不亂的頭發(fā)上顶岸,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天腔彰,我揣著相機與錄音,去河邊找鬼辖佣。 笑死霹抛,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的卷谈。 我是一名探鬼主播杯拐,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼世蔗!你這毒婦竟也來了端逼?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤污淋,失蹤者是張志新(化名)和其女友劉穎顶滩,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寸爆,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡剧罩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年古话,在試婚紗的時候發(fā)現(xiàn)自己被綠了狭莱。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片佛析。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡秧倾,死狀恐怖毒姨,靈堂內(nèi)的尸體忽然破棺而出衣陶,到底是詐尸還是另有隱情财饥,我是刑警寧澤墩衙,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布务嫡,位于F島的核電站,受9級特大地震影響漆改,放射性物質(zhì)發(fā)生泄漏心铃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一挫剑、第九天 我趴在偏房一處隱蔽的房頂上張望去扣。 院中可真熱鬧,春花似錦樊破、人聲如沸愉棱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽奔滑。三九已至,卻和暖如春顺少,著一層夾襖步出監(jiān)牢的瞬間朋其,已是汗流浹背王浴。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梅猿,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓钞啸,卻偏偏與公主長得像癞松,于是被迫代替她去往敵國和親爽撒。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容