Fragment找不到资源Id引起的线上Crash

2022-12-08 11:55:52 浏览数 (2)

一、问题起因

线上报了较多Fragment资源id找不到的Crash。找到对应资源int id fl_about_container 0x7f090283

从堆栈看全部在系统调用,首先想到先结合异常源码看看:

从代码片段看是当前Fragment.mContainerId存在,但通过findViewById找不到控件对象。再结合业务代码看:

该Fragment没有其他逻辑,布局也很简单,按道理,不应该存在资源找不到的情况。。。自此基本没法分析问题出现的场景以及根因。

该线上问题是某个版本出现,之前未出现过类似问题,这部分Fragment的代码也是很久没动过

二、尝试复现

首先看能否结合堆栈尝试线下复现,如果线下能复现,很大概率能分析清楚根因,找到解决办法。 crash堆栈里面走了onStart才到onCreateView,印象中正常进入Fragment的生命周期不是这样,于是做了个对比:

很明显与正常进入的堆栈不一样。正常进入这个fragment的时候并没有onStart的回调出现。

于是,大胆猜测线上crash的是不是出现了销毁重建的场景。一般销毁重建场景有:转屏,切后台被系统回收再切前台重建。

根据这部分业务场景,发现fragment和对应activity被强制横屏,不存在转屏的情况,所以考虑是后一种场景的可能性。

那么现在的问题是如何模拟出Activity销毁重建,来验证这个堆栈是否一致

开发者选项正好提供了这样的操作:不保留活动

开启后,在出现问题的AboutFragment页面进行前后台切换,来验证这个调用堆栈,发现操作后,应用直接crash,堆栈跟线上一模一样:

至此找到复现路径。

三、分析根因

一般对于能复现的问题,分正向分析和逆向分析。 1、逆向分析,通过排查版本发现,是一个升级较多库的提交导致,回退库会引发较多编译问题,排查起来较为困难 2、正向分析,通过日志调试寻找正常时序和异常时序 复现后,进行日志调试,梳理出调用时序。 先梳理操作路径:点击主页菜单(个人中心)-> 点击个人中心页面的菜单(设置)-> 点击设置菜单(关于片多多) 此时在“关于片多多”dump下FragmentManager的内容,具体dump代码:

FragmentManager fragmentManager = getFragmentManager(); StringWriter writer = new StringWriter(); PrintWriter printWriter = new PrintWriter(writer); fragmentManager.dump("",null,printWriter,null); Log.d("qwl",writer.toString());

dump内容较多,选出核心点信息:

梳理下重点信息:

Active Fragments(激活态)包含4个Fragment:

SettingsFragment 已经被移除mRemoving=true(这个对应调用过remove或replace),且不在添加态mAdded=false(true的话对应表示调用了add或replace)

AboutContainerFragment在添加态

AboutFragment在添加态

SettingsContainerNewFragment在添加态

Added Fragments(添加态):

AboutContainerFragment

AboutFragment

SettingsContainerNewFragment

和上面的mAdded=true是一致的。

Back Stack(操作堆栈):

Op就是事务中的operation操作记录:

#0: BackStackEntry{57d6350 #0} 12-07 16:25:11.210 30916 30916 D qwl : mName=null mIndex=0 mCommitted=true 12-07 16:25:11.210 30916 30916 D qwl : Operations: 12-07 16:25:11.210 30916 30916 D qwl : Op #0: ADD SettingsFragment{faa4052} (fc0fe3a1-0d6f-46b4-b445-ab59756eeba9 id=0x7f0902a6) 12-07 16:25:11.210 30916 30916 D qwl : #1: BackStackEntry{a85d949 #1} 12-07 16:25:11.210 30916 30916 D qwl : mName=null mIndex=1 mCommitted=true 12-07 16:25:11.210 30916 30916 D qwl : Operations: 12-07 16:25:11.210 30916 30916 D qwl : Op #0: REMOVE SettingsFragment{faa4052} (fc0fe3a1-0d6f-46b4-b445-ab59756eeba9 id=0x7f0902a6) 12-07 16:25:11.211 30916 30916 D qwl : Op #1: ADD AboutContainerFragment{49f2560} (eb97a280-43f3-428c-8778-559fc9849704 id=0x7f0902a6)

结合整个操作路径可以看到:

1、点击个人中心页面的菜单(设置),此时有Op #0: ADD SettingsFragment

说明进行了add或replace操作,SettingsFragment替换对应容器R.id.fl_settings_container

2、点击设置菜单(关于片多多) ,此时有Op #0: REMOVE SettingsFragment和Op #1: ADD AboutContainerFragment

说明进行了AboutContainerFragment的replace操作,删除了SettingsFragment,替换R.id.fl_settings_container

