安卓:launchMode="singleTask" 存在 bug 吗? -> 活动栈未被保留

76

我的主Activity A在清单文件中设置了android:launchMode="singleTask"。现在,每当我从那里启动另一个Activity,例如B,并按下手机上的HOME BUTTON返回到主屏幕,然后再次通过按应用程序的按钮或长按HOME BUTTON 显示最近使用的应用程序来返回我的应用程序时,它不会保留我的Activity堆栈,而是直接返回到A,而不是预期的Activity B

这里有两种行为:

Expected: A > B > HOME > B
Actual: A > B > HOME > A (bad!)

我是否遗漏了某些设置或者这是一个bug?如果是后者,是否有一种解决方法可以在修复该错误之前使用?

FYI:这个问题已经在这里讨论过。但是,似乎还没有真正的解决方案。


2
只想补充一下,我也看到了相同的行为,根据我的文档解释,这似乎不正确。 - ADB
1
是的,这是一个bug,因为堆栈没有被保留,在移除singletask launchmode属性后,应用程序的行为不符合预期。 - user1063884
1
如果不是一个 bug,那么就是文档中的问题。请查看图 4 及其前面的段落:http://developer.android.com/guide/components/tasks-and-back-stack.html_ 然而,如果您启动了指定了 singleTask 启动模式的活动,则如果该活动的实例存在于后台任务中,则整个任务将被带到前台。此时,返回堆栈现在包括从前面带来的所有任务的所有活动,位于堆栈顶部。如果这不是一个 bug,那么在实际行为中 singleTask 和 singleInstance 之间有什么区别呢? - prostynick
你的活动B怎么样?它是标准的还是单任务的? - Sam YC
https://issuetracker.google.com/issues/36921210 - mhsmith
显示剩余2条评论
13个回答

47
这不是一个bug。当启动一个已存在的singleTask类型的Activity时,会销毁该Activity栈中它之上的所有其他Activity。
当你按下HOME键后再次启动这个Activity时,ActivityManager会调用一个Intent。
{act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]flag=FLAG_ACTIVITY_NEW_TASK|FLAG_ACTIVITY_RESET_IF_NEEDED cmp=A}
所以结果是A > B > HOME > A。
如果A的launchMode为"Standard",那么包含A的任务将回到前台并保持与之前相同的状态。
您可以创建一个"Standard"活动(例如C)作为启动器,并在C的onCreate方法中调用startActivity(A)。
或者,只需删除launchMode="singleTask"并在调用意图到A时设置FLAG_ACTIVITY_CLEAR_TOP|FLAG_ACTIVITY_SINGLE_TOP标志。

10
您好,但是在文档中没有提到“当已经存在一个' singleTask '活动正在启动时,堆栈中它上面的所有其他活动都将被销毁”的内容,而且为什么当启动模式为“标准”时行为会有所不同?如果任务已从最近列表中恢复,那么任务至少应该只是恢复,是吗?活动管理器应该设置FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY而不是FLAG_ACTIVITY_RESET_IF_NEEDED - Vasu
1
没有任何证据表明这不是一个错误。正如其他人已经指出的那样,文档表明这种行为是一个错误,并且有与此(错误)行为相关的问题[2373](http://code.google.com/p/android/issues/detail?id=2373)和[5277](http://code.google.com/p/android/issues/detail?id=5277)文件。 - sschuberth
@renchenyu:我遇到了同样的问题。但是我发现谷歌地图也使用了singleTask,并且它没有使用任何其他活动,比如你所说的C。你可以检查一下吗?http://imgbin.org/images/15515.gif - hguser
1
这真的很糟糕。我怎么能在不将新活动放在旧活动之上的情况下把我的堆栈返回顶部呢? - JohnyTex
1
实际上这是一个错误,或者是文档中的错误。 两者之一。 是否有人找到了更新的解决方案? - shoke

11

来自 http://developer.android.com/guide/topics/manifest/activity-element.html 关于 singleTask

系统会在新任务的根部创建Activity并将Intent路由到它。但是,如果该Activity的实例已经存在,则系统通过调用其 onNewIntent() 方法将Intent路由到现有实例,而不是创建一个新的实例。

这意味着当 action.MAIN 和 category.LAUNCHER 标志从 Launcher 定向到您的应用程序时,系统更喜欢将 Intent 路由到现有的 ActivityA,而不是创建一个新的任务并将一个新的 ActivityA 设置为根。 它宁愿关闭活动A所在现有任务的所有活动,然后调用它的 onNewIntent()。

如果您想捕获 singleTop 和 singleTask 的行为,请创建一个名为 SingleTaskActivity 的单独“委托”Activity,并使用 singleTask launchMode 来调用 singleTop activity 的 onCreate(),然后结束自身。 singleTop activity 仍将具有 MAIN/LAUNCHER intent-filters,以继续充当应用程序的主 Launcher activity,但其他活动希望调用此 singleTop activity 时,必须代替调用 SingleTaskActivity 以保留 singleTask 行为。传递给 singleTask activity 的Intent也应传递给 singleTop Activity,因此像以下内容一样的东西对我有用,因为我想同时拥有 singleTask 和 singleTop 启动模式。

<activity android:name=".activities.SingleTaskActivity"
              android:launchMode="singleTask"
              android:noHistory="true"/>

public class SingleTaskActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Intent intent = getIntent();
        intent.setClass(this, SingleTop.class);
        startActivity(intent);
    }
}

