Android-Activity生命周期
Android-Activity生命周期
1. Activity简介
Activity 即一个用户界面,可以理解成一个视图容器,容器内可以包裹和展示各类控件。全部 Activity 都需要在 Manifest 内声明。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 | <activity |
exported
: 是否对外部 App 可见,默认为false
。隐式启动 Activity 时搜索 ActionName 的范围是当前 App 内,如果允许被其他 App 启动,则需要设置为true
。excludeFromRecents
: 是否在 Activity 进入后台时立即从「最近任务」中移除记录,默认为false
。如果设置为 `true,则该 Activity 一旦进入后台,就会从「最近任务」中删除其记录,不论该 Activity 是否是最后一个或唯一一个活跃的 Activity。
(1)显式启动 Actiivty:
1 | Intent intent = new Intent(context, DemoActivity.class); |
(2)隐式启动 Activity:
1 | Intent intent = new 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 | /** |
编译运行,SplashActivity 正常跳转到 MainActivity,但此时如果在 MainActivity 点击 Home 键回到桌面,再点击 App 图标,不仅 App 无法唤起,而且最近任务中也没有 App 的记录了。
3.1 问题定位
出现这个现象,我的第一反应就是:两个 Activity 都被杀了,于是分别给两个 Activity 都添加了生命周期的日志,发现打印出来的如下:
1 | // 首次启动 App 后: |
从日志上看,MainActivity 自始至终都没有被杀掉。如果仔细看录屏的后半部分,有一次在点击桌面图标之后,App 在一瞬间被拉起来了,然后又自动关闭了。结合点击 App 图标之后的日志来看,很明显是因为点击 App 图标后启动的是 SplashActivity
,由于 isFirstTime == false
所以直接执行了 finish()
。
- 但是为什么 MainActivity 无法启动呢?难道是因为给
SplashActivity
设置了singleInstance
吗?但即使改为给MainActivity
设置singleInstance
,依然是同样的现象。 - 为什么第一次点 Home 键返回桌面,最近任务中有 App 的记录,但是从桌面点击了 App 图标之后,即使 MainActivity 仍然存活,但最近任务中就没有 App 记录了呢?
- 难道是 Application 被强制 Kill 掉了,以至于连 Log 的机会都没有吗?
再次重复整个步骤,并且每一步都通过 ADB 查看当前 Activity 栈信息:
1 | // (1)首次打开 APP,MainActivity 处于前台,Launcher 处于后台。 |
WTF?MainActivity
竟然还存活着,说明 App 并没有被杀死,然而最近任务并没有任何记录!
3.2 原因分析
首先尝试反向验证,在 SplashActivity 启动 MainActivity 的 Intent 中添加两个 Flag,并且去掉 SplashActivity 中 finish()
的逻辑:
1 | /** |
再次尝试,即可在最近任务中看到 MainActivity 从启动后就一直存在未被销毁,只不过因为 App 进入后台后,点击图标启动时每次启动的都是 SplashActivity,且直接走了 finishi()
的逻辑(如果删除 finish()
的逻辑,就会停留在 SplashActivity 界面中),所以一直没有机会被唤醒而已:
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 栈状态。其中,用 /
表示一个栈的栈底,用 >
表示一个栈的栈顶,用 ***
表示会被显示在最近任务记录中的栈,且最后活跃的栈位于最上方:
首次点击 App 图标打开 App,打开的是作为
android.intent.category.LAUNCHER
的 SplashActivity:1
2/ SplashActivity > ***
/ Launcher >SplashActivity 启动 MainActivity,由于 SplashActivity 是 singleInstance 模式而独享一个栈,因此 MainActivity 被启动到新建的栈中:
1
2
3/ MainActivity > ***
/ SplashActivity >
/ Launcher >SplashActivity 调用
finish()
,由于退出后其栈内所有 Activity 均已退出,所以该栈也被清理销毁:1
2/ MainActivity > ***
/ Launcher >在 MainActivity 中点击 Home 键返回桌面,MainActivity 及其栈进入后台:
1
2/ Laucher >
/ MainActivity > ***根据栈状态可知,此时最近任务中记录的是 MainActivity 及其栈,因此从最近任务中点击进入,也自然会激活 MainActivity:
1
2/ MainActivity > ***
/ Launcher >再次点击 Home 键回到桌面,MainActivity 再次进入后台:
1
2/ Laucher >
/ MainActivity > ***点击 App 图标打开 App,Launcher 通过 Manifest 定义的
android.intent.category.LAUNCHER
打开 SplashActivity,由于设置了singleInstance
,所以新建一个栈并在其中打开 SplashActivity:1
2
3/ SplashActivity > ***
/ Launcher >
/ MainActivity >SplashActivity 执行了
finish()
,并且由于栈内所有 Activity 均已销毁,该栈也被清理销毁:1
2/ Launcher >
/ MainActivity >正是这一步将最近任务中的 App 记录清理了。因为默认情况下,一个 App 只在最近任务中记录一个栈,也就是最后活跃的栈,这个栈一旦被销毁,又没有其他栈被激活,则 App 记录就会从最近任务中删除。