欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Android7.0之Binder的数据传输新限制 TransactionTooLargeException: data parcel size xxx bytes原因与解决方案

程序员文章站 2022-03-02 13:47:36
...

原文链接:https://www.kaelli.com/20.html

最近在Bugly上看到一个上报的问题似乎比较频繁,就把该问题的原因分析与解决方案记录一下。

首先,把Bugly的错误日志展示一下:

java.lang.RuntimeException:android.os.TransactionTooLargeException: data parcel size 587588 bytes

android.app.ActivityThread$StopInfo.run(ActivityThread.java:3939)

......

Caused by:

android.os.TransactionTooLargeException:data parcel size 587588 bytes

android.os.BinderProxy.transactNative(Native Method)

android.os.BinderProxy.transact(Binder.java:619)

android.app.ActivityManagerProxy.activityStopped(ActivityManagerNative.java:3748)

android.app.ActivityThread$StopInfo.run(ActivityThread.java:3931)

android.os.Handler.handleCallback(Handler.java:751)

android.os.Handler.dispatchMessage(Handler.java:95)

android.os.Looper.loop(Looper.java:154)

android.app.ActivityThread.main(ActivityThread.java:6285)

java.lang.reflect.Method.invoke(Native Method)

com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:939)

com.android.internal.os.ZygoteInit.main(ZygoteInit.java:829)

问题表面上看起来很清晰,一个TransactionTooLargeException的异常,原因是数据包太大了。不过这里也有让人疑惑的地方,首先587588字节的数据量不算小,但也不算太大,通过Intent传递数据,具体的大小限制目前来看不同版本的系统可能不同,比较普遍的一个看法是不能超过1MB,因为官方文档里有这样的说明:

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.

但是也有人通过实验,认为不能超过512K。好吧,如果从512K这个数值来看,似乎确实是数据过大了。但实际上收集的错误日志里,还有二三百K的,这就让人很困惑了。

其次,有一个让我非常吃惊的事实是,这次报TransactionTooLargeException异常的手机,集中在Android N的版本里,也就是版本号只有24和25的,低于24或者高于25的一概没有。我们的App最低兼容到了16,但是即便在很古老的4.0,4.1,4.4的手机上也没有报这个错误。显然在Android N中,一定有一些特别的地方。因为TransactionTooLargeException是Binder传递数据的一个异常,所以有必要去了解一下相应的源码。

在/frameworks/base/core/jni/android_util_Binder.cpp中的signalExceptionForError函数里,对于FAILED_TRANSACTION这种错误的分支里,有如下代码:

 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;

显然就是这里丢出了异常,不过还是有让人困惑的地方:传递的数据超过200K就要报错?而且很关键的一点,这段代码是6.0的源码,而不是7.0的!(当然了,7.0以后的源码这里基本上还是相同的逻辑。)但是在6.0的手机上并没有报出这个错误。看来有必要去看一下7.0发生了什么变动

Many platform APIs have now started checking for large payloads being sent across Bindertransactions, and the system now rethrows TransactionTooLargeExceptions as RuntimeExceptions, instead of silently logging or suppressing them. One common example is storing too much data in onSaveInstanceState(), which causes ActivityThread.StopInfo to throw a RuntimeException when your app targets Android 7.0.

可以看到,7.0的改动日志里确实提到了,Binder的数据传输确实有了新的限制,当数据量比较大的时候就会抛出 TransactionTooLargeExceptions ,不过这里并没有具体的说明到底多大的数据量会造成这个问题。这应该就是原因所在了。限制Binder的数据量,自然是为了性能考虑,虽然可以理解但搞不懂为什么不直接说明具体的限制,当然也许不同配置的手机限制不同。

知道了问题的原因,接下来就是解决方案了。

  • 方法一:最简单、最直接的办法,当然是让数据量变小了:

我们在传递大量数据时,一般都传递的是对象,一般来说都是实现了Serilizable或Parcelable接口的。如果是简单的类对象,类似于下面这种:

public class Person implements Serilizable { 
    public String name; 
    public int age; 
}

即并没有内嵌其他自定义的类,那么还好说,不需做太多处理,如果依然报错则说明数据确实太多,可以考虑舍弃一些无用字段,或者直接看方法二和三。如果是嵌套了一层甚至多层的,如:

public class Person implements Serilizable {
        public String name;
        public int age;
        public Parents parents;
        
        public class Parents implements Serilizable {
            public Person father;
            public Person mother;
        }
    }

这时候,把它转化为Json字符串会显著减小体积(类的结构越复杂,体积减小的越多)。我常用的方法是new Gson().toJson(对象),然后把字符串放到Intent中,在接收的地方再转成对象即可。

  • 方法二:方法一转成字符串缩小后如果依然超过限制怎么办?不要认为这是不可思议的事情,对于一些复杂的页面数据量超过1M很常见,那我们就直接把字符串写到本地文件中去,然后在接收的地方再读取文件就可以了。(注意异步的使用,以及如何保证文件写完后再跳转到接收的地方)
  • Binder的缓冲区对数据大小的限制明显(不超过1M,而且是15个线程共享这块空间,实际上你操作的线程内,数据超过200K都可能挂掉),那么干脆就换一个方式呗,直接用EventBus(或者自己用RxJava实现一个事件传递)就可以了。用EventBus的话,我们可以使用粘性事件,postStickyEvent,然后在下一个Activity中接收(不要忘了最后的removeStickyEvent操作)。

总结一下,遇到问题不可怕,一定要培养自己分析问题、解决问题的能力。开发工作做的久了,就不能只会写代码了,分析解决问题的能力显然更加重要,对于个人的成长是非常有用的,否则你真的就只能做一个“码农”了……