今天是 2019年09月13日,农历八月十五。没错今天是中秋节,而且对我来说,今年的中秋不一样🖕。
故事是这样的 ——
中秋前一天的晚上线上发现了问题,在家改bug到后半夜。中秋当天上午早上又接到线上的问题反馈,于是在家与其他几个小伙伴一起连线排查解决。经过一上午的努力,修改了几个问题,但是有两个问题没有查到原因和解决办法,于是决定下午去公司解决,还好下午狠顺利的将问题复现了,并找到了原因和解决办法。当把问题都回归验证后,已经是快晚上7点钟了。回到家不到 8 点,吃了一大碗大家下午包的给我一个人留得饺子……
扯蛋完毕,现在回到正题。
需求
用户按返回键即将退出当前页面前,需要立即请求接口判断是否显示一个 Dialog,如果展示 Dialog 则不退出页面,否则再退出页面。
模拟代码
1 | class MainActivity : AppCompatActivity() { |
问题
正常情况下,一个请求几百毫秒内就可以完成,但是如果在弱网环境下,当用户点击返回键后,由于长时间没有反应,此时可能用户着急了,于是按下 Home 键,应用进入后台。一段时间后网络请求成功(返回结果为不显示 Dialog)或请求超时,用户再次返回应用时,此时发生如下异常:
- Android 8.0(不包含8.0) 以下 ANR。
- Android 8.0+ 报以下错误,
1 | 2019-09-13 21:39:05.780 2106-2106/com.xiaomai E/AndroidRuntime: FATAL EXCEPTION: main |
问题重现
为了模拟弱网环境,方便问题重现,这里把请求时间改为 3s。
1 | Handler().postDelayed({ |
当按下返回键后,立即按 Home 键,等待 3 秒后再次回到应用,问题重现。
原因排查
从堆栈的异常日志可以看到:异常发生的原因与 Fragment 有关,并且对 Fragment 进行了不正当的操作。
看一下代码中哪个地方用到了 Fragment 呢?仔细一行行的看过代码后,发现没有地方使用 Fragment!!! WHAT?????
这是为什么呢?代码中没有任何地方使用 Fragment,错误堆栈中却报了与 Fragment 有关的信息,再仔细看一下堆栈的异常日志,发现有一个叫 ReportFragment
的类。
原来,ReportFragment 是 Android 为了实现 Lifecycles 相关功能而自动添加的。
接下来,调试一下当应用从后台返回时都发生了什么?
1 | handleResumeActivity:3685, ActivityThread (android.app) |
代码分析一(此时的 FragmentManager 为 Activity 中的 FragmentManager):
1 | public boolean execPendingActions() { |
代码分析二(此时 FragmentManager 仍为 Activity 中的):
1 | private void ensureExecReady(boolean allowStateLoss) { |
虽然这里找到了异常信息,但是这时并没有抛出异常。
接下来的运行如下:
1 | dispatchStart:189, FragmentController (android.app) |
代码分析三(此时 FragmentManager 仍为 Activity 中的):
1 | private void dispatchMoveToState(int state) { |
这里要注意的是 mExecutingActions = true;然后就进入了 moveToState 方法。
代码分析四(此时 FragmentManager 仍为 Activity 中的):
1 | void moveToState(int newState, boolean always) { |
代码分析五(此时 FragmentManager 为 Activity 中的):
1 | void moveFragmentToExpectedState(final Fragment f) { |
代码分析六(此时进入 Fragment):
1 | public class Fragment { |
虽然这里再次进入代码分析一,但这时的 FragmentManager 与前面的 FragmentManager 已经不是同一个了,之前是 Activity 中的而现在是 ReportFragment 中的 childFragmentManager,并且 Activity 的栈帧仍然处在代码分析三的 moveToState() 方法中,mExecutingActions 仍然为 true。
代码分析一(此时的 FragmentManager 为 ReportFragment 中的 childFragmentManager):
1 | public boolean execPendingActions() { |
代码分析二(此时的 FragmentManager 为 ReportFragment 中的 childFragmentManager):
1 | private void ensureExecReady(boolean allowStateLoss) { |
这时仍然没有抛出异常。
接下来的运行仍然如下:
1 | dispatchStart:189, FragmentController (android.app) |
代码分析三(此时的 FragmentManager 为 ReportFragment 中的 childFragmentManager):
1 | private void dispatchMoveToState(int state) { |
这里仍然要注意的是 mExecutingActions = true;然后就进入了 moveToState 方法。
代码分析四(此时的 FragmentManager 为 ReportFragment 中的 childFragmentManager):
1 | void moveToState(int newState, boolean always) { |
因为这里不会进入 for 循环,所以方法运行结束后,会进入代码分析三中 finally 代码块中,将 mExecutingActions 赋值为 false;这时方法执行完毕,回到代码分析六,继续向下执行:
1 | public class Fragment { |
代码分析七:
1 | public class ReportFragment extends Fragment { |
在 ReportFragment 的 onStart() 方法,会对 OnStart 事件进行分发,之后 LiveData 在 MainActivity 中等到响应:
1 | override fun onBackPressed() { |
代码分析八:
1 | public class FragmentActivity { |
代码分析九:
1 | public class Activity { |
代码分析十(此时的 FragmentManager 为 Activity 中的 FragmentManager):
1 |
|
我们再次进入到代码分析二中:
1 | private void ensureExecReady(boolean allowStateLoss) { |
因为前面在代码分析三中我们强调过,Activity 中的 mExecutingActions 为 true 后,就再也没有被修改为 false,所以这里抛出了异常。
至此,问题的原因已经找到了,是由于 Activity 中的 FragmentManager 的第一次任务还没有执行完毕,其他的操作又导致它需要进行第二次任务,所以发生错误。
解决方案
问题的原因已经找到了,所以当第一次任务没有执行结束时,如果有第二个任务到来,我们可以从两个方面去解决问题:
直接丢弃第二个任务
在页面不可见时,取消对 LiveData 的监听,这样页面重新可见时,就不会接收到变化的通知了,修改代码如下:
1 | class MainActivity : AppCompatActivity() { |
等待第一个任务执行完毕后再执行第二个任务
通过 Handler.post() 方式,将任务滞后完成。
1 | class MainActivity : AppCompatActivity() { |