任务栈

Android | launchMode启动模式详解

回顾一下Activity的任务栈:如上图所示显示了一个时间轴,3个Activity的启动顺序是1->2->3,按返回键之后3被销毁, 并且会从任务栈中弹出,2恢复。

而当调用startActivity()来启动一个页面时,可以通过一系列的设置来管理任务栈,既可以在manifest中设置,也可以通过Intent.FLAG动态设置,常用设置如下:

通过manifest文件的 < activity > 中配置

  • launchMode
  • taskAffinity
  • allowTaskReparenting
  • clearTaskOnLaunch
  • alwaysRetainTaskState
  • finishOnTaskLaunch

通过Intent.FLAG标志位设置

  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ACTIVITY_SINGLE_TOP
  • FLAG_ACTIVITY_REORDER_TO_FRONT
  • FLAG_ACTIVITY_CLEAR_TASK

注:如果在manifest中设置的某些行为与Intent.FLAG标志的行为不一致,那么以哪个为准呢

如activityA 启动了activityB,其中activityB 是在manifest清单中定义它与当前任务的关联方式,而activityA启动activityB时使用intent标志来与当前任务相关联。那么此时intent所定义标志位的优先级是高于manifest中的。

launchMode(静态)

在manifest文件中可以给 Activity 的 launchMode 属性指定五种启动模式(你没看错,现在有五种启动模式了哟,惊不惊喜,意不意外!),重温下对应的5种模式:

  • standard:系统默认的启动模式,该模式会在启动它的任务中创建 activity 的新实例,并向其传送 intent。
  • singleTop :如果系统启动Activity时,该Activity实例已经存在于task栈的栈顶,则会通过调用该实例的 onNewIntent() 方法向其传送 intent,而不是创建新的 activity 实例,即栈顶复用;如果要启动的Activity实例存在但不在栈顶,则会重新启动一个新的Activity(此时相当于standard模式)。singleTop启动模式常用于通知栏结果页中,可以增加复用性。
  • singleTask:当启动Acticity时,如果已存在该 Activity 实例,则系统会通过调用现有实例的 onNewIntent() 方法,而非创建新实例,向其传送 intent,同时它上面的所有其他 activity 都会被销毁。singleTask可以用于应用的首页,保证主页面只启动一次,其余情况走onNewIntent()。
  • singleInstance:行为与 “singleTask” 相同,只是系统不会将任何其他 activity 启动到包含该实例的任务中。该 activity 始终是其任务中的唯一 activity。由此 activity 启动的任何 activity 都会在单独的任务中打开。
  • singleInstancePerTask该 activity 只能作为task栈的根activity运行,因此在task栈中该activity 只有一个实例。singleInstancePerTask跟singleTask类似,不过singleInstancePerTask不需要设置taskAffinity也能创建一个新的task栈。而如果结合FLAG_ACTIVITY_MULTIPLE_TASK或FLAG_ACTIVITY_NEW_DOCUMENT的话,可以在不同task栈的多个实例中启动。 注:这个属性是API 12新增,也就是compileSdkVersion设置到31及以上的时候才可以设置,在API 12之前不起作用。

taskAffinity

taskAffinity 用于修改 activity 之间的“亲和性”,表示 activity属于哪个任务栈。默认情况下,同一应用中的所有 activity 彼此具有亲和性,即他们在同一任务栈中。taskAffinity 属性接受一个字符串值,该值必须与 <manifest> 元素中声明的默认软件包名称不同,因为系统会使用该名称来识别应用的默认任务亲和性。

使用示例:有三个activity,A打开B,B打开C,即:A->B->C。其中AB为默认设置,C在<activity>中的 taskAffinity 设置如下:

<activity
    android:name="com.launchmode.example.mode.launchMode.ActivityC"
    android:taskAffinity="unique.mode"/>

给C设置了一个不同于包名的字符串,执行了A->B->C操作之后,通过 adb shell dumpsys activity activities 命令查看Activity组件信息,如果觉得觉得生成的结果太多,可以通过grep过滤关键信息,如:adb shell dumpsys activity activities | grep -e "Hist" -e "Task" -e "taskAffinity"

Android | launchMode启动模式详解

可以看到虽然C的taskAffinity设置的不同值,但是A、B、C还是在同一任务栈中,所以只设置taskAffinity并不能改变亲和性。继续修改如下:

<activity
     android:name="com.launchmode.example.mode.launchMode.ActivityC"
     android:launchMode="singleTask"
     android:taskAffinity="unique.mode" />

此时在看堆栈信息:

Android | launchMode启动模式详解

嗯,C已经在一个单独的任务栈里了,也就是taskAffinity + singleTask的配置可以改变亲和性。除此之外,taskAffinity还可以和singleInstance、singleInstancePerTask组合改变亲和性;taskAffinity还可以配合在启动C的Intent中设置intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK改变亲和性。

注:Mac控制台中,可以通过command + k进行清屏操作,另外其他 adb shell 常用命令如下:

