Android TransactionTooLargeException 解析,思考与监控方案

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zhao_zepeng/article/details/60136277

  最近公司遇到了一个很有意思的 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() 就会导致崩溃,具体原因我在下面会详细介绍到,我们先会通过分析三段 Crash 日志调用信息来定位原因,然后提出解决办法。
  转载请注明出处:http://blog.csdn.net/self_study/article/details/60136277
  对技术感兴趣的同鞋加群 544645972 一起交流。

TransactionTooLargeException 分析与解决

  我们来仔细分析一下这个 Exception 的错误堆栈信息,由于这里面涉及到了 AIDL 以及 WMS,AMS的相关知识,这里列出对应相关的博客,下面的分析会直接使用到这些内容:
android 不能在子线程中更新ui的讨论和分析:Activity 打开的过程分析;
java/android 设计模式学习笔记(9)—代理模式:AMS 的相关类图和介绍;
android WindowManager解析与骗取QQ密码案例分析:界面 window 的创建过程;
java/android 设计模式学习笔记(8)—桥接模式:WMS 的相关类图和介绍;
android IPC通信(下)-AIDL:AIDL 以及 Binder 的相关介绍;
Android 动态代理以及利用动态代理实现 ServiceHook:ServiceHook 的相关介绍;
Android TransactionTooLargeException 解析,思考与监控方案:TransactionTooLargeException 的解析以及监控方案。

TransactionTooLargeException StackTrace 分析

  我们这里先分析一下上面那段 Exception 的调用栈,这里直接摘取了其中的方法调用部分:

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() 函数,这个是我们应用层用来显示一个 Dialog 的方法,很正常,对吧,然后下一句:

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

我在博客:android WindowManager解析与骗取QQ密码案例分析中介绍到,Dialog Window 的创建和 Activity 类似,也是需要调用 PolicyManager.makeNewWindow 去创建一个 Window,然后通过 WindowManager 将该 Window 的 DecorView 添加到 Activity 的 Window 中就能显示出来了,与 Activity 最大的区别就是 Dialog 的 Window 需要一个 Activity 的句柄,因为需要依附在 Activity 上面,而 Toast 这种系统 Window 则可以直接显示,这三种 Window 有着不同的层级范围,层级大的 Window 会覆盖在层级小的 Window 之上,应用window的层级范围是 1~99,子 Window 的范围是 1000~1999,系统 Window 的范围是 2000~2999。所以说 Dialog.show() 会调用相关的函数去创建 Window ,而 Dialog 创建 Window 的过程我们可以参考 Activity 创建 Window 的过程,第一步会调用到 WindowManagerImpl 类中的 addView 函数去添加上面 new 出来的那个 Window 对象,而 WindowManager 和 Window 类是一个典型的桥接模式,具体的可以看看我的博客:java/android 设计模式学习笔记(8)—桥接模式,下面为 uml 类图:
这里写图片描述
WindowManagerImpl 类持有一个 WindowManagerGlobal 类的引用,所有的操作都交给了 WindowManagerGlobal 类, WindowManagerGlobal 里面会调用到 ViewRootImpl 类的 setView 方法,而这个函数里面会调用 IWindowSession 类,这个 IWindowSession 类的对象 sWindowSession 是通过 IWindowManager 的 openSession 函数获取的,而 IWindowManager 其实就是 WindowManagerService 在应用进程的 Proxy 类对象,它持有了 WMS 的 IBinder 对象,通过 AIDL 调用到主进程的 WMS 中,WMS 的 openSession 方法返回的是一个 IWindowSession.Stub 类的对象,但是由于跨进程了,所以系统进程返回的 IWindowSession.Stub 对象在应用进程中就对应为 IWindowSession 的 IBinder 对象,最后同理需要调用 IWindowSession.Stub.asInterface 函数转成 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;
}

这样就转换为了 IWindowSession$Stub$Proxy 对象,为什么调用到的是 BinderProxy 类的 transact 方法呢?android IPC通信(下)-AIDL博客中我已经介绍到了,应用进程通过 ServiceManager 获取到的 WMS 的 IBinder 对象其实就是 BinderProxy 对象,这里的 IWindowSession 也是类似的,所以调用到了 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;

这个方法就调用到了 native 方法中,全局搜索一下,这个方法对应于 native 的 android_os_BinderProxy_transact 方法,这个方法是关键:

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

其中调用到了 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;
        .......
    }
}

于是我们就看到了关键的一句话:

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

没错,这就是错误的来源,这里判断如果这个 parcelSize 大于 200K 就会报错,而这个 parcelSize 的大小,对应一下,发现就是 BinderProxy 的第二个参数,也就是说如果 Percel 对象的大小超过 200K 就会报出这个错误,而这个参数的大小就是应用进程传递给主进程的参数大小,而应用进程传递给主进程的参数对应的就是 Dialog 的相关参数,比如 Message 或者 Title 等等,如果这些参数过大的话,就会出现这个崩溃,解决办法就是就是将 Dialog 的相关参数变小,可是这真的是解决办法嘛,不一定,咱们继续看。

