【问题标题】:Misbehaviour of Backstack of activity when activity destroyed活动销毁时活动的Backstack行为不端
【发布时间】:2015-04-10 12:02:15
【问题描述】:

我有两个活动;假设 A 和 B。在 activity A 中注册了一个广播接收器,它监听将完成活动 A 的特定事件。我在 onCreate() 中注册广播接收器,并在 activity AonDestroy() 中销毁它.

为简单起见,activity B 中有一个button,名为“Destroy Activity A”。当用户点击button时,activity A应该被销毁。

通常这一切都运行顺利,没有任何问题,但问题出现在以下情况:

1) 假设我在activity B 并按下 Home 键将应用程序移至后台,那么如果我使用其他资源密集型应用程序,Android 系统将终止我的应用程序以释放内存。然后如果我从最近的任务中打开我的应用程序,activity B 将被恢复,它的onCreate()onResume() 等方法将被调用。现在我按button 来销毁activity A,但是活动A 已经被销毁,所以activity AonCreate()onResume() 等方法将不会被调用,除非我通过按去activity A back button。因此broadcast receiver 没有注册监听事件。

2) 当用户在设备设置的开发者选项中选择“不保留活动”时,也会出现同样的问题。

我一直在寻找解决这个问题很长时间,但我无法找到正确的答案。处理这种情况的最佳方法是什么?这是一个Android错误吗?这个问题应该有一些解决方案。

请帮帮我。

【问题讨论】:

  • ...注册了广播接收器,用于侦听将完成活动 A...的特定事件... - 如果活动已被销毁(在您提到的场景中) 注册该接收器以获取将完成该活动的事件的目的是什么?除了终止活动之外,您是否还在做一些额外的事情?
  • 如果A 被操作系统破坏了会有什么问题?
  • 当我按下返回按钮时,如果我从“活动 B”中按下“销毁活动 A”,我不想看到活动 A。
  • 阅读this 以更好地理解您的第一个问题,您也可以通过检查并要求用户禁用“不保留活动”选项来解决第二个问题,请参阅this
  • 无论这里真正的问题是什么,都应该使用其他技术来处理,例如Intent 标志或<activity> 属性来帮助控制后台堆栈。如果你有一个活动试图破坏另一个活动,那么你做错了。

标签: android android-activity broadcastreceiver android-lifecycle back-stack


【解决方案1】:

如果您的Activity A 已被 Android 操作系统本身破坏,则有 没办法追踪。

有些人建议通过在onDestroy 方法中列出事件来跟踪Activity A,但是如果您的Activity 被系统操作系统杀死,那么请注意它不会调用这些方法。

【讨论】:

  • 它可能会或可能不会被调用。一般来说,它被称为。但是不能依赖它,因为它可能不会被调用。
  • 如果操作系统内存不足,则不会调用此方法。
  • 是的,它可能不会被调用。我们不能确定它不会被调用。
【解决方案2】:

在保持当前广播逻辑的同时无法解决此问题。

从后台杀死活动,imo,不是正确的方法。您应该强烈考虑更改导航逻辑。

但是,如果您的项目很大并且时间很紧迫,那么重构是不可能的,A.J.的方法有效,但您提到您有很多活动需要杀死,他的解决方案变得非常难以实施。

我的建议如下。这可能不是最好的主意,但我想不出另一个。所以也许这会有所帮助。

您应该具备以下条件:

  • 您所有活动的基础活动。
  • 应用程序级别的ArrayList<String> activitiesToKill 对象。 (如果你没有扩展 Application 你可以将它作为静态变量

首先我们必须确保activitiesToKill 在操作系统终止低内存应用时不会丢失。在BaseActivity 我们在onSaveInstanceState 期间保存列表并在onRestoreInstanceState 中恢复它

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putSerializable("activitiesToKill", activitiesToKill);
}

private void onRestoreInstanceState(Bundle state) {
    if (state != null) {
        activitiesToKill = (ArrayList<String>) state.getSerializable("activitiesToKill");
    super.onRestoreInstanceState(state); 
}

}

这里的想法是通过使用它们的名称在列表中保存哪些活动应该被杀死。

逻辑如下:

假设您有活动 A、B、C、D 和 E

在活动E中,你按下按钮,你想杀死B和D

当您按下 E 中的按钮时,会将 B 和 D 的名称添加到 activitiesToKill 对象中。

