当活动以不同的Intent启动时,如何防止活动的多个实例


121

使用Google Play商店应用(以前称为Android Market)上的“打开”按钮启动应用时,我遇到了一个错误。从Play商店启动它的方式似乎与Intent从手机的应用程序图标菜单中启动方式不同。这导致启动同一活动的多个副本,而这些副本彼此冲突。

例如,如果我的应用程序由“活动” ABC组成,则此问题可能会导致一堆ABCA。

我尝试android:launchMode="singleTask"在所有Activity上使用来解决此问题,但是每当我按下HOME按钮时,都会清除将Activity堆栈清除为root的不良副作用。

预期的行为是: ABC-> HOME->恢复应用程序时,我需要:ABC-> HOME-> ABC

有没有一种好的方法可以防止启动多个相同类型的活动,而无需在使用HOME按钮时重置为根活动?


Answers:


187

将此添加到onCreate中,您应该会很好:

// Possible work around for market launches. See https://issuetracker.google.com/issues/36907463
// for more details. Essentially, the market launches the main activity on top of other activities.
// we never want this to happen. Instead, we check if we are the root and if not, we finish.
if (!isTaskRoot()) {
    final Intent intent = getIntent();
    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(intent.getAction())) {
        Log.w(LOG_TAG, "Main Activity is not the root.  Finishing Main Activity instead of launching.");
        finish();
        return;       
    }
}

25
多年来,我一直在尝试解决此错误,这是有效的解决方案,非常感谢!我还需要注意的是,这不仅是Android Market中的问题,而且还会通过将应用程序上载到服务器或通过电子邮件发送到您的手机来侧面加载应用程序,从而导致此问题。所有这些东西都使用Package Installer安装了该应用程序,我相信该bug仍在其中。此外,以防万一,您只需要将此代码添加到您的根活动是什么的onCreate方法中即可。
ubzack 2011年

2
我发现这很奇怪,发生在已部署到设备的签名应用程序中,而不是从Eclipse部署的调试版本中。使得调试非常困难!
马特·康诺利

6
从Eclipse的部署,只要你开始它也通过Eclipse(或的IntelliJ或其他IDE)调试版本发生。它与应用程序在设备上的安装方式无关。问题是由于启动应用程序的方式引起的。
大卫·瓦瑟

2
有谁知道此代码是否可以确保将应用程序的现有实例带到前台?还是只是调用finish(); 并让用户没有任何迹象表明发生了什么?
卡洛斯·P

5
@CarlosP如果正在创建的活动不是任务的根活动,则(根据定义)在其下方必须至少有一个其他活动。如果调用此活动,finish()则用户将看到下面的活动。因此,您可以放心地假设该应用程序的现有实例将被带到前台。如果不是这种情况,您将在单独的任务中拥有该应用程序的多个实例,并且正在创建的活动将成为其任务的根。
David Wasser

27

我将解释它为什么失败,以及如何以编程方式重现此错误,以便可以将其合并到测试套件中:

  1. 当您通过Eclipse或Market App启动应用程序时,它会以意图标记启动:FLAG_ACTIVITY_NEW_TASK。

  2. 通过启动器(家庭)启动时,它使用标志:FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_BROUGHT_TO_FRONT | FLAG_ACTIVITY_RESET_TASK_IF_NEEDED,并使用操作“ MAIN ”和类别“ LAUNCHER ”。

如果您想在测试案例中重现此内容,请使用以下步骤:

adb shell am start -f 0x10000000 -n com.testfairy.tests.regression.taskroot/.MainActivity 

然后做任何必要的事情去参加其他活动。出于我的目的,我只是放置了一个按钮来启动另一个活动。然后,使用以下命令返回启动器(主页):

adb shell am start -W -c android.intent.category.HOME -a android.intent.action.MAIN

并模拟通过启动器启动它:

adb shell am start -a "android.intent.action.MAIN" -c "android.intent.category.LAUNCHER" -f 0x10600000 -n com.testfairy.tests.regression.taskroot/.MainActivity

如果尚未合并isTaskRoot()解决方法,则将重现该问题。我们在自动测试中使用它来确保不再发生此错误。

希望这可以帮助!


8

您是否尝试过singleTop启动模式?

这是来自http://developer.android.com/guide/topics/manifest/activity-element.html的一些描述:

...也可以创建“ singleTop”活动的新实例来处理新的意图。但是,如果目标任务在其堆栈的顶部已经具有该活动的现有实例,则该实例将接收到新的意图(在onNewIntent()调用中);没有创建新实例。在其他情况下(例如,如果“ singleTop”活动的现有实例在目标任务中,但不在堆栈的顶部,或者它在堆栈的顶部,但不在目标任务的顶部)新实例将被创建并压入堆栈。


