Fragment和Activity生命周期管理的巨大区别

4 阅读5分钟
# Fragment重建之谜:我的回调为何在 onResume() 时离奇消失?

> “快跟我说说,你是怎么把庐江城攻下来的?” —— 袁术
>
> “主公,此事说来话长,其中颇为诡异……” —— 孙策


## 案发现场

想象一个场景:我们的App可以被设置为系统的“默认电话应用”。当用户在手机设置里,将我们的App从“默认”宝座上撤下来,换回系统自带的电话App,然后再切回我们的App时,一个诡异的NullPointerException(或者是一个依赖回调的逻辑失效)发生了。

问题出在ManageFragment的onResume()方法里。我在这里需要使用一个通过AirIMEngine单例设置的回调,但在出事时,这个回调对象竟然是null!

```kotlin
// in ManageFragment.kt
override fun onResume() {
    super.onResume()

    // 尝试使用回调,但AirIMEngine.getCallback()返回了null,导致后续逻辑崩溃
    AirIMEngine.getCallback().doSomething() // 崩溃发生在这里!
}

这太奇怪了!因为我非常确定,在onViewCreated()里,我已经把回调设置好了:

kotlin

// in ManageFragment.kt
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    Log.d(TAG, "onViewCreated: 设置回调")
    AirIMEngine.setConnectStateCallback(iMConnectCallback) // 明明在这里设置了!
}

我们都知道,Fragment的生命周期是onViewCreated() -> onStart() -> onResume()。既然onViewCreated执行了,回调就应该在那里。为什么到了onResume,它就“人间蒸发”了呢?

初步排查与“现场急救”

面对线上可能的崩溃,第一反应是先解决问题。最直接的“暴力”修复方案是在onResume里再加一次检查和设置:

kotlin

// in ManageFragment.kt
override fun onResume() {
    super.onResume()

    // 如果发现回调没了,就再设置一次
    if (!AirIMEngine.hasCallback()) {
        Log.w(TAG, "onResume: 回调丢失了!重新设置!")
        AirIMEngine.setConnectStateCallback(iMConnectCallback)
    }

    AirIMEngine.getCallback().doSomething() // 这下安全了
}

这段代码像一个“创可贴”,它能完美地解决崩溃。但是,作为有追求的开发者,我们不能满足于此。回调为什么会丢失? 这个问题就像一根刺,不拔不快。不搞清楚根本原因,难保它不会在别的地方以更诡异的形式再次出现。

深入调查:找到“另一个你”

为了找到真相,我祭出了Log大法,在onViewCreated和onDestroy里打印出this实例的哈希值。当我再次复现操作后,Logcat里的日志让我大跌眼镜:

log

// 更改默认应用,返回App后...
D/MyLog: ManageFragment > onViewCreated, this:ManageFragment{**1f02326**}
D/MyLog: Manage-Fragment > onViewCreated, this:ManageFragment{**47cf2fb**}
D/MyLog: ManageFragment > onDestroy, this:ManageFragment{**1f02326**}

真相大白!

在返回MainActivity的那一刻,系统里竟然出现了两个ManageFragment的实例!

  1. 一个实例(我们叫它F_A {1f02326})先被创建,并执行了onViewCreated。
  2. 紧接着,另一个全新的实例(F_B {47cf2fb})也被创建,并执行了onViewCreated。
  3. 最关键的是,在这一切之后,第一个实例F_A的onDestroy方法被调用了。

现在,我们可以完整地还原整个“犯罪过程”了:

  1. 更改默认电话App被系统视为一次重大的“配置变更”(Configuration Change),系统决定销毁并重建我们的MainActivity。
  2. MainActivity重建开始,FragmentManager从savedInstanceState中,自动恢复了它之前管理的ManageFragment实例,这就是F_A。
  3. F_A执行onViewCreated,调用AirIMEngine.setConnectStateCallback(),此时单例中的回调被成功设置为F_A的iMConnectCallback实例。
  4. 然而,在MainActivity的onCreate方法中,很可能存在这样一段代码:supportFragmentManager.beginTransaction().replace(R.id.container, new ManageFragment()).commit()。这段代码并不知道FragmentManager已经恢复了一个Fragment,它只是忠实地执行指令——创建并添加一个全新的ManageFragment实例,也就是F_B。
  5. F_B执行onViewCreated,再次调用AirIMEngine.setConnectStateCallback(),此时单例中的回调被覆盖为F_B的iMConnectCallback实例。
  6. replace事务的执行,意味着容器中所有旧的Fragment都将被移除。因此,刚刚被恢复的F_A被立即标记为销毁。
  7. F_A的onDestroy方法被触发。在onDestroy中,我们通常会清理资源,调用了AirIMEngine.removeCallback()。
  8. 致命一击:removeCallback()将AirIMEngine单例中的回调设置为了null。它清除了F_B刚刚才设置好的回调!
  9. 最后,F_B的onResume方法终于执行,当它尝试去获取回调时,发现它已经在上一步被F_A的“临终遗言”给清除了。

这个过程完美解释了“回调离奇失踪”的现象。其根源在于FragmentManager的自动恢复机制和我们代码中手动的Fragment事务发生了冲突。

终极解决方案:相信savedInstanceState

这个问题的解决方案非常简单,也是Google官方推荐的最佳实践。在Activity的onCreate方法中,将所有手动的Fragment事务都包裹在一个if (savedInstanceState == null)的判断里:

kotlin

// in MainActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 关键!只在Activity首次创建时才手动添加Fragment
    if (savedInstanceState == null) {
        supportFragmentManager.beginTransaction()
            .replace(R.id.fragment_container, ManageFragment.newInstance())
            .commit()
    }
    // 如果savedInstanceState不是null,说明Activity正在从之前的状态中恢复,
    // FragmentManager会自动为我们恢复Fragment,我们无需再做任何事。
}
  • 当Activity首次启动时,savedInstanceState为null,我们手动创建并添加Fragment。
  • 当Activity因为配置变更被重建时,savedInstanceState不再为null,if判断块不会执行,我们把舞台完全交给FragmentManager,让它从容地恢复之前的Fragment实例。

这样一来,就不会有第二个Fragment实例来捣乱,生命周期也就恢复了正常,回调丢失的问题自然迎刃而解。

结案陈词

这次“探案”经历给了我们几个深刻的教训:

  1. 永远用if (savedInstanceState == null)保护你的首次Fragment事务。  这是处理Activity重建时Fragment状态的黄金法则。
  2. 警惕单例(Singleton)与生命周期的爱恨情仇。  当一个有生命周期的组件(如Fragment)去修改一个全局单例的状态时,尤其要小心在重建过程中新旧实例交替时可能引发的竞态条件。
  3. 日志是你的“显微镜”。  当你遇到看似不合逻辑的生命周期问题时,在关键节点打印出实例的哈希码(this),往往能帮你发现“另一个你”的存在。

希望这次的分享能帮助大家在Fragment的“深水区”里游得更稳。如果你也遇到过类似的“诡异”问题,欢迎在评论区分享你的故事