而您的singleTop活动将继续保持其singleTop启动模式。

    <activity
        android:name=".activities.SingleTopActivity"
        android:launchMode="singleTop"
        android:noHistory="true"/>

祝你好运。


3
很棒的解决方案。为了完整性,您还需要重定向发送到SingleTaskActivityonNewIntent()函数的意图。此外,如果您的应用程序使用深度链接,请确保在清单中声明您的深度链接意图过滤器在SingleTaskActivity上。 - M Dapp
1
非常有用,就像这篇文章一样:http://labs.comcast.com/deep-linking-through-a-multiple-activity-stack-on-android。当将意图转发到SingleTop活动时,我必须添加intent.addFlags(FLAG_ACTIVITY_CLEAR_TOP | FLAG_ACTIVITY_SINGLE_TOP);以确保目标活动确实在顶部并作为singleTop处理,否则,如果此时有另一个子活动在其上方,将创建一个新实例。 - Guy Moreillon
很棒的解决方案!我已经在两个活动中添加了 android:noHistory="true" - changhao cui

5

Stefan,你有没有找到答案?我为此编写了一个测试用例,并看到了相同的(令人困惑的)行为...我将在下面粘贴代码,以防有人发现明显的问题:

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example" >

  <uses-sdk android:minSdkVersion="3"/>

  <application android:icon="@drawable/icon" android:label="testSingleTask">

    <activity android:name=".ActivityA"
              android:launchMode="singleTask">
      <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
      </intent-filter>
    </activity>

    <activity android:name=".ActivityB"/>

  </application>
</manifest>

ActivityA.java:

public class ActivityA extends Activity implements View.OnClickListener
{
  @Override
  public void onCreate( Bundle savedInstanceState )
  {
    super.onCreate( savedInstanceState );
    setContentView( R.layout.main );
    View button = findViewById( R.id.tacos );
    button.setOnClickListener( this );
  }

  public void onClick( View view )
  {
    //Intent i = new Intent( this, ActivityB.class );
    Intent i = new Intent();
    i.setComponent( new ComponentName( this, ActivityB.class ) );
    startActivity( i );
  }
}

ActivityB.java:

public class ActivityB extends Activity
{
  @Override
  public void onCreate( Bundle savedInstanceState )
  {
    super.onCreate( savedInstanceState );
    setContentView( R.layout.layout_b );
  }
}

我尝试将minSdkVersion更改,但没有效果。根据文档所述,这似乎只是一个bug:
正如上面所提到的,"singleTask"或"singleInstance" activity永远不会有多个实例,因此该实例预计处理所有新的意图。 "singleInstance" activity始终位于堆栈的顶部(因为它是任务中唯一的activity),因此它始终处于处理意图的位置。但是,"singleTask" activity可能具有其他活动在其上方,也可能没有。如果有,它就无法处理意图,并且意图被丢弃。(即使意图被丢弃,它的到来也会导致任务进入前台,保持在那里。)

4
我认为这是您想要的行为:
singleTask因某些愚蠢的原因在主屏幕按下后会重置堆栈,我不理解这一点。解决方法是不使用singleTask,而是改用标准(standard)或者栈顶复用(singleTop)来代替启动器活动(launcher activity)(到目前为止,我只尝试过singleTop)。
因为应用程序对彼此具有关联性,所以启动这样一个活动:
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if(launchIntent!=null) {
    launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}

使用这些标记将导致您的活动堆栈以原样重新出现,而不会在旧活动上启动新活动(这是我之前遇到的主要问题)。标记是重点

FLAG_ACTIVITY_NEW_TASK API级别1中添加

如果设置了此标志,则此活动将成为此历史堆栈上的新任务的起点。从启动它的活动到下一个任务活动的任务定义了用户可以移动到的原子活动组。任务可以移到前台和后台;特定任务内的所有活动始终保持相同的顺序。有关任务的更多信息,请参阅任务和返回堆栈。

该标志通常由希望呈现“启动器”样式行为的活动使用:它们给用户提供了可以独立完全运行的不同事情列表。

当使用此标志时,如果对于您现在要启动的活动已经正在运行任务,则不会启动新活动;相反,当前任务将简单地以其上次状态带到屏幕前面。有关禁用此行为的标志,请参见FLAG_ACTIVITY_MULTIPLE_TASK。

调用者请求从正在启动的活动中获得结果时,无法使用此标志。