同类 Crash

  上面的 Dialog.show() 引发的 Crash 只是冰山一角,因为我们知道调用 WMS 服务的时候,transact 函数的参数如果过大就会崩溃,那么 AMS,PMS呢?答案是肯定的,我们来看看我司的相关同类 Crash:

PMS 检查权限

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 引起的,而这个会最终会调用到 ApplicationPackageManager 类的 checkPermission 函数里面,这个函数:

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

这个函数调用到了 mPM 变量的 checkPermission 方法中,这个变量是 IPackageManager 类型,因为我司的插件化框架的缘故,所以这个变量是被修改过的,具体的可以看看我的博客:Android 动态代理以及利用动态代理实现 ServiceHook,这个变量最终被修改为一个动态生成类的对象,博客里面我介绍到这个类的名字格式为 $ProxyXXX,后面的 XXX 为具体的数字,所以紧接着就调用到了这个动态生成类的 checkPermission 函数里面,然后调用到 InvocationHandler 类的 invoke 方法里面,对应的就是 PackageManagerHook 类的内部类 HookHandler 的 invoke 方法,最终会调用到 IPackageManager 的 Proxy 对象中,对应的就是 IPackageManager$Stub$Proxy 这个角色,这个角色会调用 IBinder 对象的,也就是 BinderProxy 的 transact 方法,最终的调用过程也就是和上面 WMS 的类似了。

AMS -> WMS 启动应用或者打开页面

  还有另外的比如:

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,这个在每个应用启动之前都会执行,因为每个应用的进程都是通过 Zygote 进程 fork 出来的,Zygote进程这里简单介绍一下:Zygote 服务进程也叫做孵化进程,在 Linux 的用户空间,进程 app_process 先会做一些 Zygote 进程启动的前期工作,如启动 Runtime 运行时环境(实例),参数分解,设置 startSystemServer 标志,接着用 runtime.start() 来执行 Zygote 服务的代码,其实说简单点,就是 Zygote 抢了 app_process 这个进程的躯壳,改了名字,将后面的代码换成 Zygote 的 main 函数,这样顺利地过度到了 Zygote 服务进程。这样我们在控制台用 ps 看系统所有进程信息,就不会看到 app_process,取而代之的是 Zygote。而前面 runtime.start()这个函数实际上是类函数 AndroidRuntime::start(),在这个函数中,会新建并启动一个虚拟机实例来执行 com.android.internal.os.ZygoteInit 这个包的 main 函数。这个 main 函数中会 fork 一个子进程来启动 Systemserver,父进程就作为真正的孵化进程存在了,每当系统要求执行一个 Android 应用程序,Zygote 就会收到 socket 消息 fork 出一个子进程来执行该应用程序。因为 Zygote 进程是在系统启动时产生的,它会完成虚拟机的初始化,库的加载,预置类库的加载和初始化等操作,而在系统需要一个新的虚拟机实例时可以快速地制造出一个虚拟机出来。所以这就是应用启动之后会调用到 ZygoteInit 类的原因,这个 ZygoteInit.main 接着调用到了 ZygoteInit$MethodAndArgsCaller.run,这个函数的调用过程很有意思,这里需要着重分析一下:ZygoteInit.main -> ZygoteInit.startSystemServer -> ZygoteInit.handleSystemServerProcess -> RuntimeInit.zygoteInit -> RuntimeInit.applicationInit -> RuntimeInit.invokeStaticMain -> 抛出 MethodAndArgsCaller 异常 -> 被 ZygoteInit.main 捕获 -> MethodAndArgsCaller.run,为什么要在 RuntimeInit.invokeStaticMain 抛出异常,然后在 ZygoteInit.main 函数中捕获它呢,这个就要涉及到函数的执行模型了,我们知道,程序都是由一个个函数组成的(除了汇编程序),c/c++/java/.. 等高级语言编写的应用程序在执行的时候,他们都拥有自己的栈空间(是一种先进后出的内存区域),用于存放函数的返回地址和函数的临时数据,每调用一个函数时,就会把函数的返回地址和相关数据压入栈中,当一个函数执行完后,就会从栈中弹出,cpu 会根据函数的返回地址,执行上一个调用函数的下一条指令。 所以,在抛出异常后,如果异常没有在当前的函数中捕获,那么当前的函数执行就会异常的退出,从应用程序的栈弹出,并将这个异常传递给上一个函数,直到异常被捕获处理,否则,就会引起程序的崩溃。我们可以回想一下,无论我们写 C 程序还是 Java 程序,他们都只有一个入口就是 main 函数,当 main 函数返回退出后就代表整个程序退出了,根据上面分析的函数的执行模型,程序的 main 函数应该是每一个应用程序最后退出的函数,应该位于栈的底部。同理,Android 应用程序的入口是 ActivityThread.main 函数,所以它也应该位于新的进程栈的 ZygoteInit.main 函数的上面,这样才能实现直接退出应用程序,但是 Android 每 fork 一个新进程的时候,它都会先调用其他的函数做一些子进程的处理,这样就造成此时应用程序栈的最底部函数上面不是 ActivityThread.main 函数,而是其他函数,所以这里通过抛异常的方式启动 ActivityThread.main 函数主要是清理应用程序栈中 ZygoteInit.main 以上的函数栈,以实现当 ActivityThread.main 函数退出时,能直接退出整个应用程序。 当 ActivityThread 的 main 退出后,就会退回到 MethodAndArgsCaller.run,而这个函数直接就退回到 ZygoteInit.main 函数,而 ZygoteInit.main 也无其他的操作,直接退出了函数,这样整个应用程序将会完全退出,我们看看 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);
}