那么,继续按照复现路径操作: 1、点击home应用切后台,此时可以看到所有Fragment都调用了onDestroyView,表明已经销毁,因不是重点日志信息暂时省略。 2、在点击应用图标切回前台,这个时候会发生crash 在crash之前进行FragmentManager dump如下:

Crash前抓到的dump可以看到,多了一个SettingsFragment,同时堆栈操作还原里面有:

12-07 17:25:58.557 12390 12390 D qwl : #2: BackStackEntry{b8e6fef #2}

12-07 17:25:58.557 12390 12390 D qwl : mName=null mIndex=2 mCommitted=true

12-07 17:25:58.557 12390 12390 D qwl : Operations:

12-07 17:25:58.557 12390 12390 D qwl : Op #0: REMOVE AboutContainerFragment{9ca33c2} (3b5dc426-f3cf-4b6a-bdfc-ae0b0e8bde9d id=0x7f0902a6)

12-07 17:25:58.557 12390 12390 D qwl : Op #1: ADD SettingsFragment{20b1221} (b00b0ad7-5a1c-4ed3-ab37-26bf00e3ed57 id=0x7f0902a6)

删除了AboutContainerFragment同时添加了SettingsFragment,上面重点标红过,AboutContainerFragment和SettingsFragment替换的都是R.id.fl_settings_container,而Crash的直接堆栈报fl_about_container找不到,这个fl_about_container对应的是AboutFragment replace的AboutContainerFragment的布局容器id,如果fl_settings_container被SettingsFragment替换了,那么这里有可能导致AboutFragment找不到AboutContainerFragment在布局定义的fl_about_container的控件对象

猜是猜了,如何去验证这一猜测呢?

首先根据REMOVE AboutContainerFragment和ADD SettingsFragment历史操作堆栈,去业务中搜下看看是不是有replace操作,追溯代码调试发现SettingItemContainerFragment里面有进行这个操作

这里调用时序是

SettingsContainerNewFragment.onCreateView -> SettingsContainerFragment.onCreateView -> SettingItemContainerFragment.replaceFragment(R.id.fl_settings_container, SettingsFragment)

这里第一个参数就是R.id.fl_settings_container,第二个参数就是SettingsFragment,所以从后台切回前台新增了SettingsFragment,确实是进行了replace操作

基于此,也发现AboutContainerFragment也是在onCreateView去做了对应replaceFragment操作

注意这里的replaceFragment第二个参数是false,说明是不加入历史堆栈的,所以在进入“关于片多多”的Fragment和Crash发生前dump的历史操作堆栈是没有记录AboutFragment的replace操作的

到这里基本上把重要信息都拿到了,目前只需要重新完整梳理下调用时序,就知道问题根因在哪。

中间梳理过程就省略了,完整的调用时序如下:

1、当应用切到后台,且被系统销毁后,重新切回前台onCreate时序

这个链路调用没有问题,但不同于正常点击菜单跳转。正常点击菜单onCreate阶段这些Fragment都还没创建出来,所以常规流程不会有这样的连用链路。

接下来重点看onStart的调用链路:

在这种销毁重建的场景下,onStart阶段执行完了几乎所有的操作,但有执行的先后顺序。

1、FragmentActivity的onStart阶段因为FragmentManager缓存了销毁的3个Fragment:SettingsContainerNewFragment,AboutContainerFragment,AboutFragment

这里在onStart阶段就会创建这3个Fragment会把onCreate和onCreateView都执行完。因为SettingsContainerNewFragment和AboutContainerFragment在onCreateView阶段又调度了FragmentManager执行replace操作,所以此时历史操作堆栈里面应该还有2个replace操作待处理

2、当销毁的3个Fragment执行onCreate和onCreateView完毕后,此时FragmentManager还会调用execPendingActions,也正是因为调用了这个方法导致了Crash的出现。这个方法是立即执行挂起的操作,很显然在阶段1中有2个replace属于挂起操作,所以这个方法会把这2个replace立即执行。执行顺序:首先创建SettingsFragment,然后销毁AboutContainerFragment,在创建AboutFragment,很显然AboutFragment onCreate方法能执行,但onCreateView方法执行不了,因为在FragmentStateManager中

这里在看抛出异常的链路就很清楚了:

自此整个Crash的调用链路和发生场景都搞清楚了。

四、解决方案

明确Crash发生的根因以及具体调用链路,那么只需要将顶层SettingsContainerNewFragment.onCreateView进行的repalce操作放到对应onStart回调之后即可,这样在FragmentActivity执行onStart时,FragmentMananger执行execPendingActions就只有AboutFragment的一个replace挂起的操作,这个操作再onStart阶段执行的时候由于AboutContainerFragment存在,就不会出现上述问题。

五、回顾

线上Crash堆栈发生在系统调用确实不好分析,只能通过一点点线索切入

0 人点赞