TransactionTooLargeException问题分析

摘要

TransactionTooLargeException 源码分析与解决方案思考

问题异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Caused by: android.os.TransactionTooLargeException: data parcel size 1105388 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:526)
at android.app.IActivityTaskManager$Stub$Proxy.startActivity(IActivityTaskManager.java:3884)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1731)
at android.app.Activity.startActivityForResult(Activity.java:5363)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:767)
at android.app.Activity.startActivityForResult(Activity.java:5304)
at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:754)
at com.kaola.modules.brick.component.BaseActivity.startActivityForResult(BaseActivity.java:486)
at com.kaola.core.center.gaia.RealGaiaInvoker.startActivityForResult(RealGaiaInvoker.java:95)
at com.kaola.core.center.gaia.RealGaiaInvoker.access$600(RealGaiaInvoker.java:27)
at com.kaola.core.center.gaia.RealGaiaInvoker$1.run(RealGaiaInvoker.java:244)
at com.kaola.core.center.gaia.RealGaiaInvoker.invokeActivityForResult(RealGaiaInvoker.java:251)
at com.kaola.core.center.gaia.RealGaiaInvoker.access$200(RealGaiaInvoker.java:27)
at com.kaola.core.center.gaia.RealGaiaInvoker$GaiaAsyncInvoker.run(RealGaiaInvoker.java:172)
at com.kaola.core.center.gaia.GaiaDispatcher.enqueueInvokeSync(GaiaDispatcher.java:39)
at com.kaola.core.center.gaia.RealGaiaInvoker.invokeSync(RealGaiaInvoker.java:87)
at com.kaola.core.center.router.GaiaRouterProxy.start(GaiaRouterProxy.java:56)
at com.kaola.core.center.router.BaseRouter.start(BaseRouter.java:74)
at com.kaola.core.center.router.RouterRequestBuilder.startForResult(RouterRequestBuilder.java:195)
at com.kaola.core.center.router.RouterRequestBuilder.startForResult(RouterRequestBuilder.java:190)
at com.kaola.sku.SkuActivityDispatcher.start(SkuActivityDispatcher.kt:74)
......

问题分析

1.先查看官方文档对 TransactionTooLargeException 的介绍
1
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.

译:跨进程通信传递/返回的数据太大,超出 transaction buffer 限制,抛出 TransactionTooLargeException 异常

1
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.

译:Binder transaction buffer1Mb 的大小限制,并且这个空间提供给一个进程里所有 transaction 使用。所以当绝大多数单独的 transaction 调用的参数大小并不大但是数量很多的时候,也会抛出这个异常。

2.分析堆栈

①.Activity.startActivityForResult -> Instrumentation.execStartActivity

②.ActivityManagerNative.getDefault()

③.ServiceManager.getService("activity") 分析,最终获取的 IBinderBinderProxy.

即:ServiceManager.getService("activity") -> ServiceManagerNative.asInterface(new BinderProxy()).getService("activity") -> new ServiceManagerProxy(new BinderProxy()).getService("activity")

④.回到②,可以知道 ActivityManagerProxy 的构造方法传进去的参数就是③中的 BinderProxy

⑤.最终调用到 android_util_Binder.cpp

⑥.最终异常在 signalExceptionForError 中抛出

parcelSize 大于 200K 就会报错,而这个 parcelSize 的大小,对应一下,发现就是 BinderProxy 的第二个参数,也就是说如果 Parcel 对象的大小超过 200K 就会报出这个错误。

前面文档指出所有 transaction 共用 1Mb 大小,源码分析下来,Android 还限制了每次 transaction 的数据不超过200K。

问题原因

很明显,异常 1105388 bytes 远大于 200K ,从页面跳转过程中,由于数据过多,数据传递超过限制,抛出 TransactionTooLargeException 异常。

问题背景

1.Flutter-Sku 页依赖商详页传递的 Sku 数据,Sku 的数据大小完全依赖服务器下发,客户端无法控制;

2.若为了避免数据传递,Flutter-Sku 页单独请求 Sku 数据,会存在一定延迟并且可能出现请求失败的现象,影响用户体验。

解决思路

1.NativeFlutter 页面之间的跳转:

因为 NativeFlutter 不能共享内存,所以使用本地持久化的方式,但该方式频繁的读写数据库,影响页面启动耗时,用户体验有损耗.

2.NativeNative 页面之间的跳转方式有两个解决方案:

①.持久化数据至本地,弊端同上

②.静态内存变量( Native 页面跳转不存在无法共享内存的情况),但清空静态内存变量的时机需要业务方自行控制,较分散也容易被遗漏,一旦积累过多,容易造成内存溢出

3.关于大数据传递,应该如官方所建议的那样:
1
Avoid transferring huge arrays of strings or large bitmaps. If possible, try to break up big requests into smaller pieces.

译:避免传递大数据如 strings 或者 bitmaps ,尽可能的将大数据分解成小块。

1
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.

译:如果实现一个 Service 服务者,强制性的规定 Binder Client 的远程调用的大小和制定一些复杂的约束。比如,如果结果集合可能会变的很大,那么就不允许 Binder Client 在一个时间点内请求超过一定数量;又或者可以选择性地当返回值很大的时候,不需要一次性返回所有数据,可以第一次先返回关键的数据后续如果需要的话让 Binder Client 再次去请求额外的信息。

问题扩展

很多方式可能导致抛出该异常,可参见l
What to do on TransactionTooLargeException
,大到传输图片,小到调用一次Dialog弹出。

复盘

在问题扩展中提到的两种方式,其实都有各自的弊端。更推荐的方式是,在页面跳转间不得不传递大数据的情况下使用提供上面的解决方案,其他情况还是系统方式。

由于是再正常不过的交互行为,可能我们没用办法做到降级和回滚,除了在一些我们有意识到可能出现大数据传递的场景下我们可以主动避免该异常。 在其他我们无法意识到的正常调用下,我们要做到可以可监控。

目前 Android 路由跳转上对参数做了改造和封装,使用动态代理的方式监控需要对路由规则做一层转换,区别于系统调用上的判断。

(未完待续)

参考

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