是用来清空需要创建一个进程的前期函数调用栈的。接着在 ZygoteInit.MethodAndArgsCaller 函数中通过 method.invoke() 方法调用到了 ActivityThread.main,这个函数熟悉的味道,哈哈哈哈,这就是一个应用的 main 函数,打开某个应用的时候入口函数就是这个 main,我们看看这个函数:

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

这个函数里面有一句 Looper.prepareMainLooper(),我们知道 Android 系统是事件驱动的,所以这个 Looper 是用来接收应用事件的(这里就不介绍 Looper ,Handler 以及相关类了),接收到消息之后会调用 Handler 去处理这些消息,这个 Handler 的名字叫什么呢?就叫 H,哈哈哈,很直白,我在博客 android 不能在子线程中更新ui的讨论和分析中介绍到了这个 H 类,感兴趣的可以去了解一下,然后调用到 H 类的 handleLaunchActivity 方法中(ActivityThread.access$800 这行日志,在使用 Handler 的时候就会打印,应该是代表从 Handler 的 Looper 处理消息到了相关类也就是 ActivityThread 中),这个方法在调用 startActivity 打开一个页面时也会调用,因为第一次打开应用的时候也需要打开 HOME 界面,所以后面的步骤就和 startActivity 一样了,handleLaunchActivity 函数会调用到 handleResumeActivity,handleResumeActivity 函数中会创建 Activity 的 PhoneWindow,并且通过 WMS 添加这个创建的 PhoneWindow,因为步骤和 Dialog.show 的就是一样的了,我这里不重复分析了。

疑问以及解决方案

  为什么上面这两个 Exception 会这么诡异呢?一个简单的 PMS.checkPermission 和启动应用都会崩溃么?我们来看看 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.

一步步分析一下上面的介绍,一个跨进程调用,调用的参数和返回值是要转换成 Parcel 对象进行传递的,而这些 Parcel 对象是存储在 Binder transaction buffer 里面的,如果参数或者返回值过大,导致这个 buffer 放不下的话,程序就会失败并且抛出 TransactionTooLargeException 异常;这个 Binder transaction buffer 有一个固定的大小 1Mb,而这个空间是提供给一个进程的所有 transaction 使用的,因此甚至当绝大多数单独的 transaction 调用的参数大小并不大但是数量很多的时候,也会抛出这个 Exception;当远程调用抛出 TransactionTooLargeException 异常的时候,通常会有两个可能的结果,一个是 Binder Client 无法将请求发送给 Service(一般是由于传递的参数过大,Binder transaction buffer 放不下导致的),另一个是 Service 无法将返回值传递回 Binder Client(一般是由于返回值过大导致),一般很难去决断到底会产生这两个结果中的哪一个,所以客户端应该去假定它们中的一个会失败;避免 TransactionTooLargeException 的关键是让所有的 transaction 尽可能的小,尽量去缩小远程调用 Service 的参数大小和返回值,禁止传递大数组,String 字串或者一个大的 Bitmap 对象,如果可以的话,尽量把大的请求分解成一个个小的调用;如果你在实现一个 Service 服务者,了解这些会帮助你强制性的规定 Binder Client 的远程调用的大小和制定一些复杂的约束,举个例子,如果结果集合可能会变的很大,那么就不允许 Binder Client 在一个时间点内请求超过一定数量,又或者可以选择性地当返回值很大的时候,不需要一次性返回所有数据,可以第一次先返回关键的数据,然后如果需要的话让 Binder Client 再次去请求额外的信息。
  看到这里我们明白了,一个应用进程的所有 AIDL 调用都是共用一个 Binder transaction buffer,而这个 buffer 的大小仅仅只是 1Mb,当所有的远程调用的参数或者这些调用返回值的大小加起来超过 1Mb 的话就会抛出 TransactionTooLargeException 异常,所以这也就是我们上面的 WMS,PMS 都会抛出这个错误的原因。知道原因,我们就知道初步的处理方法了,就是查看每一个抛出这个异常的地方,修改调用参数的大小,或者去查看 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