以及:

FLAG_ACTIVITY_RESET_TASK_IF_NEEDED API级别1中添加

如果设置,并且此活动正在启动新任务或将现有任务置于顶部,则会将其作为任务的前门启动。这将导致应用程序应用需要使该任务处于适当状态的任何关联(将活动移至其中或从其中移动),或者仅在需要时重置该任务的初始状态。

如果没有它们,启动的活动将只是推到旧堆栈的顶部或其他不良行为(在这种情况下)

我认为不能接收最新Intent的问题可以通过以下方式解决(脑海中):

@Override
public void onActivityReenter (int resultCode, Intent data) {
    onNewIntent(data);
}

试一试吧!


1
我已经使用这种方法多年了,除了FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标志之外,只有新任务标志。 这个方法非常好用,但是,使用这些标志/模式的组合似乎不会触发onNewIntent。 有什么办法可以获得所有相同的行为,但也可以通过传递数据到现有实例来触发onNewIntent吗? 除了这个问题之外,我认为这是解决这个长期存在的“错误”的最佳方案。 - eselk
请检查我的更新答案。我知道我解决了它。我认为是这样的。但是只是凭记忆而已。如果不是这样,请告诉我。那么我会在工作时再检查一下解决方案。 - JohnyTex
两年后我来到这里,发现这真的很到位,亲和力是我感觉最大的区别 - 它可以将活动关联到现有任务中(即使已经清除)。 - TommySM

2

我发现只有在启动器活动的启动模式设置为singleTask或singleInstance时才会出现此问题。

因此,我创建了一个新的启动器活动,其启动模式为standard或singleTop。并将此启动器活动调用我的旧主要活动,其启动模式为single task。

启动器活动(标准/无历史记录)- > 主要活动(singleTask)。

将闪屏设置为启动器活动。并在调用主要活动后立即杀死启动器活动。

public LauncherActivity extends Activity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent = new Intent(this, HomeActivity.class);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
    startActivity(intent);
    finish();
  }
}

<activity
  android:name=".LauncherActivity"
  android:noHistory="true"
  android:theme="@style/Theme.LauncherScreen">

    <intent-filter>
      <action android:name="android.intent.action.MAIN"/>

      <category android:name="android.intent.category.DEFAULT"/>
      <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>

</activity>

<!-- Launcher screen theme should be set for the case that app is restarting after the process is killed. -->
<activity
  android:name=".MainActivity"
  android:launchMode="singleTask"
  android:theme="@style/Theme.LauncherScreen"/>

优点:可以将MainActivity的启动模式设置为singleTask,以确保始终只有一个MainActivity。


1
如果A和B都属于同一个应用程序,请尝试删除。
android:launchMode="singleTask"

从您的活动和测试中返回,因为我认为默认行为是您所期望的。


1
如果我删除android:launchMode="singleTask",每当我调用一个Intent时,它就会创建A(我的主活动)的新实例,例如,我有一个菜单快捷方式返回到我的主活动(我的应用程序主屏幕)。 - znq
为了返回主屏幕,我会使用一个带有FLAG_ACTIVITY_CLEAR_TOP标志的Intent。这样,您可以回到主Activity的第一个实例并删除所有位于其上方的活动。 - Catalin Morosan
默认值为std:系统始终在目标任务中创建活动的新实例并将意图路由到它。 http://developer.android.com/guide/topics/manifest/activity-element.html#lmode - RzR
请查看我的答案,了解如何在移除singleTask时不创建新的activity。 - JohnyTex

0

在Android清单活动中添加以下内容,它将在视图顶部添加新任务并销毁早期的任务。 如下所示:android:launchMode="singleTop"

 <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:launchMode="singleTop"
        android:theme="@style/AppTheme.NoActionBar">
    </activity>

0

当使用singleTop作为启动模式时,请确保在启动下一个活动(使用startActivity(Intent)方法,比如B)时调用finish()(在当前活动A上)。这样当前活动就会被销毁。 A -> B -> 暂停应用并点击启动器图标,开始A 在A的oncreate方法中,您需要进行检查。

if(!TaskRoot()) {
    finish();
     return;
  }

这样,当启动应用程序时,我们会检查根任务,并且先前的根任务是B而不是A。因此,此检查将销毁活动A并带我们进入当前位于堆栈顶部的活动B。

希望对您有所帮助!


0
在子活动或B活动中。
@Override
public void onBackPressed() {
   
        Intent intent = new Intent(getApplicationContext(), Parent.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP);
        startActivity(intent);
        finish();
    

}

0

这就是我最终解决这个奇怪行为的方法。在 AndroidManifest 中,我添加了以下内容:

Application & Root activity
            android:launchMode="singleTop"
            android:alwaysRetainTaskState="true"
            android:taskAffinity="<name of package>"

Child Activity
            android:parentActivityName=".<name of parent activity>"
            android:taskAffinity="<name of package>"

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