Activity的启动模式LaunchMode

Posted by 阿呆 on 2019-01-28

前言

在Android开发中,打造良好的用户体验是非常重要的,而在用户体验中,界面的引导和跳转是值得深入研究的重要内容,在开发中,与界面跳转联系比较紧密的概念是 Task(任务) 和 Back Stack (回退栈), Activity 的启动模式会影响到 Task 和 Back Stack 的状态,进而影响用户体验,除了启动模式之外,Intent类中定义的一些标志(以FLAG_ACTIVITY开头)也会影响Task 和 Back STack 的状态

Task是一个存在于 Framework 层的概念,容易与它混淆的有 Application与 Process。在开始介绍 Activity的启动模式之前,首先对这些概念做一个简单的说明和区分

概念

Application: 一组相互关联的组件和资源的集合

Task: 一组相互关联的Activity的集合 (只针对Activity)

BackStack : 保存Task的数据结构

standard mode

Task是可以跨应用的,这正是 Task 存在的一个重要原因。有的Activity虽然不在同一个App中,但是为了保持用户操作的连贯性,把他们放在一个任务中

Process:进程,操作系统内核中的一个概念,表示接受内核调度的执行单位。应用程序中的不同组件默认运行在同一个进程中,但是也可以通过指定 'android:process’属性指定组件的运行的进程的名字

1
2
3
<activity android:name=".MyActivity" android:label="@string/app_nam"
android:process=":remote">
</activity>

Activity四种启动模式详解

Activity有四种启动模式:

  • standard
  • singleTop
  • singleTask
  • singleInsatnce
1
android:launchMode = "singleTask"

statndard

在这种模式下启动的Activity可以被多次实例化,即便在同一任务中可以存在多个Activity的实例,每个实例都会处理一个Intent对象

singleTop

如果一个以 singleTop 模式启动的activity的实例已经存在于任务栈的栈顶,那么再次启动这个Activity时,不会创建新的实例,而是重用栈顶的那个实例,并且会调用该实例的 onNewIntent()方法将intent对象传递到这个实例中。

常用场景

1.通知栏点开进入页面,为了避免创建多个通知页面,往往采用singleTop的启动模式

singleTask

如果一个Activity的启动模式为 singleTask,那么系统总会在一个新任务的最底部(root)启动这个Activity,并且被这个Activity启动的其它Activity会和该Activity同时存在于这个新任务中,如果系统中存在这样的一个Activity则会重用这个实例(onNewIntent),即,这样一个activity在系统中只会存在一个实例

注意:

官方文档中的这种说法并不准确,启动模式为singleTask的Activity并不总会开启一个新的任务

应用场景:大多数App的主页。对于大部分应用,当我们在主界面点击回退按钮的时候都是退出应用,那么当我们第一次进入主界面之后,主界面位于栈底,以后不管我们打开了多少个Activity,只要我们再次回到主界面,都应该使用将主界面Activity上所有的Activity移除的方式来让主界面Activity处于栈顶,而不是往栈顶新加一个主界面Activity的实例,通过这种方式能够保证退出应用时所有的Activity都能报销毁。

singleInstance

总是在新任务中开启,并且这个新任务中有且只要这一个实例,也就是说被该实例启动的其它Activity会自动运行于另一个任务中,当再次启动该Actvity的实例时,会重用已存在的任务和实例(onNewIntent).和singleTask相同,同一时刻在系统中只会存在一个这样的Activity实例

singleInstance实际上是一种加强的singleTask模式,具有此启动模式的Activity只能位于一个单独的任务栈中。它适合需要与程序分离开的页面,经常被用于系统中的应用,比如 Launch、锁屏键、电话拨打、闹铃提醒等,整个系统中仅有一个,所以我们在我们呢的应用中一般不会用到,了解就可以。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题。

比如下图:

A--------->B (singleInsatnce)————–>C

回退: C—>A

C回退的话会直接到A,B就好像不存在一样,B如果为singleTask是不会这样的,我们来分析一下出现这种情况的原因

