嵌套片段 - 非法状态异常“在 onSaveInstanceState 之后无法执行此操作”背景我的问题更新 2更新1:快速和肮脏(官方)解决方案我的解决方案结论

2022-09-03 16:03:42

背景

安卓系统中的异步回调

尝试在Android上以可靠的方式执行异步操作是不必要的复杂,即AsyncTask是否真的在概念上存在缺陷,或者我只是错过了一些东西?

现在,这都是在引入碎片之前。随着 Fragments 的引入,onRetainNonConfigurationInstance() 已被弃用。因此,最新的Google纵容黑客攻击是使用持久的非UI片段,该片段在发生配置更改时(即旋转屏幕,更改语言设置等)从您的活动中附加/分离。

示例:https://code.google.com/p/android/issues/detail?id=23096#c4

非法状态异常 - 无法在 onSaveInstanceState 之后执行此操作

从理论上讲,上面的黑客可以让你绕过可怕的:

IllegalStateException - "Can not perform this action after onSaveInstanceState"

因为持久性非 UI 片段将接收 onViewStateRestored() (或者 onResume) 和 onSaveInstanceState() (或者 onPause) 的回调。因此,您可以判断何时保存/恢复实例状态。对于如此简单的东西来说,这是一段相当多的代码,但是利用这些知识,您可以将异步回调排队,直到活动的FragmentManager在执行它们之前将其mStateSaved变量设置为false。

mStateSaved 是最终负责触发此异常的变量。

private void checkStateLoss() {
    if (mStateSaved) {
        throw new IllegalStateException(
                "Can not perform this action after onSaveInstanceState");
    }
    if (mNoTransactionsBecause != null) {
        throw new IllegalStateException(
                "Can not perform this action inside of " + mNoTransactionsBecause);
    }
}

因此,从理论上讲,现在您知道何时可以安全地执行片段交易,因此可以避免可怕的非法状态异常。

错!

嵌套片段

上述解决方案仅适用于活动的 FragmentManager。片段本身也有一个子片段管理器,用于嵌套片段。遗憾的是,子片段管理器根本没有与活动的片段管理器保持同步。因此,虽然活动的片段管理器是最新的,并且始终具有正确的mStateSaved;儿童碎片不这么认为,并且会很乐意在不适当的时间抛出可怕的非法状态异常。

现在,如果您已经查看了支持库中的 Fragment.java和 FragmentManager.java您不会对与片段相关的所有事情都容易出错感到惊讶。设计和代码质量是...啊,值得怀疑。你喜欢布尔值吗?

mHasMenu = false
mHidden = false
mInLayout = false
mIndex = 1
mFromLayout = false
mFragmentId = 0
mLoadersStarted = true
mMenuVisible = true
mNextAnim = 0
mDetached = false
mRemoving = false
mRestored = false
mResumed = true
mRetainInstance = true
mRetaining = false
mDeferStart = false
mContainerId = 0
mState = 5
mStateAfterAnimating = 0
mCheckedForLoaderManager = true
mCalled = true
mTargetIndex = -1
mTargetRequestCode = 0
mUserVisibleHint = true
mBackStackNesting = 0
mAdded = true

无论如何,有点偏离主题。

死胡同解决方案

因此,您可能会认为问题的解决方案很简单,在这一点上似乎有点反义词,将另一个漂亮的黑客非UI片段添加到您的子片段管理器中。据推测,它的回调与其内部状态同步,一切都会很花哨。

又错了!

Android 不支持作为子级附加到其他片段(也称为嵌套片段)的保留片段实例。现在,事后看来,这应该是有道理的。如果在活动因未保留而销毁父片段时销毁父片段,则无需重新附加子片段。所以这是行不通的。

我的问题

是否有人有解决方案来确定何时与异步代码回调一起对子片段执行片段事务是安全的?


答案 1

更新 2

React Native

