Android-Activity生命周期

Android-Activity生命周期

1. Activity简介

Activity 即一个用户界面,可以理解成一个视图容器,容器内可以包裹和展示各类控件。全部 Activity 都需要在 Manifest 内声明。Activity 的生命周期为:

Activity 生命周期

需要注意的是:

  • onStop() 在 Activity 不可见时才调用,例如 AActivity 启动 BActivity,若 BActivity 是透明或弹窗形式(android:theme="@android:style/Theme.Dialog"),则 AActiivty 不会调用 onStop()
  • Activity 的 onSaveInstanceState()onRestoreInstanceState() 并不是生命周期方法,当 Activity 处于 isFinishing() 状态时,onSaveInstanceState() 就不会被调用,因此只适合用于保存一些临时性的状态。

1.1 启动Activity

假设有一个 DemoActivity 注册信息如下:

1
2
3
4
5
6
7
8
9
10
<activity
android:name="demo.DemoActivity"
android:exported="false"
android:excludeFromRecents="false"
>
<intent-filter>
<action android:name="demo.customActionName" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
  • exported: 是否对外部 App 可见,默认为 false。隐式启动 Activity 时搜索 ActionName 的范围是当前 App 内,如果允许被其他 App 启动,则需要设置为 true
  • excludeFromRecents: 是否在 Activity 进入后台时立即从「最近任务」中移除记录,默认为 false。如果设置为 `true,则该 Activity 一旦进入后台,就会从「最近任务」中删除其记录,不论该 Activity 是否是最后一个或唯一一个活跃的 Activity。

(1)显式启动 Actiivty:

1
2
3
4
5
6
7
8
9
Intent intent = new Intent(context, DemoActivity.class);
// 直接插入数据
intent.putExtra("key", value)
// 通过 Bundle
Bundle bundle = new Bundle();
bundle.putXXX(String key, XXX value);
intent.putExtra(bundle);

context.startActivity(intent);

(2)隐式启动 Activity:

1
2
3
Intent intent = new Intent();
intent.setAction("demo.customActionName");
context.startActivity(intent)

(3)startActivity() 是 Context 中的方法,因此 Activity 或 Application 都可以调用。但如果使用 Application.startActivity() 则需要给 Intent 添加一个 Flag FLAG_ACTIVITY_NEW_TASK,否则会报错:

1
android.util.AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?

Activity、Application 都是 Context 抽象类的子类,而 ContextImpl 是 Context 的实现。ActivityThread 在 main() 方法中实例化一个 ComtextImpl 并与 Activity 绑定。

1.2 退出Activity

Activity 可以通过调用 finish() 退出。当 App 正在退出时,isFinishing() == true,可用于在 onPause()onStop() 中判断该 Activity 是正在退出或只是进入后台。

同时退出多个 Activity 的方法:

  • 在所有被开启的 Activity 中注册特定的广播监听,收到该广播时退出自己,并在需要统一退出时发送广播。
  • 启动新的 Activity 时,使用 startActivityForResult() 替代 startActivity(),并在 onActivityResult() 中将自己 finish()
  • 用 List 等记录每个启动的 Activity,并在需要时分别关闭。需要注意的是,使用 List 存放 Activity 有内存泄露的风险,当 Activity 关闭后,要及时清掉对应 List 中的引用。
  • 启动一个 singleTask 模式的 Activity,或是给 Intent 添加 Flag:FLAG_ACTIVITY_CLEAR_TOP,则系统会在新 Activity 启动后将其上的所有 Activity 销毁。

1.3 Fragment懒加载

关键点在于 setUserVisibleHint() 会在 onCreateView 之前执行,当 Fragment 可见状态改变时调用,设置一个标志位表示是否已经加载过,在 setUserVisibleHint() 回调时如果没加载过且可见,则加载数据并将标志位改为已加载。


2. Activity启动模式

Activity 有 4 种启动模式:

  • standard
  • singleTop
  • singleTask
  • singleInstance

需要先了解到,Android 中 Activity 是存在任务栈 Task 中的,启动一个 Activity 时会将其压栈,销毁时弹出,不同启动模式会导致 Activity 任务栈产生不同的行为,且一个 App 进程可以拥有多个 Activity 任务栈。

Activity 可以通过在 Manifest 中指定 taskAffinity 来指定其 倾向于 加入的任务栈,但只有在满足条件时才会添加到该指定任务栈中。Activity 在启动时会先查找是否存在指定的栈,假如存在则会 「优先尝试」 压入对应栈,否则才会新建一个栈并启动。如果 Activity 没有指定,则默认等同于 Application 的 taskAffinity,如果 Application 也没有指定,则默认等同于包名。

但是,taskAffinity 并非 100% 生效;例如 singleInstance 模式启动 Activity 时,其栈内仅允许存在该 Activity;即使其他 Activity 指定同一个 taskAffinity,依然会启动到新的栈中,但这两个栈的 taskAffinity 名称相同;所以判断栈是否相同仍然只能通过 TaskID,而不是 taskAffinity。

下文描述了 Activity 不同启动模式对栈的影响,用 / 表示 栈底> 表示 栈顶,任务栈之间用 - 连接,右侧的任务栈为最新任务栈。

2.1 Standard

(1)Standard 即标准默认模式。启动一个 Activity 时,不论 Activity 在任务栈中是否已经有实例,都新建一个实例并压入任务栈。当前 Activity 在当前任务栈可以有多个实例,每个实例也可以在不同任务栈。

当前任务栈 如果返回 执行任务
/A> 退出 Start Standard B
/AB> /A> Start Standard B
/ABB> /AB> Start Standard A
/ABBA> /ABB> -

(2)Standard 模式通常用于允许一个页面同时创建多个,或者从逻辑上可以保证不会触发多个的情况下。

2.2 SingleTop

(1)SingleTop 即栈顶唯一模式,启动一个 Activity 时,如果该 Activity 已在栈顶则不会新建一个 Activity 实例而是复用它,否则其行为和 Standard 模式一致。

当前任务栈 如果返回 执行任务
1 /A> 退出 Start SingleTop B
2 /AB> /A> Start SingleTop B
3 /AB> /A> Start SingleTop A
4 /ABA> /AB> Start SingleTop B
5 /ABAB> /ABA> -

(2)SingleTop 模式通常用于可能多次弹出的页面。实际上,我认为除非某个 Activity 有明确的需求要打开多个实例,否则都应该使用 SingleTop。因为大多数页面跳转都有可能被误触发多次,但绝大多数情况下都是不希望同时存在多个实例的。

2.3 SingleTask

(1)SingleTask 即任务栈唯一模式,启动一个 Activity 时,如果任务栈中存在该 Activity 实例,则复用它,否则才创建一个新的 Activity 实例。SingleTask 和 SingleTop 的区别在于,SingleTop 只在栈顶 Activity 和要启动的 Activity 相同时才复用,而 SingleTask 是每个 Activity 在当前整个任务栈中都只有一个实例。

此外 SingleTask 还有一个重要特性:当一个 SingleTask 模式 Activity 被从任务栈中间移到栈顶时,会将原本任务栈中位于该 SingleTask Activity 之上的其他 Activity 销毁。需要注意的是,如果新启动一个 SingleTask Activity 并压入任务栈,由于其原本并不在栈中而是直接压入栈顶,因此不会导致栈内其他 Activity 被销毁。

当前任务栈 如果返回 执行任务
1 /A> 退出 Start SingleTask B
2 /AB> /A> Start SingleTask B
3 /AB> /A> Start SingleTask A
4 /A> 退出 Start Standard B
5 /AB> /A> -

(2)SingleTask 模式常用在 App 的 MainActivity 中,因为大多数情况下 App 在主页面上返回时都应该返回到桌面或退出,而不是返回到其他页面中,因此可以在将 MainActivity 移入栈顶时,直接销毁其他 Activity。

2.4 SingleInstance

(1)SingleInstance 即唯一实例模式,启动一个 Activity 时,寻找所有的任务栈,如果某个任务栈中存在该 Activity 实例,则切换到该任务栈,否则新建一个任务栈并创建该 Activity 实例。

假如有三个 Activity 按照以下关系启动:

1
A(Standard) -> B(SingleInstance) -> C(Standard)
  • 因为 B 是 SingleInstance,所以会创建一个新的栈,并在新的栈中启动 B。
  • 而 B 启动 C 时,同样由于 B 设置了 SingleInstance 属性,因此会先判断 C 的启动模式,以及 Manifest 中 C 对应的 taskAffinity,如果没有指定,则会把 C 启动在默认栈中,也就是和 A 同一个栈。
  • 因此如果在 C 中按下返回键,由于 A 和 C 在同一个栈,所以会回到 A,而不是 B。

重点在于:任务栈不是唯一的,一个 App 可以有多个任务栈,每个任务栈又分别管理包含的 Activity。

当前任务栈 如果返回 执行任务
0 未启动 未启动 Start standard M
1 /M> 退出 Start SingleInstance A
2 /M> - /A> /M> Start SingleInstance B
3 /M> - /A> - /B> /M> - /A> Start SingleInstance A
4 /M> - /B> - /A> /M> - /B> Start Standard C
5 /B> - /A> - /MC> /B> - /A> - /M> 返回
6 /B> - /A> - /M> /B> - /A> 返回
7 /B> - /A> /B> 返回
8 /B> 退出 -

前 3 步都比较好理解,反正就是整个 App 所有任务栈都只允许有一个实例。重点在从第 4 步开始,由于最开始的 M 是 Standard 模式启动的,而第 4 步中 C 也是 Standard 模式,也就导致本来在最底下的任务栈 /M> 处于最上层任务栈,并将 C 压入了 /M> 所在的这个栈顶,因此从 C 返回的时候会先返回到 M。

(2)SingleInstance 通常用于某些可被共享唤起的 App,但这些 App 仅有部分页面需要被共享唤起。

例如:

  • 电话:其包括了拨号页和来电提醒页,而来电提醒页(通话页)则需要设置为 SingleInstance。
  • 闹铃:其包括了设置闹钟的页面和响铃提醒的页面,而响铃提醒页面则需要设置为 SingleInstance。

以电话的来电提醒页为例:

  • 首先 standard 和 singleTop 是一定不能满足的,否则如果通话期间切到其他 App 后再切回来,就会再次创建一个新的「通话页」。
  • 而 singleTask 同样存在问题:假如用户通过「拨号页」拨打电话,进入「通话页」后,由于设置了 singleTask,「拨号页」就会被销毁,通话结束后用户就无法再次返回到「拨号页」了。收到来电提醒也是类似的场景。
  • 如果电话 App 的「拨号页」是启动的但被置到后台,假如用户正在某个 App「Demo」中,此时收到来电进入「通话页」,电话结束后用户当然是期望能返回到原先的「Demo」页面,因此「通话页」必须处于独立的栈中,否则「通话页」结束后返回时,就会回到「拨号页」了。

3. SingleInstance导致的异常现象

平时开发在绝大多数时候,都不会给 Activity 设置为 SingleInstance 模式,所以了解不够深入。有一次遇到一个需求,要给 App 开发一套闪屏 SplashActivity 流程,考虑到如下技术方案需求:

  • 闪屏页是 App 唯一的。
  • 由于闪屏是很重要的商业化手段,所以希望闪屏页可以在任意一个其他页面存在时被激活到前台。
  • 闪屏启动结束后,还需要返回到原来的页面(这也明确了不能使用 singleTask)。

根据这三个需求,自然而然想到了 singleInstance,表面上看似乎完全满足以上条件。另外因为闪屏页会经常在触发某些条件时被唤起,但并不一定每一次都需要真实可见,例如闪屏资源未准备好时就需要直接 finish(),用伪代码表示如下:

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
/**
* Manifest
*/
<manifest>
<activity
android:name="demo.SplashActivity"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="demo.MainActivity" />
</manifest>

/**
* SplashActivity
*/
public class SplashActivity extends AppCompatActivity {
// 这里用 isFirstTime 模拟第一次进入 App 后,会跳转到 MainActivity,
// 然后退到后台再切回来后,即第二次打开时,视为不满足闪屏条件,直接关闭闪屏。
private static boolean isFirstTime = true;

@Override
protected void onCreate(Bundle intent) {
if (isFirstTime) {
isFirstTime = false;
startActivity(new Intent(this, MainActivity.class));
}
finish();
}
}

编译运行,SplashActivity 正常跳转到 MainActivity,但此时如果在 MainActivity 点击 Home 键回到桌面,再点击 App 图标,不仅 App 无法唤起,而且最近任务中也没有 App 的记录了。

返回桌面后,点击图标无法启动 Activity,且最近任务记录消失

3.1 问题定位

出现这个现象,我的第一反应就是:两个 Activity 都被杀了,于是分别给两个 Activity 都添加了生命周期的日志,发现打印出来的如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 首次启动 App 后:
priv.luis.launchmodetest D/luis: SplashActivity - onCreate
priv.luis.launchmodetest D/luis: MainActivity - onCreate
priv.luis.launchmodetest D/luis: MainActivity - onResume
priv.luis.launchmodetest D/luis: SplashActivity - onDestroy
// 点击 Home 键:
priv.luis.launchmodetest D/luis: MainActivity - onPause
priv.luis.launchmodetest D/luis: MainActivity - onStop
// 点击多任务键,从最近任务中返回 App:
priv.luis.launchmodetest D/luis: MainActivity - onResume
// 再次点击 Home 键:
priv.luis.launchmodetest D/luis: MainActivity - onPause
priv.luis.launchmodetest D/luis: MainActivity - onStop
// 点击 App 图标:
priv.luis.launchmodetest D/luis: SplashActivity - onCreate
priv.luis.launchmodetest D/luis: SplashActivity - onDestroy

从日志上看,MainActivity 自始至终都没有被杀掉。如果仔细看录屏的后半部分,有一次在点击桌面图标之后,App 在一瞬间被拉起来了,然后又自动关闭了。结合点击 App 图标之后的日志来看,很明显是因为点击 App 图标后启动的是 SplashActivity,由于 isFirstTime == false 所以直接执行了 finish()

  • 但是为什么 MainActivity 无法启动呢?难道是因为给 SplashActivity 设置了 singleInstance 吗?但即使改为给 MainActivity 设置 singleInstance,依然是同样的现象。
  • 为什么第一次点 Home 键返回桌面,最近任务中有 App 的记录,但是从桌面点击了 App 图标之后,即使 MainActivity 仍然存活,但最近任务中就没有 App 记录了呢?
  • 难道是 Application 被强制 Kill 掉了,以至于连 Log 的机会都没有吗?

再次重复整个步骤,并且每一步都通过 ADB 查看当前 Activity 栈信息:

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
// (1)首次打开 APP,MainActivity 处于前台,Launcher 处于后台。
Running activities (most recent first):
TaskRecord{392952a #745 A=.abc U=0 StackId=744 sz=1}
Run #0: ActivityRecord{a4f60c2 u0 priv.luis.launchmodetest/.MainActivity t745}
Running activities (most recent first):
TaskRecord{cf7cd87 #2 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
Run #0: ActivityRecord{5dab3ba u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t2}

// (2)点击 Home 键回到 Launcher,MainActivity 处于后台。
// 此时多任务中 App 记录正常显示,停留在 MainActivity 界面。
Running activities (most recent first):
TaskRecord{cf7cd87 #2 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
Run #0: ActivityRecord{5dab3ba u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t2}
Running activities (most recent first):
TaskRecord{392952a #745 A=.abc U=0 StackId=744 sz=1}
Run #0: ActivityRecord{a4f60c2 u0 priv.luis.launchmodetest/.MainActivity t745}

// (3)点击 App 图标,App 未被唤起,仍然停留在桌面。
// 此时 多任务中 App 记录已经消失。
Running activities (most recent first):
TaskRecord{cf7cd87 #2 I=com.google.android.apps.nexuslauncher/.NexusLauncherActivity U=0 StackId=0 sz=1}
Run #0: ActivityRecord{5dab3ba u0 com.google.android.apps.nexuslauncher/.NexusLauncherActivity t2}
Running activities (most recent first):
TaskRecord{96b6b62 #750 A=.abc U=0 StackId=749 sz=1}
Run #0: ActivityRecord{e949a28 u0 priv.luis.launchmodetest/.MainActivity t750}

WTF?MainActivity 竟然还存活着,说明 App 并没有被杀死,然而最近任务并没有任何记录!

3.2 原因分析

首先尝试反向验证,在 SplashActivity 启动 MainActivity 的 Intent 中添加两个 Flag,并且去掉 SplashActivity 中 finish() 的逻辑:

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
37
38
39
40
41
42
43
/**
* Manifest
*/
<manifest>
<activity
android:name="demo.SplashActivity"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name="demo.MainActivity" />
</manifest>

/**
* SplashActivity
*/
public class SplashActivity extends AppCompatActivity {
// 这里用 isFirstTime 模拟第一次进入 App 后,会跳转到 MainActivity,
// 然后退到后台再切回来后,即第二次打开时,视为不满足闪屏条件,直接关闭闪屏。
private static boolean isFirstTime = true;

@Override
protected void onCreate(Bundle intent) {
// 这里延迟 2 秒,只是为了让 SplashActivity 的界面加载出来,
// 这样在最近任务中能看的更清晰,否则可能无法生成 Activity 的缩略图。
mainHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (isFirstTime) {
isFirstTime = false;
Intent intent = new Intent(SplashActivity.this, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
startActivity(intent);
}
}
}, 2000);
// 不再退出 SplashActivity
// finish();
}
}

再次尝试,即可在最近任务中看到 MainActivity 从启动后就一直存在未被销毁,只不过因为 App 进入后台后,点击图标启动时每次启动的都是 SplashActivity,且直接走了 finishi() 的逻辑(如果删除 finish() 的逻辑,就会停留在 SplashActivity 界面中),所以一直没有机会被唤醒而已:

在最近任务中显示 MainActivity 的记录

3.3 SingleInstance对Activity栈的影响

(1)思考一个问题,从一个 App 返回到桌面(App 在后台)后,「点击 App 图标打开」和「从最近任务列表打开」有什么不同?

答案如下:

  • Android 的桌面(或者应用列表)本身也是一个 Activity(Launcher),因此从桌面点击 App 图标打开一个 Activity,本质上也是一个 Activity 通过 startActivity() 启动另一个 Activity 的过程,只不过此时会默认给 Intent 添加一个 FLAG_ACTIVITY_NEW_TASK 的 Flag,作用是先查找目标 Activity 指定的 taskAffinity 栈是否存在,如果存在就会直接唤起这个栈。
  • Android 在通过点击 App 图标启动时,启动的是 Manifest 中指定了 android.intent.category.LAUNCHER 属性的 Activity,而最近任务列表中显示的则是 App 最后活跃的栈中的栈顶 Activity。

(2)因此,对上文 4.1 节的异常现象完整回放整个 App 的 Activity 栈状态。其中,用 / 表示一个栈的栈底,用 > 表示一个栈的栈顶,用 *** 表示会被显示在最近任务记录中的栈,且最后活跃的栈位于最上方:

  1. 首次点击 App 图标打开 App,打开的是作为 android.intent.category.LAUNCHER 的 SplashActivity:

    1
    2
    / SplashActivity > ***
    / Launcher >
  2. SplashActivity 启动 MainActivity,由于 SplashActivity 是 singleInstance 模式而独享一个栈,因此 MainActivity 被启动到新建的栈中:

    1
    2
    3
    / MainActivity > ***
    / SplashActivity >
    / Launcher >
  3. SplashActivity 调用 finish(),由于退出后其栈内所有 Activity 均已退出,所以该栈也被清理销毁:

    1
    2
    / MainActivity > ***
    / Launcher >
  4. 在 MainActivity 中点击 Home 键返回桌面,MainActivity 及其栈进入后台:

    1
    2
    / Laucher >
    / MainActivity > ***
  5. 根据栈状态可知,此时最近任务中记录的是 MainActivity 及其栈,因此从最近任务中点击进入,也自然会激活 MainActivity:

    1
    2
    / MainActivity > ***
    / Launcher >
  6. 再次点击 Home 键回到桌面,MainActivity 再次进入后台:

    1
    2
    / Laucher >
    / MainActivity > ***
  7. 点击 App 图标打开 App,Launcher 通过 Manifest 定义的 android.intent.category.LAUNCHER 打开 SplashActivity,由于设置了 singleInstance,所以新建一个栈并在其中打开 SplashActivity:

    1
    2
    3
    / SplashActivity > ***
    / Launcher >
    / MainActivity >
  8. SplashActivity 执行了 finish(),并且由于栈内所有 Activity 均已销毁,该栈也被清理销毁:

    1
    2
    / Launcher >
    / MainActivity >

    正是这一步将最近任务中的 App 记录清理了。因为默认情况下,一个 App 只在最近任务中记录一个栈,也就是最后活跃的栈,这个栈一旦被销毁,又没有其他栈被激活,则 App 记录就会从最近任务中删除。