2
我想到了,但是如果活动不在堆栈的顶部怎么办?例如,似乎singleTop将阻止AA,但不会阻止ABA。
bsberkeley,2010年

您可以通过在Activity中使用singleTop和finish方法来实现所需的功能吗?
埃里克·莱文

我不知道它能否完全实现我想要的。示例:如果我在弹出A和B之后进入活动C,那么将启动一个新的活动A,而我会得到类似CA的信息,不是吗?
bsberkeley,2010年

如果不了解这些活动的作用,就很难回答这个问题。您能否提供有关您的申请和活动的更多详细信息?我想知道“主页”按钮的功能和您希望它如何操作之间是否不匹配。主页按钮不会退出活动,而是将其“后台”,以便用户可以切换到其他活动。后退按钮是退出/完成和活动的对象。打破这种范式可能会使用户感到困惑/沮丧。
埃里克·莱文

我为此线程添加了另一个答案,以便您可以看到清单的副本。
bsberkeley,2010年

4

也许是这个问题?还是其他形式的相同错误?


另请参阅code.google.com/p/android/issues/detail?id=26658,这表明它是由Eclipse以外的其他原因引起的。
克里斯托弗·约翰逊

1
因此,我应该复制粘贴可能会过时的问题描述?哪一部分?如果链接发生更改,是否应保留必要的部分?保持答案最新是我的责任吗?人们应该认为,只有解决了问题,链接才变得无效。毕竟,这不是指向博客的链接。
DuneCat 2014年

2

我认为已接受的答案(Duane Homick)有未解决的案例:

您有不同的附加功能(因此应用程序重复):

  • 当您从电子市场或主屏幕图标(由电子市场自动放置)启动应用程序时
  • 当您通过启动器启动应用程序或手动创建的主屏幕图标时

这是一个解决方案(通知的SDK_INT> = 11),我相信可以处理这些情况和statusbar通知。

清单

    <activity
        android:name="com.acme.activity.LauncherActivity"
        android:noHistory="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
            <category android:name="android.intent.category.DEFAULT" />
        </intent-filter>
    </activity>
    <service android:name="com.acme.service.LauncherIntentService" />

启动器活动

public static Integer lastLaunchTag = null;
@Override
public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    mInflater = LayoutInflater.from(this);
    View mainView = null;
    mainView = mInflater.inflate(R.layout.act_launcher, null); // empty layout
    setContentView(mainView);

    if (getIntent() == null || getIntent().getExtras() == null || !getIntent().getExtras().containsKey(Consts.EXTRA_ACTIVITY_LAUNCH_FIX)) {
        Intent serviceIntent = new Intent(this, LauncherIntentService.class);
        if (getIntent() != null && getIntent().getExtras() != null) {
            serviceIntent.putExtras(getIntent().getExtras());
        }
        lastLaunchTag = (int) (Math.random()*100000);
        serviceIntent.putExtra(Consts.EXTRA_ACTIVITY_LAUNCH_TAG, Integer.valueOf(lastLaunchTag));
        startService(serviceIntent);

        finish();
        return;
    }

    Intent intent = new Intent(this, SigninActivity.class);
    if (getIntent() != null && getIntent().getExtras() != null) {
        intent.putExtras(getIntent().getExtras());
    }
    startActivity(intent);
}

服务内容

@Override
protected void onHandleIntent(final Intent intent) {
    Bundle extras = intent.getExtras();
    Integer lastLaunchTag = extras.getInt(Consts.EXTRA_ACTIVITY_LAUNCH_TAG);

    try {
        Long timeStart = new Date().getTime(); 
        while (new Date().getTime() - timeStart < 100) {
            Thread.currentThread().sleep(25);
            if (!lastLaunchTag.equals(LauncherActivity.lastLaunchTag)) {
                break;
            }
        }
        Thread.currentThread().sleep(25);
        launch(intent);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

private void launch(Intent intent) {
    Intent launchIintent = new Intent(LauncherIntentService.this, LauncherActivity.class);
    launchIintent.addCategory(Intent.CATEGORY_LAUNCHER);
    launchIintent.setAction(Intent.ACTION_MAIN); 
    launchIintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    launchIintent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 
    if (intent != null && intent.getExtras() != null) {
        launchIintent.putExtras(intent.getExtras());
    }
    launchIintent.putExtra(Consts.EXTRA_ACTIVITY_LAUNCH_FIX, true);
    startActivity(launchIintent);
}

通知事项

ComponentName actCN = new ComponentName(context.getPackageName(), LauncherActivity.class.getName()); 
Intent contentIntent = new Intent(context, LauncherActivity.class);
contentIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);    
if (Build.VERSION.SDK_INT >= 11) { 
    contentIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); // if you need to recreate activity stack
}
contentIntent.addCategory(Intent.CATEGORY_LAUNCHER);
contentIntent.setAction(Intent.ACTION_MAIN);
contentIntent.putExtra(Consts.EXTRA_CUSTOM_DATA, true);

