任务和返回栈

应用通常包含多个 Activity ,每个 Activity 均应围绕用户可以执行的特定操作设计,并且能够启动其他 Activity,一个 Activity 可以启动设备上其他应用中的 Activity,即使两个 Activity 可能来自不同的应用,但是 Android 仍会将 Activity 保留在相同的任务中,以维护这种无缝的用户体验。这里所说的任务就是指在执行特定作业时与用户交互的一系列 Activity,这些 Activity 按照各自的打开顺序排列在堆栈(即返回栈)中。返回栈以“后进先出”对象结构运行,如下图

如果要查看 Activity Task栈的情况,可以在命令行用 adb 命令查看

adb shell dumpsys activity activities

执行命令会出现很长一段详细信息 找到 Running activities 即可查看,如下图

启动模式

在了解了任务和返回栈后,我们来说说启动模式,上图的堆栈是比较常规的,如果我们一直启动同一个 Activity 系统会重复创建多个实例,但这不是我们想要的结果。这时候为了满足我们的需求就需要使用 Android 提供的启动模式来修改系统的默认行为。目前有四种启动模式:standard、singleTop、singleTask 和 singleInstance。在 AndroidManifest.xml 中配置即可,如下:

1
2
3
4
<activity 
        android:name="com.will.testdemo.launchmode.A"
        android:launchMode="standard">
    </activity>

standard 默认模式

系统在启动 Activity 的任务中创建 Activity 的新实例并向其传送 Intent。Activity 可以多次实例化,不管这个实例是否已经存在,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例。这种模式的 Activity 被创建时它的 onCreate、onStart 都会被调用。这是一种典型的多实例实现,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个 Activity,那么这个 Activity 就运行在启动它的那个 Activity 所在的栈中。

这里通过简单的代码来验证,先实现方法来打印 Activity 的生命周期调用过程和 Taskid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
fun printTaskInfo(activity: Activity, methodName: String) {
        log("${activity.localClassName} $methodName taskId = ${activity.taskId}")
    }

fun log(message: String, tag: String = "debugLog") {
            Log.i(tag, message)
    }

/**
 * @param T 目标 Activity
 */
inline fun <reified T : Activity> Context.toActivity() {
    startActivity(Intent(this, T::class.java))
}

界面很简单就一个按钮,就不截图了,Activity 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_a)
        printTaskInfo(this,"onCreate")
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        printTaskInfo(this,"onNewIntent")
    }

    override fun onStart() {
        super.onStart()
        printTaskInfo(this,"onStart")
    }

    fun click(view: View?) {
        toActivity<A>()
    }
}

启动 A 然后点击两下按钮,日志如下:

1
2
3
4
5
6
01-02 22:07:00.330 28281-28281/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 44
01-02 22:07:00.332 28281-28281/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 44
01-02 22:07:01.580 28281-28281/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 44
01-02 22:07:01.582 28281-28281/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 44
01-02 22:07:02.325 28281-28281/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 44
01-02 22:07:02.327 28281-28281/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 44

使用 adb 命令查看 Activity Task 栈,可以看出每启动一次 A 都会创建一次实例,不管这个实例是否已经存在

singleTop 栈顶复用模式

在这种模式下,如果当前任务的顶部已存在 Activity 的一个实例,则系统会通过调用该实例的 onNewIntent() 方法向其传送 Intent,而不是创建 Activity 的新实例。Activity 可以多次实例化,而每个实例均可属于不同的任务,并且一个任务可以拥有多个实例(但前提是位于返回栈顶部的 Activity 并不是 Activity 的现有实例)。这个 Activity 的 onCreate、onStart 不会被系统调用,因为它并没有发生改变。

image.png

这里我们新建一个 Activity B ,调用 Activity 流程 :A -> A -> B -> A

1
2
3
4
<activity
     android:name="com.will.testdemo.launchmode.A"
     android:launchMode="singleTop">
 </activity>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_a)
        printTaskInfo(this, "onCreate")
    }

    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        printTaskInfo(this, "onNewIntent")
    }

    override fun onStart() {
        super.onStart()
        printTaskInfo(this, "onStart")
    }

     fun click(view: View?) {
        when (view?.id) {
            R.id.bt_toA -> toActivity<A>()
            R.id.bt_toB -> toActivity<B>()
            else -> { }
        }
    }

打印日志:

1
2
3
4
5
6
7
01-02 22:15:18.399 28530-28530/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 45
01-02 22:15:18.400 28530-28530/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 45
01-02 22:15:21.229 28530-28530/com.will.testdemo I/debugLog: launchmode.A onNewIntent taskId = 45
01-02 22:15:24.927 28530-28530/com.will.testdemo I/debugLog: launchmode.B onCreate taskId = 45
01-02 22:15:24.929 28530-28530/com.will.testdemo I/debugLog: launchmode.B onStart taskId = 45
01-02 22:15:26.449 28530-28530/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 45
01-02 22:15:26.450 28530-28530/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 45

Activity Task 栈

可以看出当 A 在当前栈顶的时候没有创建新的实例,并调用 onNewIntent 方法,没有调用 onCreate 和 onStart 方法

singleTask 栈内复用模式

这是一种单实例模式,在这种模式下,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,和 singleTop一样,系统也会回调其 onNewIntent。当一个具有 singleTask 模式的Activity请求启动后,比如 Activity A,系统首先会寻找是否存在 A 想要的任务栈,如果不存在,就重新创建一个任务栈,然后创建 A 的实例后把 A 放到栈中。如果存在 A 所需的任务栈,这时要看 A 是否在栈中有实例存在,如果有实例存在,那么系统就会把 A 调到栈顶并调用它的 onNewIntent 方法,如果实例不存在,就创建 A 的实例并把 A 压入栈中 。

