Android基础回顾(1)——深入理解 Activity 启动模式


为什么 Activity 需要不同的启动模式

我们知道,不管是启动 App 的 Launcher Activity,还是手动调用 Context 的 startActivity 方法启动别的 Activity,默认每次都会创建这个 Activity 的实例,并依次放入一种“后进先出”的任务栈中,当我们按下返回键时,这些 Activity 会一一出栈。这是默认的启动方式,明显不能适应所有的业务场景。最简单的,如果一个 Activity 被连续打开了多次,默认情况下就会创建多个相同的 Activity 实例,这当然是不利于内存使用的。于是就有了其他的启动方式,singleTop(栈顶复用模式)、singleTask(栈内复用模式)、singleInstance(单实例模式),从命名上也可以看得出,这三种模式实现了不同的复用机制,这不同的情况下,选用不同的复用机制避免默认模式(standard)出现的实例重复创建的问题。

1.standard:标准模式

这是系统默认的启动模式,Activity 的启动模式设置为 standard 的话,不管这个 Activity 的实例是否存在或者和启动它的 Activity 是否在相同的任务栈中,都会创建一个新的实例。这样创建的 Activity 依次执行 onCreate、onStart、onResume 生命周期方法。在这种模式下,谁启动了这个 Activity,这个 Activity 就会运行在启动它的那个 Activity 的任务栈中,例如,在 MainActivity 中打开 Main2Activity(标准模式),Main2Activity 就会位于 MainActivity 所在栈的栈顶。

使用 adb shell dumpsys activity 命令查看 Activity 的栈信息,此命令的输出比较多,我们只看 Running activities (most recent first): 部分。

可以看到栈信息如下:

Running activities (most recent first):
  TaskRecord{baa1051 #2748 A=me.bakumon.launchmode U=0 StackId=1 sz=2}
    Run #12: ActivityRecord{1d38839 u0 me.bakumon.launchmode/.Main2Activity t2748}
    Run #11: ActivityRecord{6a0be5 u0 me.bakumon.launchmode/.MainActivity t2748}

说明 Main2Activity 和 MainActivity 位于相同的栈:#2748,并且 Main2Activity 位于栈顶。

使用场景

没有特殊的要求,一般使用默认的启动方法即可。

2.singleTop:栈顶复用模式

见名知意,使用这个模式启动的 Activity,如果已经位于栈顶,那么启动它并不会创建新的实例。它的 onCreate、onStart 生命周期方法不会被调用,而是会调用 onNewIntent 方法,随后调用 onResume,注意,前提是这个 Activity 已经位于栈顶,否则还是会新建实例。例如,我们在 MainActivity(标准模式) 中启动 Main2Activity(栈顶复用模式),然后在 Main2Activity 再次启动 Main2Activity,那么栈信息为 MainActivity Main2Activity,我们打印一些方法的调用情况看看。

打开 MainActivity 会依次调用 onCreate、onStart、onResume 方法。

D/MainActivity: onCreate run
D/MainActivity: onStart run
D/MainActivity: onResume run

在 MainActivity 中启动 Main2Activity,会依次调用 Main2Activity 的 onCreate、onStart、onResume 方法,虽然 Main2Activity 是 singleTop 模式,但是由于栈顶是 MainActivity,还是会新建 Main2Activity。

D/Main2Activity: onCreate run
D/Main2Activity: onStart run
D/Mai2nActivity: onResume run

在 Main2Activity 中启动 Main2Activity,由于 Main2Activity 是 singleTop 模式,并且 Main2Activity 目前位于栈顶,所以 Main2Activity 将会被复用,而不创建新的实例,会调用 Main2Activity 的 onNewIntent 方法。

D/Main2Activity: onNewIntent run
D/Main2Activity: onResume run

最后查看栈信息,启动了一次 MainActivity了,两次 Main2Activity,栈内信息:MainActivity Main2Activity,Main2Activity 只存在一个位于栈顶的实例。

Running activities (most recent first):
  TaskRecord{4337d6d #2755 A=me.bakumon.launchmode U=0 StackId=1 sz=2}
    Run #13: ActivityRecord{c3ebed u0 me.bakumon.launchmode/.Main2Activity t2755}
    Run #12: ActivityRecord{72d0112 u0 me.bakumon.launchmode/.MainActivity t2755}

使用场景

可以用来防止多次打开同一 Activity。

3.singleTask:栈内复用模式

在这种模式下,只要被启动的 Activity 在一个栈中已经存在,那么多次启动此 Activity 都不会创建新的实例,并且和 singleTop 一样,也会回调 onNewIntent。当一个启动模式为 singleTask 的 Activity 被请求启动后,系统首先会判断是否存在该 Activity 的任务栈,如果不存在,就创建一个新的任务栈,然后创建此 Activity 的实例放入新建的栈中;如果存在所需的任务栈,再判断该 Activity 是否在栈中已经存在实例,如果实例存在,系统会把该 Activity 调到栈顶,并回调 onNewIntent 方法,如果实例不存在,就创建该 Activity 的实例,压入栈中。

画出流程图,更加直观。

singleTask

具体的例子:

1.假设任务栈 S1 的情况为 ABC,现在要在 C 中启动 D(singleTask模式),D 所需的任务栈为 S2,由于 S2 和 D 的实例均不存在,所以系统先会创建任务栈 S2,然后创建 D 的实例并压入 S2 中。 此时栈的情况为:S1:ABC,S2:D。

2.假设 D 所需的任务栈为 S1,其他和第一种情况一样,那么由于 S1 已经存在,D 的实例不存在,系统直接会创建 D 的实例,压入 S1 中。 此时栈的情况为:S1:ABCD。

3.假设任务栈 S1 的情况为 ADBC,现在要在 C 中启动 D(singleTask模式),D 所需的任务栈为 S1,此时 D 不会被创建新的实例,系统会把 D 调到栈顶并回调其 onNewIntent 方法,由于singleTask 默认具有 clearTop 效果,BC 会被出栈。 此时栈的情况为:S1:AD。

第一和第二个例子的区别在于 D 所需的任务栈不同,但是怎样区别所需的任务栈呢,换句话说,什么情况下是第一种,什么情况下又是第二种。

这里就涉及到了 AndroidManifest.xml 中 Activity 节点的 android:taskAffinity 属性。默认情况下,启动一个启动模式为 singleTask 的 Activity,并不会为此 Activity 开启一个新的任务栈,而是用启动该 Activity 的 Activity 的任务栈,即第二个例子那样。

如果启动模式为 singleTask 的 Activity 设置 android:taskAffinity 属性(默认的包名)的值为非应用的包名,那么系统才会为该 Activity 创建一个新的任务栈,即第一个例子那样。

下面运行代码实例来验证我们的说法。

1.不设置 Main2Activity 的 android:taskAffinity 属性,MainActitvity 启动方式为默认,Main2Activity 为 singleTask,逻辑是在 MainActivity 中启动 Main2Activity,在 Main2Activity 中 再次启动自身。然后查看栈的情况。

Running activities (most recent first):
  TaskRecord{8e47f5c #3119 A=me.bakumon.launchmode U=0 StackId=1 sz=2}
    Run #2: ActivityRecord{9e35bb8 u0 me.bakumon.launchmode/.Main2Activity t3119}
    Run #1: ActivityRecord{39eb835 u0 me.bakumon.launchmode/.MainActivity t3119}

上面的信息显示,Main2Activity 和 MainActivity 位于相同的栈(me.bakumon.launchmode)中,并且 Main2Activity 位于栈顶。

2.和上面不同的是,设置 Main2Activity 的 android:taskAffinity 属性为非 me.bakumon.launchmode。

<activity android:name=".Main2Activity"
          android:taskAffinity="me.bakumon.launchmode.Main2Activity"
          android:launchMode="singleTask"/>

同样在 MainActivity 中启动 Main2Activity,在 Main2Activity 中 再次启动自身。查看栈的情况。

Running activities (most recent first):
  TaskRecord{f3c3fb3 #3121 A=me.bakumon.launchmode.Main2Activity U=0 StackId=1 sz=1}
    Run #2: ActivityRecord{a8820fd u0 me.bakumon.launchmode/.Main2Activity t3121}
  TaskRecord{f81d970 #3120 A=me.bakumon.launchmode U=0 StackId=1 sz=1}
    Run #1: ActivityRecord{ddc64fb u0 me.bakumon.launchmode/.MainActivity t3120}

上面信息显示,Main2Activity 和 MianActivity 位于不同的栈中,并且 Main2Activity 的栈名和我们设置的 android:taskAffinity 属性值相同,MainActivity 的栈名为默认的包名。

打开手机的最近任务,也可以证明这一点,最顶是 Mian2Activity,底下是 MainActivity,它们位于不同的最近任务中。

最近任务.jpg

使用场景

项目的 HomeActivity 使用此启动方式,可以确保 HomeActivity 只存在一个实例,并且从深层 Activity 直接打开 HomeActivity,默认会自动清空 HomeActivity 以上的多个 Activity,不用手动关闭。

4.singleInstance:单实例模式

此模式是加强的 singleTask,拥有 singleTask 所有的特性,强化点在于,用此模式开启的 Activity 只能单独位于一个任务栈中。在第一次启动该模式的 Activity 时,系统会为该 Activity 创建一个专属的任务栈,并创建该 Activity 的实例压入这个专属的栈中。以后启动该 Activity,不会创建新的实例,系统会回调 onNewIntent 方法,除非这个专属的栈被系统销毁。

评论