adb shell dumpsys activity :查看AMS所有信息
adb shell dumpsys activity activities:查看Activity组件信息
adb shell dumpsys activity services:查看Service组件信息
adb shell dumpsys activity providers:产看ContentProvider组件信息
adb shell dumpsys activity broadcasts:查看BraodcastReceiver信息
adb shell dumpsys activity intents:查看Intent信息
adb shell dumpsys activity processes:查看进程信息

taskAffinity + allowTaskReparenting

allowTaskReparenting为true时,activity 可以从其启动的任务栈移至与其有亲和性的任务栈中,前提是该任务栈出现在前台时。

示例:两个app应用,第一个应用中有A、B、C三个activity,打开顺序为A->B->C,其中C的设置如下:

<activity
      android:name="com.launchmode.example.mode.launchMode.ActivityC"
      android:allowTaskReparenting="true"
      android:taskAffinity="unique.mode" />

第二个应用中只有一个页面D,taskAffinity设置的值与C相同。运行第一个应用并执行A->B->C操作,然后按Home键,再打开第二个应用进入D页面,此时C因为设置了allowTaskReparenting=truetaskAffinity与D相同,所以会转移到包含D的任务栈里。这种在项目中暂时没用到过,有实践过的小伙伴可以讨论一下。

Intent常用FLAG标志位(动态)

启动 activity 时,可以通过在传递给 startActivity() 的 intent 中添加标志来修改 activity 与其任务的默认关联。常用标记位如下:

1、FLAG_ACTIVITY_NEW_TASK

系统会在新任务栈中启动 activity。单独设置没有作用,配合taskAffinity一起使用时(设置的taskAffinity需要保持不同)。如下所示在D跳转到B时,B在manifest中设置不同的taskAffinity,D启动B时设置了FLAG_ACTIVITY_NEW_TASK,结果也如预期一样B会在一个新的任务栈中启动:

Android | launchMode启动模式详解

除此之外,FLAG_ACTIVITY_NEW_TASK通常还可以和FLAG_ACTIVITY_CLEAR_TASK或FLAG_ACTIVITY_CLEAR_TOP一起使用。

a、FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TASK一起使用时: 如果有对应taskAffinity的task栈,会直接把栈清空,并创建新activity作为根成员放入。

b、FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TOP 一起使用时: 如果activity在对应taskAffinity的task栈里存在实例,会把它及以上的activity清空,并重新创建这个新activity。使用场景:在清除数据之后重新登录LoginActivity:

val intent = Intent(this, LoginActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)

2、FLAG_ACTIVITY_SINGLE_TOP

Intent.FLAG_ACTIVITY_SINGLE_TOP 与在launchMode中设置singleTop 的功能相同。这个很简单,就不再贴代码验证了。

3、FLAG_ACTIVITY_CLEAR_TOP

Intent.FLAG_ACTIVITY_CLEAR_TOP 销毁目标Activity和它之上的所有Activity,重新创建目标Activity,示例: A、B、C、D四个Activity,启动模式均为默认,依次启动,最后在D中再启动B,即启动顺序为:A->B->C->D->B。

首先看A->B->C->D之后的任务栈信息:

Android | launchMode启动模式详解
在D启动B时,分别尝试下面几种情况:

1、设置Intent的flags为:intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP,代码如下:

val intent = Intent(this, ActivityB::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP //这里
startActivity(intent)

执行之后的任务栈信息如下:

Android | launchMode启动模式详解

此外B还执行了自身的 onCreate() 方法,由此可以说明Intent.FLAG_ACTIVITY_CLEAR_TOP的效果:B、C、D被清除出栈,且B重新启动,重走生命周期。

2、重新设置flags为:intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP,点击D->B之后的堆栈信息如下:

Android | launchMode启动模式详解

B还执行了onNewIntent()、onResume()方法,所以Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP的效果: C,D出栈,B调用onNewIntent()方法而不是重新启动。

结论:通过实验可以得知Intent.FLAG_ACTIVITY_CLEAR_TOP + Intent.FLAG_ACTIVITY_SINGLE_TOP的组合相当于launchMode中的singleTask功能了。

4、FLAG_ACTIVITY_REORDER_TO_FRONT

Context.startActivity()时,如果目标activity之前启动过,那么设置这个标志会将启动的activity重新调到前面来,并调用它的onNewIntent()方法。

示例:页面启动顺序A->B->C->D,此时D调用startActivity(),并设置intent 的flags为FLAG_ACTIVITY_REORDER_TO_FRONT,跳转之后,任务栈变成了:A->C->D->B。

注意,如果Intent中还指定了FLAG_ACTIVITY_CLEAR_TOP,那么FLAG_ACTIVITY_REORDER_TO_FRONT标志将被忽略。

5、FLAG_ACTIVITY_CLEAR_TASK

这个标志会将对应任务栈中的activity清空,然后再启动目标activity。也就是说,该activiy将成为一个空任务的根activity,任何旧activity都将结束。通常这个标志位与FLAG_ACTIVITY_NEW_TASK结合使用。