B打开C,C运行在什么任务栈?由后面的分析我们可以知道,如果不指定C的taskAffinity的话,C默认运行在A的任务栈中,这样的话,说明AC的任务栈处于前台,所以C退出自然会回到A,因为AMS调度Activity是以栈为单位的。足以可见 signleInstance 的特殊性。不过如果我们指定C的taskAffinity为与A不一样的,则回退顺序正常。注意,由B启动的Activitity,系统默认会在Intent中加入 FLAG_ACTIVITY_NEW_TASK,相当于指定它为singleTask启动了,但是如果你不指定taskAffinity,它会默认为A的taskAffinity,则不会创建新的栈。

A———->B(singleInstance)————–>C(differ taskAffinity from A)

回退: C–>B–>A

这里实际上相当于有了三个任务栈,不存在第一种情况那样的冲突问题

要关注一个问题就是,被启动模式为singleInstance的Activity启动的Activity运行在什么任务栈 中?

验证singleTask模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.jg.zhang.androidtasktest"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk android:minSdkVersion="10" android:targetSdkVersion="17" />

<application android:icon="@drawable/ic_launcher"
android:label="@string/app_name">
<activity android:label="@string/app_name"
android:name="com.jg.zhang.androidtasktest.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<!--android:taskAffinity="com.jg.zhang.androidtasktest.second"
android:alwaysRetainTaskState="true"
android:allowBackup="true" -->

<activity android:name="com.jg.zhang.androidtasktest.SecondActivity"
android:launchMode="singleTask">
<intent-filter >
<action android:name="com.jg.zhang.androidtasktest.SecondActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>

<activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"
android:label="@string/app_name" >
</activity>
</application>

</manifest>

情况1

我们可以在Activity中运行以下代码得到当前Activity的任务ID

1
int taskId = getTaskId();

那么我们运行完之后,会打印如下 log

情况1

SecondActivit与MainActivity仍然启动在同一个任务中的

其实,把启动模式设置为singleTask,framework在启动该Actiivty时只会把它标识为 可在一个新任务中启动,至于是否在一个新任务中启动,还受到其它条件的限制

接下来,我们在SecondActivity中增加一个 taskAffinity 属性

1
android:taskAffinity="com.jg.zhang"

情况立马变得不一样了,SecondActivity与ThirdActivity都运行在一个新的Task里面

这里,便引出了manifest文件中的一个重要属性,taskAffinity,在官方文档中,可以得到taskAffinity的以下信息:

  1. taskAffinity 表示当前 activity 具有亲和力的一个任务(翻译不是很准确,原文 The task that the activity has an affinity for .)大致理解为当前 activity 所在的任务

  2. 一个任务的 affinity 取决于这个任务的根 activity(root activity)的 taskAffinity,具有相同 affinity 的activity属于同一个任务

  3. 默认的情况下,一个应用中所有的 activity 具有相同的 taskAffinity,即应用程序的包名。但是我们可以通过设置不同的taskAffinity属性给应用中的activity分组,也可以把不同应用中的 activity 的 taskAffinity 设置成相同的值

  4. 这个属性决定两件事

    4.1 当 Activity 被 re-parent 时,它可以被 re-parent 哪个任务中

    4.2 当Activity 以 FLAG_ACTIVITY_NEW_TASK 标志启动时,它会被启动到哪个任务中

  5. 为一个 Activity 的taskAffinity 设置成一个空字符串,标明它不属于任何一个 task

可以简单地认为,singleTask 和 FLAG_ACTIVITY_NEW_TASK 作用相同,当启动模式为 singleTask时,framework会将它的启动标志设置为 FLAG_ACTIVITY_NEW_TASK

回到上面

1
2
3
4
<SecondActivity
android:launchMode="singleTask"
android:taskAffinity="com.jg.zhang">
</SecondActivity>

当我们启动一个 singleTask 模式的 Activity (SecondActivity) 时,framework 会首先检查是否已经存在一个 affinity 为 “com.jg.zhang” 的任务。