如果你能忍受它,使用React Native。我知道 我知道。。。“肮脏的网络技术”,但严肃地说,Android SDK是一场灾难,所以吞下你的骄傲,只是试一试。你可能会给自己一个惊喜;我知道我做到了!

不能或不会使用 React Native

不用担心,我建议从根本上改变你的网络方法。触发请求并运行请求处理程序来更新 UI 与 Android 的组件生命周期不太兼容。

相反,请尝试以下方法之一:

  1. 移动到基于的简单消息传递系统,并让长期存在的对象(常规Java类或Android服务)执行您的请求并在应用程序的本地状态更改时触发事件。然后在您的活动/片段中,只需收听某些内容并相应地进行更新。LocalBroadcastReceiverIntent
  2. 使用反应式事件库(例如RxJava)。我自己没有在Android上尝试过,但是使用类似的概念库ReactiveCocoa在Mac /桌面应用程序上取得了相当大的成功。诚然,这些库的学习曲线相当陡峭,但是一旦你习惯了它,这种方法就会非常令人耳目一新。

更新1:快速和肮脏(官方)解决方案

我相信这是谷歌最新的官方解决方案。但是,该解决方案确实不能很好地扩展。如果您不愿意自己弄乱队列,处理程序和保留的实例状态,那么这可能是您唯一的选择...但不要说我没有警告你!

Android活动和片段支持LoaderManager,可以与AsyncTaskLoader一起使用。后台加载器管理器的保留方式与保留片段完全相同。因此,此解决方案确实与下面的我自己的解决方案有一些共同之处。AsyncTaskLoader是一个部分预制的解决方案,在技术上确实有效。但是,API非常繁琐;因为我相信你会在使用它的几分钟内注意到。

我的解决方案

首先,我的解决方案绝不容易实现。但是,一旦您的实现正常工作,使用起来就轻而易举了,您可以根据自己的内心内容对其进行自定义。

我使用添加到活动的片段管理器(或在我的情况下支持片段管理器)的保留片段。这与我的问题中提到的技术相同。此片段充当某种提供程序,跟踪它附加到的活动,并具有 Message 和 Runnable(实际上是自定义子类)队列。当实例状态不再保存并且相应的处理程序(或可运行)“准备执行”时,队列将执行。

每个处理程序/可运行存储一个引用使用者的 UUID。使用者通常是活动内某处的片段(可以安全地嵌套)。当使用者片段附加到活动时,它会查找提供者片段并使用其 UUID 注册自身。

重要的是,你使用某种抽象,如UUID,而不是直接引用消费者(即片段)。这是因为片段经常被销毁和重新创建,并且您希望回调具有对新片段的“引用”;而不是属于被摧毁的活动的旧活动。因此,不幸的是,您很少可以安全地使用匿名类捕获的变量。同样,这是因为这些变量可能引用旧的已销毁片段或活动。相反,您必须向提供程序请求与处理程序存储的 UUID 匹配的使用者。然后,您可以将此使用者强制转换为它实际上所在的任何片段/对象,并安全地使用它,因为您知道它是具有有效上下文(活动)的最新片段。

使用者(由 UUID 引用)准备就绪时,处理程序(或可运行)将“准备好执行”。除了提供者之外,有必要检查消费者是否准备就绪,因为正如我的问题中提到的,消费者片段可能认为其实例状态已保存,即使提供者另有说明。如果使用者(或提供者)尚未准备就绪,则将 Message(或可运行)放在提供者的队列中。

使用者片段到达 onResume() 时,它会通知提供者它已准备好使用排队的消息/运行对象。此时,提供程序可以尝试在其队列中执行属于刚刚准备就绪的使用者的任何内容。

这导致处理程序始终使用有效的上下文(提供程序引用的活动)和最新的有效片段(也称为“使用者”)执行。

结论

该解决方案非常复杂,但是一旦您弄清楚如何实现它,它确实可以完美地工作。如果有人想出一个更简单的解决方案,那么我很乐意听到它。


答案 2

推荐