image.png

关于上文中所说的想要的任务栈,指的是 taskAffinity 属性,手动设置所需的任务栈,这个后面会具体介绍

调用 Activity 流程 依旧是:A -> A -> B -> A
打印日志:

1
2
3
4
5
6
7
01-02 22:25:59.608 24498-24498/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 54
01-02 22:25:59.611 24498-24498/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 54
01-02 22:26:02.844 24498-24498/com.will.testdemo I/debugLog: launchmode.A onNewIntent taskId = 54
01-02 22:26:05.753 24498-24498/com.will.testdemo I/debugLog: launchmode.B onCreate taskId = 54
01-02 22:26:05.758 24498-24498/com.will.testdemo I/debugLog: launchmode.B onStart taskId = 54
01-02 22:26:07.040 24498-24498/com.will.testdemo I/debugLog: launchmode.A onNewIntent taskId = 54
01-02 22:26:07.047 24498-24498/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 54

Activity Task 栈

纳尼 Activity B 呢 怎么不见了,这是什么鬼操作,原来是 singleTask 默认有 clearTop 的效果,会导致栈内所有在它上面的 Activity 全部出栈,这点一定不要忽略了

singleInstance 单实例模式

与 singleTask 相同,只是系统不会将任何其他 Activity 启动到包含实例的任务中。该 Activity 始终是其任务唯一仅有的成员;由此 Activity 启动的任何 Activity 均在单独的任务中打开。也就是有此种模式的 Activity 只能单独地位于一个任务栈中

调用 Activity 流程 :A -> B -> B -> A,这次把 B 的启动模式设置为 singleInstance

打印日志:

1
2
3
4
5
6
7
01-02 22:41:59.069 305-305/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 57
01-02 22:41:59.071 305-305/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 57
01-02 22:42:00.280 305-305/com.will.testdemo I/debugLog: launchmode.B onCreate taskId = 58
01-02 22:42:00.283 305-305/com.will.testdemo I/debugLog: launchmode.B onStart taskId = 58
01-02 22:42:02.340 305-305/com.will.testdemo I/debugLog: launchmode.B onNewIntent taskId = 58
01-02 22:42:03.658 305-305/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 57
01-02 22:42:03.659 305-305/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 57

Activity Task 栈

结合打印日志和 Activity Task 栈可以看出,有此种模式的 Activity 只能单独地位于一个任务栈中,如果已经创建过,则调用 onNewIntent 方法 不会调用 onCreate 和 onStart

taskAffinity 属性

taskAffinity,可以翻译为任务相关性。这个参数标识了一个 Activity 所需要的任务栈的名字,默认情况下,所有 Activity 所需的任务栈的名字为应用的包名,当 Activity 设置了 taskAffinity 属性,那么这个 Activity 在被创建时就会运行在和 taskAffinity 名字相同的任务栈中,如果没有,则新建 taskAffinity 指定的任务栈,并将 Activity 放入该栈中。另外,taskAffinity 属性主要和 singleTask 或者 allowTaskReparenting 属性配对使用,在其他情况下没有意义。

与 singleTask 结合使用,调用 Activity 流程:A -> B -> B -> A -> B,设置 B 的启动模式为 singleTask ,并设置 taskAffinity

1
2
3
4
5
6
7
8
9
<activity
        android:name="com.will.testdemo.launchmode.A">
    </activity>
    
 <activity
        android:name="com.will.testdemo.launchmode.B"
        android:launchMode="singleTask"
        android:taskAffinity="com.will.testdemo.task1">
    </activity>

打印日志:

1
2
3
4
5
6
7
8
9
01-02 23:13:29.179 16793-16793/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 59
01-02 23:13:29.180 16793-16793/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 59
01-02 23:13:31.800 16793-16793/com.will.testdemo I/debugLog: launchmode.B onCreate taskId = 60
01-02 23:13:31.801 16793-16793/com.will.testdemo I/debugLog: launchmode.B onStart taskId = 60
01-02 23:13:33.740 16793-16793/com.will.testdemo I/debugLog: launchmode.B onNewIntent taskId = 60
01-02 23:13:34.928 16793-16793/com.will.testdemo I/debugLog: launchmode.A onCreate taskId = 60
01-02 23:13:34.931 16793-16793/com.will.testdemo I/debugLog: launchmode.A onStart taskId = 60
01-02 23:13:36.203 16793-16793/com.will.testdemo I/debugLog: launchmode.B onNewIntent taskId = 60
01-02 23:13:36.204 16793-16793/com.will.testdemo I/debugLog: launchmode.B onStart taskId = 60

Activity Task 栈

B 被创建时,因没有 com.will.testdemo.task1 的任务栈,于是新建任务栈,并把 B 放入栈内。继续创建 A,由于 A 没有设置启动模式,则放入 com.will.testdemo.task1 栈中。再一次启动 B,因栈内有 B 实例,所以系统就把 B 调到栈顶,由于 singleTask 默认有 clearTop 的效果,导致栈内所有在它上面的 Activity 全部出栈,所以最后 com.will.testdemo.task1 栈内只有 B 一个实例

总结

上面废话了那么多,那么这些启动模式到底什么时候使用呢,这里列出部分使用场景以供参考。

launchMode 使用场景
singleTop 适合启动同类型的 Activity,例如接收通知启动的内容显示页面
singleTask 适合作为程序入口
singleInstance 适合需要与程序分离开的页面,例如闹铃的响铃界面