–>

  • 存在任务 : 检查是否存在 SecondActivity 的实例
    • 存在:重用这个SeondActivity的实例,并清除位于 SecondActivity上面的所有的Activity,显示SecondActivity,并调用SecondActivity的onNewIntent
    • 不存在:会在这个任务中创建SecondActivity的实例,并调用 onCreate 方法
  • 不存在任务: 创建一个新的名为 "com.jg.zhang"的任务,并且将 SecondActivity 启东道这个新的任务中去

其实 framework 中对任务和 activity 的调度是很复杂的,尤其是把启动模式设置为 singleTask 或者以 FLAG_ACTIVITY_NEW_TASK 标志启动时,所以,在使用 singleTask 和 FLAG_ACTIVITY_NEW_TASK时,要仔细测试应用程序,这也是官方文档的建议。

实例验证不同App中的Activity的taskAffinity设成相同

1
2
3
4
5
新建AppNew ,指定seconde Acivity
<activity
android:launchMode="singleTask"
android:taskAffinity = "com.jg.zhang">
</activity>

虽然两个activity不在一个应用中,却会在运行时分配到同一任务中

注意:

仅设置 taskAffinity 而不设置 singleTask 是没用的

仅设置singleTask 而不设置taskAffinity,则默认为当前任务的taskAffinity

task的形式需要注意,写成分隔的形式,直接一个字符串是不行的

任务可以跨进程,也可以跨应用实例验证

实例验证singleTask的另一个意义:在同一个任务中具有唯一性

上面已经验证了,singleTask不一定会开启新的任务,还有很多的限制条件,由上面的介绍可知,这个限制条件为:

是否已经存在了一个由它的taskAffinity属性指定的任务

注意,是singleTask 而不是 singleInTask ,我想这句话能帮助你理解哈哈

验证:

我们可以增加一个 FourthActivity,并且 MainActivity 、SecondActivity 、ThirdActivity 和 FourthActivity这四个 activity 都不设置 taskAffinity 属性,并且将 SecondActivity 启动模式设置为 singleTask,这样,四个任务会在同一个任务中开启

我们指定在 FourthActivity 中开启 SecondActivity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<application android:allowBackup="true"
android:icon="@drawable/ic_launcher" android:label="androidtasktest">

<activity android:name="com.jg.zhang.androidtasktest.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity android:name="com.jg.zhang.androidtasktest.SecondActivity"
android:launchMode="singleTask"/> <!--默认为当前任务了-->

<activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"/>

<activity android:name="com.jg.zhang.androidtasktest.FourthActivity"/>

</application>

现在从MainActivity一直启动到 FourthActivity,打印出的系统 log 为:

1
2
3
4
MainActvity 所在的任务id为: 6
SecondActivity 所在的任务id为:6
ThirdActivity 所在的任务id为:6
FourthActivity 所在的任务id为:6

这时,我们来执行 FourthActivity 中点击按钮启动 SecondActivity 的操作(注意,Second的启动模式为 singleTask),那么现在栈中的情况如何呢?

再次执行 adb sehll dumpsys activity 命令,有以下输出

