一、探究 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 的行为及其应用场景

在 Android 中,我们有时需要对 Activity 的启动模式进行精细的控制,以满足特定的需求。为此,Android 提供了一些标志来帮助我们实现这些控制,其中 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 就是这样两个标志。

1.1 FLAG_ACTIVITY_CLEAR_TOP

首先来看 FLAG_ACTIVITY_CLEAR_TOP。当我们为一个新启动的 Activity 设置了这个标志,系统会检查当前任务栈中是否已经存在相同的 Activity 实例。

  • 如果存在,那么这个 Activity 之上的所有 Activity 都会被销毁,使得这个 Activity 实例成为栈顶。
  • 如果不存在,系统会正常启动新的 Activity。

这个标志通常用于需要返回到任务栈中某个 Activity 的场景,如注销登录后返回到主页等。但是,如果我们没有与 FLAG_ACTIVITY_CLEAR_TOP 同时使用 FLAG_ACTIVITY_SINGLE_TOP,系统仍然会重新创建目标 Activity 实例。另外,如果任务栈中没有目标 Activity,这个标志将不起作用。

1.2 FLAG_ACTIVITY_NEW_TASK

1.2.1 任务和任务栈

在 Android 中,任务(Task)和任务栈(Task Stack)是用来管理应用的 Activity 生命周期和导航的重要概念。

  1. 任务(Task):任务是一个用户与应用进行交互的会话。它是由用户从启动应用开始,到用户离开应用结束的一系列操作过程。一个任务对应于一个应用程序,但一个应用程序可以有多个任务。任务中可以包含一个或多个 Activity,这些 Activity 按照它们打开的顺序排列,形成了任务栈。

  2. 任务栈(Task Stack):任务栈是用来管理一个任务中所有 Activity 的堆栈结构。新的 Activity 被放置(push)到栈的顶部,用户看到的总是位于栈顶的 Activity。当用户按下返回键时,当前的 Activity 会从栈顶被移除(pop),并销毁,之前的 Activity 会重新显示。任务栈遵循“后进先出”(LIFO)的原则。

这两个概念对于理解 Android 的 Activity 启动模式,以及如何控制 Activity 的导航和生命周期等都非常重要。

1.2.2 FLAG_ACTIVITY_NEW_TASK 的使用和注意事项

接下来,我们来看一看 FLAG_ACTIVITY_NEW_TASK。当我们为一个 Activity 设置了这个标志,这个 Activity 会成为新任务的根,也就是新任务的第一个 Activity。如果没有设置这个标志,Activity 会被插入到当前任务栈。

这个标志通常用于从非 Activity(如 Service、BroadcastReceiver)中启动 Activity,或者需要在新的任务栈中打开 Activity 的场景。然而,使用这个标志时需要注意,如果已经存在相同的任务,那么这个标志会使得 Activity 请求被路由到已经存在的任务中,而不会创建新的任务。此外,如果没有正确理解任务和任务栈的概念,可能会导致 Activity 启动的行为与预期不符。因此,在使用 FLAG_ACTIVITY_NEW_TASK 时,我们需要确保充分理解了它的行为和可能的副作用。

1.3 代码例子

下面是两个使用这些标志的代码例子:

Intent intent = new Intent(this, TargetActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);

在这个例子中,我们创建了一个指向 TargetActivityIntent,并为它添加了 FLAG_ACTIVITY_CLEAR_TOP 标志。当我们启动这个 Intent 时,系统会检查当前任务栈中是否已经存在 TargetActivity 的实例。如果存在,那么这个实例之上的所有 Activity 都会被销毁,使得 TargetActivity 实例成为栈顶。