讨论与思考

  经过上面的三种同类型 Crash 的分析,我们知道了一个应用进程会对应一个 Binder transaction buffer(如果应用有多个进程,那就是对应多个 buffer),如果一个应用进程的所有 AIDL 调用,这里包括系统 Service 和应用内部跨进程通信的 Client 和 Server 的调用,在一个时间点内这些调用的参数和返回值大小如果加起来超过 1Mb,就会引起 TransactionTooLargeException 错误,那么问题来了!!!我们在分析第一个 Dialog.show() 引发的崩溃日志的时候,跟踪到 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);
} 

而这里是检测了调用的参数如果大于 200K 就会报出错误,而且这里的大小仅仅只是调用的参数大小,我全局搜索了 Android 的源码,发现抛出异常的地方只有这一处:Androidxref search TransactionTooLargeException,所以这就和 google 的官方文档有出入了,而且实际的情况更倾向于 google 文档的介绍,但是代码确实摆在这,抛出异常的地方只有这一处,还是说我的代码分析出现了问题,但是 200K 确实是硬编码写死的,而且我看了一下我司的代码,Dialog.show() 函数确实没有传递大的数据,PMS.checkPermission() 函数也没有传递大的参数,所以不会有参数超过 200K 的情况出现,那么实际可能是由于 buffer 已经快要满了,导致一次小参数的调用也会导致抛出这个异常,也就是实际更倾向于 1Mb 的解释,可是这就和源码对应不上了,这就是我纠结的地方了,因为这个确实是让我很困惑,希望有大神可以知会我一下,非常感谢~~

最终监控方案&&源码

  这个问题的最终处理的方法就是去检查参数和返回值的大小,还有不能在短时间内有大量的系统 Service 调用,如果是前者比较好处理,但是如果是后者,就相对比较麻烦,需要去仔细查看工程源码,查找每一处可能引发的地方,能不能有一种方式可以获取应用每次调用 Service 的参数大小和调用的频率呢?可以的,怎么去做呢,这就要用到上一篇博客:Android 动态代理以及利用动态代理实现 ServiceHook 内容了,我们将上篇博客的源码稍微改造一下就OK了!!怎么获取调用系统 Service 的参数大小呢?上面分析源码的时候我们知道,在 BinderProxy 对象调用 transact 方法的时候,第二个参数 Parcel 对象对应的就是我们参数,所以我们只需要获取到这个参数的大小并通过日志打印出来,这样就能够实时监控参数的大小。怎么获取调用的频率呢?能够打印大小了,那么只需要查看每次打印大小的日志间隔时间就可以了,如果在短时间内有大量的 AIDL 调用就可以定位问题源码的所在了。
  比如我们现在就需要监控 ClipboardService 每次调用的参数大小和频率,怎么做?很简单,我们知道 ClipboardService 返回给应用进程的 IBinder 对象会转成一个 Proxy 对象,而这个 Proxy 对象会持有上面 IBinder 对象的引用,这个引用名字叫 mRemote,Proxy 的每次调用其实就是简单的 new 两个 Parcel 对象,一个是参数,一个是返回值,然后调用 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 的参数就写进了 _data 那个 Parcel 对象中,BinderProxy 对象调用 transact 函数的时候,这个参数被放在了第二位,我们只需要打印第二个参数的大小不就可以了么,我们现在已经获取到了 ClipboardService 在应用进程的 Proxy 对象,所以接下来只需要通过反射 mRemote 变量,设置为我们动态生成类的一个对象,让调用 transact 函数的时候调用到我们 InvocationHandler 对象的 invoke 方法中,然后把参数取出来,打印第二个参数的大小即可:

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();
    }
}
.......
//用来监控 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) {
            //第二个参数对应为 Parcel 对象
            Log.e(TAG, "clipboard service invoked, transact's parameter size is " + ((Parcel)objects[1]).dataSize() + " byte");
        }
        return method.invoke(binder, objects);
    }
}

这里只贴出来了关键代码,其他代码可以去看看Android 动态代理以及利用动态代理实现 ServiceHook,这样就成功获取到了参数的大小,单位为 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 可以复制多少字符 1024*1024/32 = 32768,感兴趣的可以复制一下这么多字符,看看是不是会崩溃,哈哈哈哈。
  当然这只是监控 ClipboardService 的每次 AIDL 调用,PMS,WMS 的监控和这里类似,步骤是一样的,这里就不一一介绍了。
  转载请注明出处: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

展开阅读全文

没有更多推荐了,返回首页