背景
在一个 Android 应用中,某个“账号解绑确认弹窗”大致是下面这种写法,确认动作依赖 Fragment 的一个成员变量:
class ConfirmDialogFragment : DialogFragment() {
private var onConfirm: (() -> Unit)? = null
fun setOnConfirmListener(listener: () -> Unit): ConfirmDialogFragment {
onConfirm = listener
return this
}
private fun onConfirmClick() {
onConfirm?.invoke()
}
}
也就是说,真正触发解绑逻辑的不是 arguments 或 savedInstanceState,而是 Fragment 实例里的字段 onConfirm。
最初做 MR review 时,提出了一个潜在风险:
如果设备在弹出确认弹窗后发生横竖屏切换,宿主 Activity 重建,那么弹窗里的
onConfirm由于不在Bundle中,理论上应该丢失。此时点击“确认解绑”,应该只dismiss,不应继续执行解绑逻辑。
但实际手测发现:
- 竖屏打开解绑确认弹窗
- 切换到横屏
- 点击“解绑”
- 解绑依然生效
这和最初的静态分析结论矛盾,因此需要做一次完整排查。
发生场景
问题发生时,确认解绑弹窗是挂在应用主界面 Activity 的 supportFragmentManager 下的。
实际链路如下:
主界面 Activity
└─ 首页 Fragment
└─ 内容 Fragment
└─ 打开账号管理 Dialog
└─ 点击解绑
└─ 打开确认解绑 Dialog
这意味着:
- 宿主
FragmentManager来自主界面 Activity - 确认解绑 Dialog 是挂在主界面 Activity 的
supportFragmentManager下的
而这个主界面 Activity 在 manifest 中并没有声明 configChanges:
<activity
android:name="..."
android:launchMode="singleInstance"
android:theme="..." />
因此,按标准 Android 语义,横竖屏切换时它应该走 destroy + recreate。
日志方案
为了直接回答这个问题,只加了最小必要的一组日志,统一 tag 为:AccountRotateDbg
- 主界面 Activity:
onCreate、onDestroy - 确认解绑 Dialog:
setOnConfirmListener、onCreateView、onDestroyView、onDestroy、确认点击时onConfirm是否存在 - 解绑逻辑入口:
unbindAccount、logout
目标只有三个:
- Activity 是否真的重建
- 确认解绑 Dialog 是否还是同一个实例
- 点击“解绑”时是否真的执行了解绑逻辑
实测日志
用户给出的关键日志如下:
2026-08-19 16:02:08.035 AccountRotateDbg onCreate instance=45440906 savedState=false orientation=1
2026-08-19 16:02:59.865 AccountRotateDbg unbindAccount provider=googleCalendar accountId=95800185641911bec36e572a057a9383
2026-08-19 16:02:59.867 AccountRotateDbg setOnConfirmListener instance=103017373 onConfirmSet=true
2026-08-19 16:02:59.870 AccountRotateDbg onCreate instance=103017373 savedState=false onConfirmSet=true
2026-08-19 16:02:59.871 AccountRotateDbg onCreateView instance=103017373 savedState=false onConfirmSet=true
2026-08-19 16:03:05.812 AccountRotateDbg onDestroy instance=45440906 isFinishing=false
2026-08-19 16:03:05.830 AccountRotateDbg onDestroyView instance=103017373 onConfirmSet=true
2026-08-19 16:03:05.925 AccountRotateDbg onCreate instance=197475998 savedState=true orientation=2
2026-08-19 16:03:06.365 AccountRotateDbg onCreateView instance=103017373 savedState=true onConfirmSet=true
2026-08-19 16:03:09.977 AccountRotateDbg confirmClick instance=103017373 onConfirmSet=true -> invoke logout
2026-08-19 16:03:09.991 AccountRotateDbg logout provider=googleCalendar accountId=95800185641911bec36e572a057a9383
2026-08-19 16:03:10.008 AccountRotateDbg onDestroyView instance=103017373 onConfirmSet=true
2026-08-19 16:03:10.009 AccountRotateDbg onDestroy instance=103017373 onConfirmSet=true
结论
1. 主界面 Activity 确实被销毁重建了
这一点由下面两行直接证明:
onDestroy instance=45440906 isFinishing=false
onCreate instance=197475998 savedState=true orientation=2
说明旋转时确实不是简单改方向,而是宿主 Activity 走了标准 recreate 流程。
2. 确认解绑 Dialog 实例没有变
确认解绑 Dialog 从显示到旋转后再到点击确认,一直是同一个实例:instance=103017373
日志里的关键模式是:
- 旋转前有
onCreateView - 旋转时只看到
onDestroyView - 旋转后又是同一个 instance 的
onCreateView - 中间没有出现
onDestroy
这说明旋转时发生的是:Fragment 实例被保留,只是 View 被销毁并重新创建。
3. onConfirm 没丢,所以解绑逻辑会继续执行
日志明确显示:
confirmClick instance=103017373 onConfirmSet=true -> invoke logout
logout ...
为什么会这样
这次排查的核心收获是:
在配置变更时,被销毁重建的是宿主 Activity,但 FragmentManager 可能会保留已经加入管理器的 Fragment 实例,只重建它们的 View。
这正是 AndroidX FragmentManager 在配置变更时的典型行为:
- 旧 Activity 销毁
- FragmentManager 通过非配置实例机制保留 Fragment 对象
- 新 Activity 创建
- 同一个 Fragment 实例重新 attach 到新宿主
- 走新的
onCreateView()
所以在这次场景里:
- 确认解绑 Dialog 对象本身没有换
- 它的普通成员字段
onConfirm也就还在
这是否说明当前实现完全安全
不能这么下结论。
这次只能说明:
在“宿主 Activity 因旋转重建,但确认解绑 Dialog 实例被 FragmentManager 保留”的这条路径里,
onConfirm不会丢。
但下面这些场景仍然可能出问题:
- 进程被系统杀掉后恢复
– lambda 不会被序列化
–onConfirm仍然会丢 - 开发者选项开启“不保留活动”
– 更容易触发真正的实例重建 - 某些恢复路径导致 Fragment 不是保留旧实例,而是重新 new
– 此时onConfirm仍然不会回来
因此,“这次横竖屏没问题”不代表“这种写法在所有恢复场景都没问题”。
工程判断
当前这个问题不需要因为“旋转必现异常”而阻塞,因为日志已经证明:在这条实际复现路径里,横竖屏后解绑仍会正常执行。
但从设计上,仍然有改进空间。
更稳妥的写法仍然是:
- 不依赖确认弹窗内存中的 lambda
- 改为通过
arguments传入必要参数,例如: providerNameaccountIdauthFeature- 在 Dialog 内直接执行 logout action
或者:
- 使用
FragmentResult回传“用户确认解绑” - 宿主根据可恢复数据自行处理
这样才能同时覆盖:
- 配置变更
- 进程重建
- 更严格的系统恢复场景
这次排查的经验
1. 生命周期问题不能只靠静态推断,必要时一定要打日志
这个问题如果只看代码,很容易得到一个“理论上应该丢 listener”的答案。
但日志证明:
- Activity 的确重建了
- Fragment 实例却被保留了
这类问题不用日志,很容易误判。
2. onDestroyView() 和 onDestroy() 不是一回事
这次正好是一个非常好的案例:
onDestroyView()发生了onDestroy()没有立即发生
说明:
- View 被销毁了
- Fragment 实例还活着
在分析“成员字段是否会丢失”时,这个区别非常关键。
总结
这次关于“确认解绑 Dialog 在横竖屏切换下的行为”的排查,最终结论是:
- 宿主主界面 Activity 在旋转时确实重建
- 确认解绑 Dialog 实例被 FragmentManager 保留
- 旋转时只重建了 Dialog 的 View
- 因为实例没换,内存中的
onConfirm仍然存在 - 所以横屏后点击“解绑”,logout 依然会执行
也就是说:
“横竖屏后解绑仍然生效”不是偶然,也不是另一个新 Dialog 被重新设置了 listener,而是 AndroidX FragmentManager 在配置变更时保留了原 Fragment 实例。
不过,这个结论只覆盖当前这条恢复路径;若要从工程设计上完全稳妥,仍建议把解绑动作改为可恢复的参数驱动,而不是依赖临时 lambda。
发表回复