15

背景

Android 中的异步回调

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

现在,这一切都发生在 Fragments 引入之前。随着 Fragments 的引入,onRetainNonConfigurationInstance()已被弃用。所以最新的谷歌纵容黑客是使用一个持久的非 UI 片段,当配置更改发生时(即旋转屏幕、更改语言设置等)从你的 Activity 附加/分离。

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

IllegalStateException - 在 onSaveInstanceState 之后无法执行此操作

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

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);
    }
}

所以理论上,现在您知道何时执行片段事务是安全的,因此您可以避免可怕的 IllegalStateException。

错误的!

嵌套片段

上述解决方案仅适用于 Activity 的 FragmentManager。片段本身也有一个子片段管理器,用于嵌套片段。不幸的是,子片段管理器根本不与 Activity 的片段管理器保持同步。因此,虽然活动的片段管理器是最新的并且始终具有正确的 mStateSaved;子片段不这样认为,并且会在不适当的时候愉快地抛出可怕的 IllegalStateException。

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

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

总之,有点跑题了。

死胡同解决方案

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

又错了!

Android 不支持作为子片段附加到其他片段(也称为嵌套片段)的保留片段实例。现在,事后看来,这应该是有道理的。如果在activity被销毁时父fragment因为没有被保留而被销毁,那么就没有地方可以重新附加子fragment了。所以这是行不通的。

我的问题

是否有人有解决方案来确定何时可以安全地对子片段执行片段事务以及异步代码回调?

4

1 回答 1

4

更新 2

反应原生

如果您能忍受,请使用 React Native。我知道,我知道……“肮脏的网络技术”,但说真的,Android SDK 是一场灾难,所以放下你的骄傲,试一试吧。你可能会让自己大吃一惊;我知道我做到了!

不能或不会使用 React Native

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

而是尝试以下之一:

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

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

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

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

我的解决方案

首先,我的解决方案实施起来并不简单。但是,一旦您的实现工作起来,使用起来就很容易了,您可以根据自己的喜好对其进行自定义。

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

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

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

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

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

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

结论

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

于 2013-07-29T01:08:17.223 回答