2

我意识到这个问题与Xamarin Android没有任何关系,但是我想发布一些东西,因为我在其他地方都没有看到它。

为了在Xamarin Android中解决此问题,我使用了@DuaneHomick中的代码并将其添加到中MainActivity.OnCreate()。Xamarin与Xamarin的不同之处在于,它必须遵循Xamarin.Forms.Forms.Init(this, bundle);and LoadApplication(new App());。所以我OnCreate()看起来像:

protected override void OnCreate(Bundle bundle) {
    base.OnCreate(bundle);

    Xamarin.Forms.Forms.Init(this, bundle);
    LoadApplication(new App());

    if(!IsTaskRoot) {
        Intent intent = Intent;
        string action = intent.Action;
        if(intent.HasCategory(Intent.CategoryLauncher) && action != null && action.Equals(Intent.ActionMain, System.StringComparison.OrdinalIgnoreCase)) {
            System.Console.WriteLine("\nIn APP.Droid.MainActivity.OnCreate() - Finishing Activity and returning since a second MainActivity has been created.\n");
            Finish();
            return; //Not necessary if there is no code below
        }
    }
}

*编辑:自Android 6.0起,上述解决方案在某些情况下还不够。我现在还设置LaunchModeSingleTask,这似乎使事情再次正常运行。不幸的是,不确定这可能对其他事物产生什么影响。


0

我遇到了同样的问题,并使用以下解决方案对其进行了修复。

在您的主要活动中,将此代码添加到onCreate方法顶部:

ActivityManager manager = (ActivityManager) this.getSystemService( ACTIVITY_SERVICE );
List<RunningTaskInfo> tasks =  manager.getRunningTasks(Integer.MAX_VALUE);

for (RunningTaskInfo taskInfo : tasks) {
    if(taskInfo.baseActivity.getClassName().equals(<your package name>.<your class name>) && (taskInfo.numActivities > 1)){
        finish();
    }
}

不要忘记在清单中添加此权限。

< uses-permission android:name="android.permission.GET_TASKS" />

希望对您有帮助。


0

我也有这个问题

  1. 不要调用finish(); 在home活​​动中,它会无限运行-当ActivityManager完成时会调用home活​​动。
  2. 通常,当配置更改时(即旋转屏幕,更改语言,电话服务更改即mcc mnc等),将重新创建活动-如果家庭活动正在运行,则它将再次调用A。为此需要添加到清单中 android:configChanges="mcc|mnc"-如果您已连接到蜂窝网络,请参阅http://developer.android.com/guide/topics/manifest/activity-element.html#config引导系统或推开时进行的配置。

0

尝试以下解决方案:
创建Application类并在那里定义:

public static boolean IS_APP_RUNNING = false;

然后,在你的第一个(启动)在活动onCreatesetContentView(...)补充一点:

if (Controller.IS_APP_RUNNING == false)
{
  Controller.IS_APP_RUNNING = true;
  setContentView(...)
  //Your onCreate code...
}
else
  finish();

PS Controller是我的Application课。


您应该使用原始布尔值,该布尔值使检查不必要的空值成为可能。
WonderCsabo 2015年

这并不总是有效。您将永远无法启动您的应用程序,退出您的应用程序然后再次快速启动您的应用程序。一旦没有活动,Android不一定会终止托管OS进程。在这种情况下,当您再次启动应用程序时,变量IS_APP_RUNNING将为,true并且您的应用程序将立即退出。用户可能不会觉得有趣。
David Wasser

-2

尝试使用 将亲缘关系设置为allowtaskreparenting的SingleInstance启动模式,此操作将始终在新任务中创建活动,但也允许其重新父母。检查dis:亲和力属性


2
可能无法使用,因为根据文档,“重新父母限定为“标准”和“ singleTop”模式。” 因为“使用“ singleTask”或“ singleInstance”启动模式进行的活动只能位于任务的根源”
bsberkeley 2010年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.