活动被销毁时,后退栈的不良行为

25

我有两个活动,假设为A和B。在activity A中注册了一个广播接收器,用于监听特定事件并终止activity A。我在activity AonCreate()方法中注册广播接收器,并在onDestroy()方法中销毁它。

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

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

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

2)当用户在设备设置中从开发人员选项中选择“不保留活动”时,将出现相同的问题。

我一直在寻找解决这个问题的方法,但是我无法找到合适的答案。处理这种情况的最佳方法是什么?这是Android的一个bug吗?应该有一些解决方案来解决这个问题。

请帮助我。


1
...已经注册了一个广播接收器来监听特定事件,该事件将结束A活动...- 如果活动已经被销毁(在您提到的场景中),那么注册接收器以获取结束活动的事件有什么意义呢?除了杀死活动之外,您是否还做了其他额外的事情? - user
1
当我按下返回按钮时,如果我从“Activity B”中按下“销毁Activity A”,我不想再看到Activity A。 - Smeet
2
无论真正的问题是什么,都应该使用其他技术来处理,比如使用Intent标志或<activity>属性来帮助控制后退栈。如果一个活动试图销毁另一个活动,那就错了。 - CommonsWare
我很好奇你是如何通过Activity B来"destroy" Activity A的?听起来非常可疑,好像你在做一些不应该做的事情。 - Kai
嗨CommonsWare,欢迎。是的,有一些情况下,我可以使用活动的标志,如清除顶部等...我想销毁一些随机活动...我在我的应用程序中也有相同的情况。这就是为什么我正在使用BroadcastReceiver来销毁这些活动。 - Smeet
显示剩余4条评论
7个回答

6

如果您的Activity A被Android操作系统自己销毁,那么就没有追踪的方法。

有些人建议通过在onDestroy方法中监听事件来跟踪Activity A,但是如果您的Activity被操作系统杀死,则不会调用这些方法。


它可能会被调用,也可能不会被调用。通常情况下,它会被调用。但是不能依赖于此,因为它可能不会被调用。 - A.J.
1
如果操作系统内存较低,则不会调用这些方法。 - dharmendra
是的,它可能不会被调用。我们不能确定它不会被调用。 - A.J.

3

如果要保留您目前的广播逻辑,则无法修复此问题。

我认为,从后台堆栈中杀死活动并不是正确的方法。您应该强烈考虑更改导航逻辑。

但是,如果您的项目很大,时间很紧,重构也不可行,那么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。

如果应用程序已经被杀死并恢复,您按下按钮,广播不会产生任何效果,但您已将B和D添加到activitiesToKill对象中。因此,当您点击返回时,该活动将被创建并且savedInstanceState不为null,则该活动将被结束。

此方法考虑到E活动知道需要杀死哪些活动。

如果您不知道要从E活动杀死哪些活动,则必须稍微修改此逻辑:

不要使用ArrayList,而是使用HashMap<String,bool>

当创建B活动时,它会将自己注册到哈希映射表中:

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

从活动E开始,您只需要将所有条目设置为true 然后在基本活动的onCreate中,您必须检查是否该活动已在activitiesToKill中注册(哈希映射包含键)且布尔值为true,则杀死它(不要忘记将其返回为false或删除键)
这确保每个活动都向HashMap注册自己,而Activity E不必知道所有要终止的活动。并且不要忘记在广播杀死它们的情况下将它们移除。
这种方法还确保在从意图正常打开时不会杀死活动,因为在这种情况下,在onCreate中 onSaveInstanceState将为空,因此不会发生任何事情。
如果有一组需要根据不同条件(不仅是按钮单击)终止的活动,则可以执行更高级别的检查,因此可以使用哈希图的哈希图将它们分成类别。
另请注意,如果有多个名称相同但bundle不同的活动,则可以使用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
@Smeet很奇怪,因为我刚刚尝试了一下,我获得了子类的简单名称。你可以尝试使用getName吗? - Y2theZ
很棒的答案。正常运行。感谢Youssef的付出和时间。 - Smeet
@Smeet 很好,很高兴我能帮到你。但是我强烈建议重构整个导航,并尝试找到一种逻辑来避免从后堆栈中关闭活动。但我知道这可能需要一年的时间 :) - Y2theZ
在Android中管理后退栈非常困难。当用户按下Home键并且我的应用程序进入后台时,我会遇到这个问题。如果长时间后用户返回我的应用程序,并且此时会话已过期,则我想要杀死一些后退堆栈活动。然而,我尝试找到最佳解决方案,并在此处发布。 - Smeet

1
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的描述,有一些诡计需要考虑。

0

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

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

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

在调用 setContentView() 之前,在您的 onCreate() 方法中检查该标志,如果该标志为 true,则完成该活动。


这只是为了让问题更清晰易懂而给出的简单场景,但在真正的复杂场景中,我必须管理大量的标志并像这样操作,这看起来会对用户产生闪烁效果。因为它将对用户可见一段时间,然后我们就销毁了。 - Smeet
在创建活动时,如果您不调用setContentView,则屏幕上将不显示任何内容。如果您调用setContentView然后杀死活动,则会出现闪烁。 - Axxiss
是的,它将显示为白色或黑色背景。然而,主要问题是我什么时候可以做到这一点?我应该接收特定事件以便我可以销毁它。 - Smeet

0
根据您提供的信息,您可以在Activity B的onCreate中注册广播,并在检查其是否已注册后进行。如果在您提到的任何一种情况下都调用了Activity A的onDestroy,则广播的注销将被调用。因此,在这种情况下,您可以在Activity B的onCreate中注册广播,以便即使只有Activity B在您的后台堆栈中,您也可以收听它。

0

您是否考虑过使用粘性广播?此外,您可以在应用程序级别(在清单中)注册接收器并监听此事件,而不管Activity A的状态如何。

但是,正如Youssef已经说过的那样,从后台堆栈中杀死活动并不是正确的方法。您应该认真考虑更改导航逻辑。


-1
许多解决方案浮现在我的脑海中,但由于您没有提供有关应用程序的详细信息,因此我认为这个方法通常应该有效。
不要发出广播来杀死A活动,只需在B活动中按下“杀死A活动”按钮时执行以下代码即可。
        Intent intent = new Intent(getApplicationContext(),
                ActivityA.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
        intent.putExtra("EXIT", true);
        startActivity(intent);

在Activity 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;
    }

在清单文件中为Activity A设置"singleTop"启动模式。
<activity
    android:name=".ActivityA"
    ...
    android:launchMode="singleTop" />

这将产生以下后果:

  • 如果Activity A已经在运行,则会将其置于活动堆栈的前面并完成,从而将其从堆栈中删除。
  • 如果Activity A已被销毁但仍然存在于活动堆栈中(在按下返回按钮时启动),则它将被启动,置于前台并完成,从而将其从活动堆栈中删除。
  • 如果Activity A已经被销毁且不在活动堆栈中,并且您仍然按下“删除Activity A”按钮,则它将被启动,置于前台并完成。

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

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


你说得没错。但我提供了一个简单的场景。我想销毁多个活动,所以我使用了广播接收器。例如,在后台有10个活动(这只是示例,根据用户导航的情况,可能有任意数量的活动),我想销毁第2、4和5个活动。我该如何销毁它们?我无法将其重新排序到前面,因为我不知道后台堆栈活动。 - Smeet
如果您知道要销毁哪些活动,您可以使用此方法将它们全部销毁。只需设置所描述的意图并将其发送到所有活动即可,没有任何问题。 - A.J.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接