1
2
3
4
5
TaskRecord {412e9458 #6 A com.jg.zhang.androidtasktest}

Run #2: ActivityRecord{412a4dd8 com.jg.zhang.androidtasktest/.SecondActivity}

Run #1: ActivityRecord{4122fae0 com.jg.zhang.androidtasktest/.MainActivity}

这时栈中的状态为 MainActivity --> SecondActivity; **确实确保了在任务中是唯一的,并且清除了同一任务中它上面的所有Activity。**并且我们可以从系统log中可以看出,SecondActivity的onCreate方法并没有重新执行,也就是说重用了上次已经启动的实例,而不是销毁创建。

实例验证 singleInstance 的行为

1.以singleInstance模式启动的Activity具有全局唯一性,即整个系统中只会存在一个这样的实例

2.以singleInstance模式启动的Activity具有独占性,即它会独占一个任务,被它开启的任何activity都会运行在其它任务中

3.被singleInstance模式的Activity开启其它的activity,能够开启一个新任务,但不一定开启新的任务,也有可能在已有的一个任务中开启

我们可以通过隐式Intent来调用App之外的Activity,来验证singleInstance的全局唯一性以及它的独占性

前面两个好理解,我们来看看第三点,如果我们不指定 ThirdActivity 的 taskAffinity 属性,那么它的默认的 taskAffinity属性就是 和 MainActivity一样的,所以,从secondActivity中启动的 ThirdActivity 会运行在MainActivity所在的任务栈中

那么,被singleInstance模式的 Activity开启的其它activity,什么情况下会开启新的任务呢?

我们对清单文件做一下修改:

1
2
<activity android:name="com.jg.zhang.androidtasktest.ThirdActivity"
android:taskAffinity="com.jg.zhang.androidtasktest.second"/>

指定一个新的 taskAffinity , 即可让 ThirdActivity 运行在新的任务栈中

singleTask vs singleInstance

其实上面的这种行为和singleTask很像

在Activity的启动模式设置为 singleTask 时,启动时系统会为它加上 FLAG_ACTIVITY_NEW_TASK

在被signleInstance模式的 Activity启动的 activity,启动时系统也会为它加上 FLAG_ACTIVITY_NEW_TASK

这也造成了它们的一些共性,在启动时都会判断是否有运行的该taskAffinity的任务

小结

Task是Android Framework中的一个概念,Task是由一系列相关的Activity组成的,是一组相关Activity的集合

Task是以栈的形式来管理的

我们在操作界面的过程中,一定会涉及到界面的跳转,其实在对界面进行跳转时,Android Framework 既能在同一个任务中对Activity进行调度,也能以Task为单位进行调度,分别对应不同的启动模式

四种启动模式的区别

场景

singleTask

当我们在B中启动D时:

注意,栈没有合并,这里只是展示了启动之后,**后退列表**的变化

补充

谁来管理任务栈

我问过温哥一个问题,Activity任务栈是从系统角度定义的还是Application?因为我看到不同App之间也可以互调,那么这个任务栈有没有一个界限呢?比如说任务栈A属于 App A,但是App B可以调用,还是说任务栈A是整个系统意义上的任务栈A?

温哥答:系统意义,在AMS里面管理,准确的说,是除了App以外的另一个进程里,不算系统。

还有一个问题,比如任务栈A完全退出了,接下来显示哪个任务栈?这个也是由AMS来调度的么?

温哥答:是,一切靠AMS,Activity Manager Service

AMS以任务栈为单元来调度:

当一个 Activity被启动,它所在的栈也会被调整到最前面(AMS以栈为调度单元),假设现在有如下情况

任务栈T1: B(Top),A

任务栈T2: D(Top),C (D、C都是singleTask模式)

B调用D:B—>D ===》 (Top) DC+BA

B调用C:B—>C ===》 (Top) C+BA

注意,上面的”+“号表示这两个任务栈并不是就是成为了一个栈(栈没有合并),而是它们的回退顺序是这样

设置launchMode与taskAffinity的一些组合

在singleTask的情况下,不设置taskAffinity默认会设置它的taskAffinity为当前任务栈

在singleInstance的情况下,A—->B—–>C

  • C会被默认设置为singleTask启动(设置FLAG_ACTIVITY_NEW_TASK,等价于singleTask)

  • 不设置C的taskAffinity的话,C的taskAffinity设置为 A所在的task,这时候回退顺序是 C——>A

startActivityForResult 方法

在Android5.0之前startActivityForResult不一定能拿到返回值

参考

CSDN zejian :Activity启动模式与任务栈全记录

我打赌你一定没搞清Actiivty的启动模式

程序亦非猿的贼船技术讨论群

Android开发艺术探索

Android面试官装逼失败之Activity的启动模式