Intent intent = new Intent(this, TargetActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

在这个例子中,我们创建了一个指向 TargetActivityIntent,并为它添加了 FLAG_ACTIVITY_NEW_TASK 标志。当我们启动这个 Intent 时,TargetActivity 会成为新任务的根,也就是新任务的第一个 Activity

二、深入探究:小米手机离线推送跳转问题实例分析

本节将阐述在小米手机上点击离线推送,跳转到消息页面时,无法弹出手势密码页面的问题定位过程。

首先描述一下出问题的表现:

设置了手势密码,kill掉app,收到消息离线推送弹窗,点击弹窗拉起app,没有弹出手势密码页面,而是直接进入消息页面。

2.1 问题流程分析

  1. 点击小米离线推送弹窗,会自动调起小米推送sdk的页面com.xiaomi.mipush.sdk.NotificationClickedActivity(下文简化为NotificationClickedActivity),同时弹窗也会调起消息页面(业务逻辑)。
  2. NotificationClickedActivityonResume 方法触发ActivityLifecycle弹出手势密码页面。
  3. 手势密码页面的弹出逻辑会延迟300ms执行,目的是为了先让前一个业务页面弹出,再盖上手势密码页面。
  4. 但紧接着 NotificationClickedActivity 就自动 finish 了(特定小米机型必现),手势密码页面被连带着finish
  • 这个场景描述的是系统行为。
  • NotificationClickedActivity 页面后续在某个时机会自动finish(猜测是小米推送sdk控制)。
  • NotificationClickedActivity 页面finish时,跟 NotificationClickedActivity页面在同一个任务栈的页面,都会跟着finish(多次测试观察验证如此,只有特定小米机器上会出现)。
  1. 此时只剩一个消息界面。

onActivityResumed 方法中,我们调用了 upAppLock 方法。upAppLock的作用是重新把不在最顶部的手势密码页面弹出到任务栈的最上面。

onActivityResumedActivityLifecycle 中的方法,这个 ActivityLifecycle 单例是在App进程启动时,通过 registerActivityLifecycleCallback 注册的。NotificationClickedActivity、手势密码页、消息界面的 onResume 被调用前,都会先进入 ActivityLifecycleonActivityResumed函数。

public final class ActivityLifecycle implements Application.ActivityLifecycleCallbacks, ITPFEventListener {
//...

@Override
public void onActivityResumed(Activity activity) {
    // ... 省略部分代码 ...
    if (ActivityLifecycleState.INSTANCE.isAndroid10Background()) {
	// 设置前台状态
        ActivityLifecycleState.INSTANCE.setAndroid10Background(false);
        // 处理进入前台任务
        handleGoForeground(activity);
	}
     
	upAppLock(activity);
	// ... 省略部分代码 ...
}

private void handleGoForeground(Activity activity) {
	// ... 省略部分代码 ...
	ThreadUtils.runOnMainThread(new Runnable() {
		@Override
        public void run() {
        	// 弹出手势密码页面
            IAccount.get().onActivityStarted(activity);
        }
	}, 300);
	// ... 省略部分代码 ...
}

private void upAppLock(Activity activity) {
    // ... 省略部分代码 ...

    if (appLockActivity != null && (!appLockActivity.isFinishing() || !appLockActivity.isDestroyed())) {
        // 防止出现连续两个手势密码页面
        if (!activity.getClass().getName().equals(appLockName) &&  
        	// 防止把最上面的手势密码页面 finish
        	topActivity != null && !topActivity.getClass().getName().equals(appLockName)) {
        	// 结束任务栈内的重复手势密码页面,这个手势密码页面当前已经被压在底下看不见
            if (appLockActivity.getTaskId() == topActivity.getTaskId()) {
                appLockActivity.finish();
            }
            // 弹出手势密码页面
            IAccount.get().onActivityStarted(activity);
        }
    }
}

// ...
}

2.2 解决问题过程

在这个过程中,我们将关注两个问题:FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK

2.2.1 问题一:FLAG_ACTIVITY_CLEAR_TOP

首先,我们猜测消息页面使用的 FLAG_ACTIVITY_CLEAR_TOP,会导致手势密码页面消失。所以第一个改动点是启动消息页面时,不使用FLAG_ACTIVITY_CLEAR_TOP

2.2.2 问题二:FLAG_ACTIVITY_NEW_TASK

关于 FLAG_ACTIVITY_NEW_TASK,我们可以观察以下四种情况:

  1. 消息页面有 FLAG_ACTIVITY_NEW_TASK,手势密码页面没有 FLAG_ACTIVITY_NEW_TASK:没有弹出手势密码页面,直接进入消息页面。

  2. 消息页面和手势密码页面都没有 FLAG_ACTIVITY_NEW_TASK:手势密码页面显示,但没有消息页面,Launcher 启动的是主页面。原因是消息页面也被 NotificationClickedActivity 一起 finish 了(特定机型必现)。

  3. 消息页面和手势密码页面都有 FLAG_ACTIVITY_NEW_TASK,弹出手势密码页面有300ms延迟:

  • 正常弹出手势密码页面的情况是,先弹出了手势密码页面,消息页面在 300ms 后创建,upAppLock 起作用。
  • 无法弹出手势密码页面的情况是,消息页面在 300ms 内创建,再弹出手势密码页面,upAppLock 不起作用。
  1. 消息页面和手势密码页面都有 FLAG_ACTIVITY_NEW_TASK,没有延迟弹出手势密码页面:成功弹出手势密码页面,upAppLock 起作用。

最后,我们重点分析第4种情况表现正常的原因:

  1. 因为消息页面和手势密码页面都有 FLAG_ACTIVITY_NEW_TASK,所以两个页面都和NotificationClickedActivity不在同一个任务栈内,不会被连带finish

  2. 因为手势密码页面不延迟弹出,所以页面的弹出时序变成了:先弹出手势密码页面,再弹出消息页面,此时任务栈中,手势密码页面在消息页面的下面。

  3. 消息页面的onActivityResumed触发了upAppLock,重新把手势密码页面弹出到任务栈的最上面。此时的任务栈符合产品预期逻辑。

页面名称修改前修改后修改原因分析
手势密码页面没有FLAG_ACTIVITY_NEW_TASK,没有FLAG_ACTIVITY_CLEAR_TOP有FLAG_ACTIVITY_NEW_TASK,没有FLAG_ACTIVITY_CLEAR_TOP因为手势密码页面带上了FLAG_ACTIVITY_NEW_TASK,所以手势密码页面跟小米推送sdk的NotificationClickedActivity页面不在同一个任务栈,不会被连带finish。
消息页面有FLAG_ACTIVITY_NEW_TASK,有FLAG_ACTIVITY_CLEAR_TOP有FLAG_ACTIVITY_NEW_TASK,没有FLAG_ACTIVITY_CLEAR_TOP因为消息页面删掉了FLAG_ACTIVITY_CLEAR_TOP,所以消息页面启动时,不会清除掉它上面的手势密码页面。

通过以上分析,我们可以得出结论:为了正确弹出手势密码页面,我们需要注意 FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 的使用,以及如何正确处理任务和任务栈。

三、总结

总的来说,FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASK 可以用来控制 Activity 的启动模式和任务栈的行为。然而,使用它们时需要谨慎,确保理解了它们的行为和可能的副作用。在实际开发中,我们可能会遇到一些复杂的场景,如小米手机上的离线推送问题。这时,我们需要深入理解和分析问题,找出问题的根源,才能找到解决问题的方法。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