问题表现
只有Android12 和 13的机型会发生,而且没有明显的复现路径,有刚进入页面就崩的,也有使用一段时间才崩的。
为什么发生
问题表现,关键日志data parcel size 520600 bytes,差不多大小在508KB,崩溃日志如下
java.lang.RuntimeException:android.os.TransactionTooLargeException: data parcel size 520600 bytes
android.app.ActivityClient.activityStopped(ActivityClient.java:86)
......
Caused by:
android.os.TransactionTooLargeException:data parcel size 520600 bytes
android.os.BinderProxy.transactNative(Native Method)
android.os.BinderProxy.transact(BinderProxy.java:596)
android.app.IActivityClientController$Stub$Proxy.activityStopped(IActivityClientController.java:1297)
android.app.ActivityClient.activityStopped(ActivityClient.java:83)
android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:143)
android.os.Handler.handleCallback(Handler.java:955)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loopOnce(Looper.java:206)
android.os.Looper.loop(Looper.java:296)
android.app.ActivityThread.main(ActivityThread.java:8953)
java.lang.reflect.Method.invoke(Native Method)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:569)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:976)
从TransactionTooLargeException的文档上看,Binder 缓冲区有一个有限的固定大小,目前为 1MB,由进程正在进行的所有事务共享。也就是综合大于1MB时会发生这个异常,从崩溃日志看,大小都在500-600KB左右。 一般碰到这个问题都是在启动新的页面时,传输的数据过大导致触发异常。比如传入了巨大的字符串或者BitMap等 从堆栈看,都是发生在Activity的stop的时候,但是堆栈都是系统的,很难从堆栈中去发现问题。所以还需要用户的崩溃前浏览和点击信息
数据筛选和分析
大部分用户打开时间只有40s到2分钟之内,由于时间短,针对用户浏览过的页面做个归类统计,比较容易发现大部分用户都进入过同一个页面(这种比oom好排查多了~)
源码分析
简单概括就是,当Activity变得不可见时(onSaveInstanceState和onStop方法回调后),在应用进程这边会通过ActivityTaskManagerService的activityStopped方法(Binder通讯),把刚刚在onSaveInstanceState中保存了数据的Bundle对象(这个Bundle里面包含了已添加到该Activity的Fragment的所有信息(FragmentState),当然也包括Arguments),传到系统服务进程那边,在BinderProxy的transactNative会抛出TransactionTooLargeException异常,在源码里可能因素有很多,但一般都是传输的数据过大。
不再过多赘述,可以参考传送门,讲的非常清楚
为什么是Android-12?
这同样是问题分析的一个重要线索,在翻阅了Android12的变更文档之后,发现了这个
Activity lifecycle Root launcher activities are no longer finished on Back press Android 12 changes the default handling of the system Back press on launcher activities that are at the root of their tasks. In previous versions, the system would finish these activities on Back press. In Android 12, the system now moves the activity and its task to the background instead of finishing the activity. The new behavior matches the current behavior when navigating out of an app using the Home button or gesture. Note: The system applies the new behavior only to launcher activities that are the root of their tasks—that is, to activities that declare an Intent filter with both ACTION_MAIN and CATEGORY_LAUNCHER. For other activities, the system handles Back press as it did before, by finishing the activity. For most apps, this change means that users who use Back to navigate out of your app are able to more quickly resume your app from a warm state, instead of having to completely restart the app from a cold state. We recommend testing your apps with this change. If your app currently overrides onBackPressed() to handle Back navigation and finish the Activity, update your implementation to call through to super.onBackPressed() instead of finishing. Calling super.onBackPressed() moves the activity and its task to the background when appropriate and provides a more consistent navigation experience for users across apps. Also note that, in general, we recommend using the AndroidX Activity APIs for providing custom back navigation, rather than overriding onBackPressed(). The AndroidX Activity APIs automatically defer to the appropriate system behavior if there are no components intercepting the system Back press.
翻译下就是
activity 生命周期 按下“返回”按钮时,不再完成根启动器 activity Android 12 更改了在按下“返回”按钮时系统对为其任务根的启动器 activity 的默认处理方式。在以前的版本中,系统会在按下“返回”按钮时完成这些 activity。在 Android 12 中,现在系统会将 activity 及其任务移到后台,而不是完成 activity。当使用主屏幕按钮或手势从应用中导航出应用时,新行为与当前行为一致。 注意:系统仅会将新行为应用于为其任务根的启动器 activity,即使用 ACTION_MAIN 和 CATEGORY_LAUNCHER 声明 intent 过滤器的 activity。对于其他 activity,在按下“返回”按钮时,系统会像以前一样完成 activity。 对于大多数应用而言,此变更意味着使用“返回”按钮退出应用的用户可以更快地从温状态恢复应用,而不必从冷状态完全重启应用。 建议您针对此变更测试您的应用。如果您的应用目前替换 onBackPressed() 来处理返回导航并完成 Activity,请更新您的实现来调用 super.onBackPressed() 而不是完成 Activity。调用 super.onBackPressed() 可在适当时将 activity 及其任务移至后台,并可为不同应用中的用户提供更一致的导航体验。 另请注意,通常,我们建议您使用 AndroidX Activity API 提供自定义返回导航,而不是替换 onBackPressed()。如果没有组件拦截系统按下“返回”按钮,AndroidX Activity API 会自动遵循适当的系统行为。
简要概括,在Android 12,根Activity在home键退出后,不会finish,而是常驻在后台,从温状态恢复应用,而不是之前的冷启动。 但同时,如果系统希望常驻后台的话,当应用进程被杀,系统会默认恢复页面,所以此时大概能猜测是走到了onSaveInstanceState,系统恢复页面后又没有复用,导致的异常。
问题复现
第一步,借助工具toolargetool可以在观察页面(Activity\Fragment)走到stop时,SaveInstanceState里bundle的大小,大致原理就是onActivitySaveInstanceState和onFragmentSaveInstanceState时保存系统save的bundle,然后在(Activity\Fragment)stop或者destory时把这些bundle计算大小,然后打印到日志台。 第二步,打开开发者模式里的不保留活动,然后进入到问题页面后,home键退到后台,再进入app,这样能模拟线上app 后台被杀然后被恢复的过程,通过toolargetool观察发现,在每次页面被重建后,saved的bundle一直在增长,在重复多次后,触发TransactionTooLargeException(每次重建,由于bundle一直在增长,这个页面打开的速度会越来越慢)
问题解决
为什么每次页面被重建后,saved的bundle大小一直在增长?原因是这个页面是 Fragment->Fragment->ViewPage+Fragment的机构,系统会自动帮我们保存Activity里的Fragment和Fragment里的子Fragment,但是在页面重建时,业务代码并没有复用,而是每次都新建Fragment.
解决方式1,放弃所有缓存,简单粗暴
// 在Activity或者Fragment的onCreate方法里, super.onCreate(null);传null就行,
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(null);
///业务代码...
}
解决方式2
- Fragment可交互的情况下,为Fragment增加tag,由于可复用的Fragment是系统帮我们自动保存的,所以用tag找到相对应的Fragmetn即可
- ViewPager+Fragment的情况下,注意不要自己新建Fragment,把新建Fragment的时机交给PagerAdapter自己管理
后续如何治理和规避
线上监控:在工具toolargetool的基础上,增加信息统计上报,比如以200k为阀值,把数据偏大的save bundle上报
简单总结
在Android12的机子上,app按home键or返回键退到后台时,系统不会结束这些Activity,而是常驻后台,当手机内存紧张时,这些Activiy会被kill,当下次进入app时,系统会帮我们自动重建这些Activity,如果在这些Activity或者Fragment里处理不当,就会导致save bundle越来越大,重复几次上述步骤后,就会触发TransactionTooLargeException异常