activitiesToKill.add(B.class.getSimpleName()
activitiesToKill.add(D.class.getSimpleName()

在BaseActivity的onCreate方法中,我们要检查

if(savedInstanceState != null)
{
    //The activity is being restored. We check if the it is in the lest to Kill and we finish it                
    if(activitiesToKill.contains(this.getClass().getSimpleName()))
    {
        activitiesToKill.remove(this.getClass().getSimpleName())
        finish();
    }
}

如果活动通过广播被杀死,请确保删除活动的名称。

所以基本上每个场景都会发生这种情况。

如果应用程序正常运行,并且您单击按钮,则会发送广播,并且 B 和 D 将被杀死。确保从 activitiesToKill 中删除 B 和 D

如果应用被杀死并恢复,你按下按钮,广播将没有效果,但是你已经在activitiesToKill对象中添加了B和D。所以当你点击返回的时候,activity被创建并且saveInstanceState不为null,activity就结束了。

这种方法认为活动 E 知道它必须杀死哪些活动。

如果你不知道要从 E 中杀死哪些活动,你必须稍微修改一下这个逻辑:

使用HashMap&lt;String, bool&gt;代替ArrayList

当Activity B被创建时,它会将自己注册到hashmap中:

activitiesToKill.put(this.class.getSimpleName(), false)

然后在活动 E 中,您只需将所有条目设置为 true

然后在创建基本活动时,您必须检查此活动是否已在活动ToKill 中注册(哈希映射包含密钥)并且布尔值是 true 你杀死它(不要忘记将它返回给false,或删除密钥)

这确保了每个活动都将自己注册到 HashMap 并且活动 E 不知道要杀死的所有活动。并且不要忘记删除它们,以防广播杀死它们。

这种方法还确保在从 Intent 正常打开时不会杀死 Activity,因为在这种情况下 onSaveInstanceState 在 onCreate 中将为 null,因此不会发生任何事情。

如果您有一组活动需要通过不同的条件(不仅仅是单击按钮)终止,则可以完成更高级的检查,这样您就可以拥有一个 HashMap 的 HashMap 来将它们划分为类别。

另外请注意,如果您有多个名称相同但捆绑包不同的活动,则可以使用 getName 而不是 getSimpleName。

我希望我的解释足够清楚,因为我是从脑海中写出来的,如果有任何地方不清楚,请告诉我。

祝你好运

【讨论】:

  • 你提到过: if(savedInstanceState != null) { //活动正在恢复。我们检查它是否在最不可能杀死并完成它 if(activitiesToKill.contains(this.getClass().getSimpleName())) { activitiesToKill.remove(this.getClass().getSimpleName()) finish() ; this.getClass().getSimpleName() 这给了我 BaseActivity 的名称,而不是像 A、B、C 等要被杀死的活动......它永远不会满足条件。你检查过这个吗?
  • @Smeet Weird,因为我刚刚尝试过,我得到了孩子的简单名字。你可以试试 getName 吗?
  • 很好的答案。工作正常。感谢 Youssef 付出的努力和时间。
  • @Smeet 太好了,很高兴我能帮上忙。但是我强烈建议重构整个导航,并尝试找到一种逻辑来避免从后台关闭活动。但我知道这可能是一个一年的项目:)
  • 在 Android 中管理后台堆栈非常困难。当用户按下主页键并且我的应用程序进入后台时,我遇到了这个问题。长时间用户返回我的应用程序,如果同时会话过期,那么我想杀死一些后置的活动。但是,我会尝试找出最佳解决方案并在此处发布(如果有)。
【解决方案3】:

Activities 的主要规则之一是除了前台活动之外,您不能依赖任何活动的活动。您尝试对广播进行的操作与后退堆栈无关 - 后退堆栈不能保证所有活动始终处于活动状态,但它会确保在进入前台时重新创建它们。

在您的示例中(如果我了解您的目标),您需要导航到 A 下方的内容——例如 Activity Z,堆栈如下所示:Z-A-[B]。在正常的事件过程中,您点击back,它会将您带到A,然后在另一次点击之后 - 到Z,但在某些情况下(比如按下按钮)您想回到Z绕过 A -- 这是使用 FLAG_ACTIVITY_CLEAR_TOP 并显式启动 Z 的经典案例

Intent intent = new Intent(this, ActivityZ.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

这将完成BA,并将意图传递给Z。您可能还需要FLAG_ACTIVITY_SINGLE_TOP 标志,密切注意FLAG_ACTIVITY_CLEAR_TOP 的描述,您应该考虑一些技巧。

【讨论】:

    【解决方案4】:

    我不知道是否有可能以“正确”的方式处理这个问题。

    我想到的是以某种方式标记 A 活动。您不能使用startActivityForResult(),因为您将在调用onResume() 之前收到结果,即UI 已经膨胀。

    如果您使用 Otto,您可以尝试使用粘性事件。否则,您将需要一个单例来处理标志或将其保存到共享首选项中。

    在调用setContentView() 之前,您必须在onCreate() 方法上检查该标志,如果该标志为真,则完成活动。

    【讨论】:

    • 这是一个简单的场景,让问题更加清晰和容易,但在真正复杂的场景中,我必须管理大量的标志,并且这样做,它看起来像是对用户的闪烁效果。因为它会在短时间内对用户可见,然后我们销毁。
    • 创建活动时,如果您不调用 setContentView,则屏幕上不会显示任何内容。如果您调用 setContentView 然后终止活动,则会显示闪烁。
    • 是的,它将以白色或黑色背景显示。然而主要问题是我什么时候可以做到这一点?我应该收到特定的事件,以便我可以销毁它。
    【解决方案5】:

    根据您提供的信息,您在检查广播是否已注册后,在 Activity B 的 onCreate 中注册广播如何。如果在您提到的任何一种情况下都调用了活动 A 的 onDestroy,那么就会调用广播的注销。所以在这种情况下,你可以在Activity B的onCreate中注册你的Broadcast,这样你就可以收听它,即使你的backstack中只有Activity B。

    【讨论】:

      【解决方案6】:

      您是否考虑过使用Sticky Broadcast? 您还可以在应用程序级别(在清单中)注册您的接收器并监听此事件,无论Activity A 状态如何。

      但是,就像 Youssef 已经说过的那样,从后台杀死活动不是正确的方法。您应该强烈考虑更改导航逻辑。

      【讨论】:

        【解决方案7】:

        我想到了许多解决方案,但由于您没有提供有关您的应用的太多信息,所以我认为这应该可以正常工作。

        不用发射广播来杀死Activity A,只需在Activity B中按下“Kill Activity A”按钮时执行以下代码。

                Intent intent = new Intent(getApplicationContext(),
                        ActivityA.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
                intent.putExtra("EXIT", true);
                startActivity(intent);
        

        在活动A中添加以下代码

        @Override
        protected void onNewIntent(Intent intent) {
            super.onNewIntent(intent);
            if (intent.getBooleanExtra("EXIT", false)) {
                finish();
            }
        }
        
        protected void onCreate(Bundle savedInstanceState) {
            //Ideally, there should not be anything before this
            super.onCreate(savedInstanceState);
            if(getIntent().getBooleanExtra("EXIT", false)){
                finish();
                return;
            }
        

        在清单中为活动 A 设置“singleTop”启动模式。

        <activity
            android:name=".ActivityA"
            ...
            android:launchMode="singleTop" />
        

        这将产生以下后果:

        • 如果 Activity A 已经在运行,它将被带到 Activity 堆栈的前面并完成,从而将其从堆栈中移除。
        • 如果 Activity A 已被销毁但仍存在于 Activity 堆栈中(按下后退按钮时将启动),它将被启动、置于前面并完成,从而将其从 Activity 堆栈中移除。
        • 如果 Activity A 已经被销毁并且不存在于 Activity 堆栈中,并且您仍然按下“移除 Activity A”按钮,它将被启动、置于前面并完成。

        通常,您应该看不到任何闪烁。

        基于这个想法,您可以为您的特定应用构建性能更好的解决方案。例如,您可以使用 FLAG_ACTIVITY_CLEAR_TOP 并在 Activity B 的 onBackPressed() 中完成 Activity A。

        【讨论】:

        • 你是对的。但我给出了一个简单的场景。我想销毁多个活动,所以我使用了广播接收器。例如,backstack 中有 10 个活动(这是示例,根据用户导航的场景,它可能是我不知道的任意数量的活动)。我想销毁第 2、第 4 和第 5 个活动。我怎样才能销毁它?我不能重新排序到前面,因为我不知道后面堆叠的活动。
        • 如果您知道要销毁哪些活动,则可以使用此方法将它们全部销毁。只需设置所描述的意图并将其发送给所有人,这没有问题。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-14
        • 1970-01-01
        相关资源
        最近更新 更多