基本概念
Activity
用来做界面显示,交互,也是程序的入口。
生命周期
生命周期流转图
注意: 在实现生命周期方法时必须始终先调用超类实现,然后再执行其他操作。
1 |
|
生命周期回调方法汇总表
- 生命周期回调方法汇总表
onPause
和onStop
的区别onPause
:失去焦点,不可交互onStop
:完全被遮挡,Activity
不可见
示例:Activity
在弹出 Dialog
时,只是失去焦点,并没有完全不可见,所以只会执行 onPause
;而 Activity
在启动另一个 Activity
时被完全遮挡,就会执行到 onStop
A
启动 B
生命周期的执行顺序
1 | // 启动A |
可以清楚的看到在 A
启动 B
的流程中,onPause
在失去焦点后就立即执行,当 B
完全启动后并遮挡了 A
,onStop
才开始执行。同理,B
在退出过程中,等 A
完全恢复并遮挡 B
后,B
才开始执行 onStop -> onDestroy
。onStop -> onRestart
这个流程出现在:Activity
被遮挡但没有销毁,下次恢复时就会执行 onRestart -> onStart
。
finish()
对生命周期的影响
- 在
onCreate
中调用,执行顺序onCreate -> onDestroy
- 在
onStart
中调用,执行顺序onCreate -> onStart -> onStop -> onDestroy
Activity
重建的影响因素
如下因素的变化,都会导致 Activity
重建,也就是会走 onDestroy -> onCreate
:
- mcc
国际移动用户识别码所属国家代号是改变了,sim
被侦测到了,去更新mcc
。mcc
是移动用户所属国家代号 - mnc
MNC
是移动网号码,最多由两位数字组成,用于识别移动用户所归属的移动通信网 - locale
地址改变了,用户选择了一个新的语言会显示出来 - touchscreen
触摸屏是改变了,通常是不会发生的 - keyboard
键盘发生了改变,例如用户用了外部的键盘 - keyboardHidden
键盘的可用性发生了改变 - navigation
导航发生了变化,通常也不会发生 - screenLayout
屏幕的显示发生了变化,不同的显示被激活 - fontScale
字体比例发生了变化,选择了不同的全局字体 - uiMode
用户的模式发生了变化 - orientation
屏幕方向改变了 - screenSize
屏幕大小改变了 - smallestScreenSize
屏幕的物理大小改变了,如:连接到一个外部的屏幕上
这些影响因素可以在 AndroidManifest.xml
中设置来屏蔽掉,凡是在这申明后,将不会影响生命周期。
1 | <activity android:name=".fourcomponents.ActivityLifeCycleA" |
屏幕旋转对生命周期的影响
- 常规流程
Activity
销毁重建
屏幕旋转会完整的触发这两个生命周期,其中状态恢复在onCreate
和onRestoreInstanceState
中都可以实现
1 | TAG:: onCreate |
- 避免
Activity
销毁重建
在屏幕旋转时,避免Activity
销毁重建,可以在AndroidManifest.xml
中设置对应属性(Android 3.2
及以上需要设置screenSize
):
1 | <activity android:name=".fourcomponents.ActivityLifeCycleA" |
设置后屏幕旋转将完全不影响 Activity
的生命周期,如果想监听屏幕旋转,可以重写如下方法:
1 |
|
“锁屏/解锁”对生命周期的影响
“锁屏/解锁” 整个流程包含:锁屏,亮屏,解锁三个步骤:
不涉及横竖屏切换
如:当前 Activity
为竖屏,在竖屏状态下,锁屏并解锁,不做方向切换,即正常流程:
锁屏:onPause -> onStop
解锁:onRestart -> onStart -> onResume
涉及横竖屏切换
如:当前 Activity
为横屏,在横屏状态下锁屏会显示锁屏界面,涉及方向切换,当前 Activity
会被销毁重建。
锁屏:onPause -> onStop -> onDestroy -> onCreate -> onStart -> onResume -> onPause
- 解锁后当前
Activity
变为竖屏
解锁:onResume
- 解锁后当前
Activity
仍然转换为横屏
即会出现当前Activity
销毁重建过程
解锁:onResume -> onPause -> onStop -> onDestroy -> onCreate -> onStart -> onResume
横竖屏切换监听屏幕旋转
如:当前 Activity
为横屏,并且设置了属性 android:configChanges="orientation|screenSize"
。即当前 Activity
不会销毁重建,方向切换时会被监听。
锁屏:onPause -> onStop -> onConfigurationChanged
,此时当前 Activity
已经被切换到竖屏。
解锁:onRestart -> onStart -> onResume -> onConfigurationChanged
小结
- 屏幕旋转
如果涉及屏幕旋转,当前Activity
在“锁屏/解锁”两个阶段都会出现销毁重建(onDestroy -> onCreate
),如果此时监听了屏幕旋转,则在方向切换时仅执行onConfigurationChanged
。 - 锁屏
总体来说会先执行onPause -> onStop
,即不可见流程。如果屏幕旋转当前Activity
重建,会执行到onPause
,同时解锁会直接从onResume
开始;如果监听了旋转,则屏幕切换时仅会执行onConfigurationChanged
。 - 亮屏
不影响生命周期 - 解锁
总体来说会先执行onRestart --> onResume
,即恢复可见流程。如果当前Activity
重建过,则直接从onResume
恢复;如果监听了旋转,则屏幕切换时仅会执行onConfigurationChanged
。
?为什么锁屏时,如果出现
Activity
销毁重建时,只执行到onPause
,而不执行onStop
?
Home
键对生命周期的影响
按下 Home
键不影响生命周期,正常的不可见和恢复可见流程:
不可见流程:onPause -> onStop
恢复可见流程:onRestart -> onStart -> onResume
状态栏下拉对生命周期的影响
下拉状态栏不影响生命周期,但是如果需要监听状态栏下拉,可以重写如下方法:
1 |
|
保存 Activity
的状态
状态保存和恢复
涉及到两个函数:onSaveInstanceState()
和 onRestoreInstanceState()
,这两个函数大部分情况下并不会成对出现。
如果没有重写这两个函数,系统 Activity
类的 onSaveInstanceState()
默认实现也会恢复部分 Activity
状态。具体地讲,默认实现会为布局中的每个 View
调用相应的 onSaveInstanceState()
方法,让每个视图都能提供有关自身的应保存信息。Android
框架中几乎每个小部件都会根据需要实现此方法,以便在重建 Activity
时自动保存和恢复对 UI
所做的任何可见更改。例如 EditText
小部件保存用户输入的任何文本,CheckBox
小部件保存复选框的选中或未选中状态。
注意:想要保存其状态的每个小部件,必须提供一个唯一的
ID
(通过android:id
属性)。如果小部件没有ID
,则系统无法保存其状态;如果是相同的ID
,数据会被覆盖,在恢复时数据显示出错。
保存的数据
数据通过 Bundle
保存起来,使用 putString()
和 putInt()
等方法以[名称-值]对形式保存有关 Activity
状态的瞬时数据,如成员变量,UI
状态等。如果需要保存持久数据,需要在 onPause
中保存。
onSaveInstanceState()
触发时机
在 Activity
不可见时触发,也就是 onPause --> onSaveInstanceState --> onStop
,但是系统并不是每次都会调用。
调用
遵循一个重要原则,即当系统存在“未经你许可”销毁 Activity
的可能时,onSaveInstanceState
会被系统调用,这是系统的责任:必须要提供一个机会让你保存你的数据。
- 启动另外一个
Activity
并被遮挡 - 屏幕旋转时当前
Activity
销毁并被重新创建 Home
键相关操作- 按下电源键灭屏
不调用
- 直接退出当前
Activity
- 被
kill
命令杀掉
onRestoreInstanceState()
触发时机
在 Activity
恢复可见时触发,也就是 onStart --> onRestoreInstanceState --> onResume
,同样系统也不是每次都会调用。
调用
重要原则,即当 Activity
确实已经被销毁。
- 屏幕旋转时当前
Activity
销毁并被重新创建
不调用
- 当
Activity
并没有销毁而只是进入生命周期恢复过程中,并不会被调用,直接走流程onRestart --> onStart --> onResume
- 被
kill
命令杀掉
示例
可以参考:“屏幕旋转对生命周期的影响”中“常规流程 Activity
销毁重建”的 log
打印。
Activity
存储方式:栈
基本概念
- 返回栈(Back Stack)
返回栈: 这些Activity
按照各自的打开顺序排列在栈中,表示Activity
的存储方式。返回栈以“后进先出”对象结构运行,并且栈中的Activity
永远不会重新排列,仅推入和弹出栈。由当前Activity
启动时推入堆栈;用户使用“返回”按钮退出时弹出堆栈。
源码中查看推测是如下结构来存储返回栈的,但是似乎不像是栈的操作?
1 | // TaskRecord.java中定义,暂时没有看出来是栈存储结构? |
- 任务(Task)
任务:在执行特定作业时与用户交互的一系列Activity
,使用返回栈记录这些Activity
的执行顺序。任务分为前台任务和后台任务,前台任务接受用户交互;任务被移动到后台时,整个返回栈顺序不变。任务可以是同一个应用程序中的Activity
,也可以是跨应用的Activity
,放到同一个任务中,以维护这种无缝的用户体验。
源码中通过如下方式来记录前后台任务列表,每个任务对应一个 TaskRecord
:
1 | // ActivityStatck.java |
前后台任务切换时,仅仅把需要的后台任务移动到最前面,其他任务顺序不变:
1 | // ActivityStatck::moveTaskToFrontLocked方法中 |
示例
- 命令
adb shell dumpsys activity
,通过该命令可以显示所有Activity
的任务及存储信息,对应源码为:
1 | // 1. ActivityManagerService.java |
- 任务只包含同一个应用的
Activity
1 | Display #0 (activities from top to bottom): |
由于返回栈中的 Activity
永远不会重新排列,因此如果应用启动特定 Activity
,则会创建该 Activity
的新实例并推入堆栈中(而不是将 Activity
的任一先前实例置于顶部),因此应用中的一个 Activity
可能会多次实例化。如上面 Log
显示,A
和 B
有多次实例化。
- 任务包含不同应用的
Activity
1 | // 1. ActivityManagerService::dumpActivitiesLocked打印 |
使用清单文件定义启动模式
在清单文件中声明 Activity
时,可以使用 android:launchMode
属性指定 Activity
应该如何与任务关联,默认为 android:launchMode="standard"
,launchMode
属性的启动模式共有四种:
standard
系统默认,Activity
会被多次实例化。
singleTop
如果当前任务的顶部已存在该 Activity
的一个实例,则系统不会创建 Activity
的新实例;如果不在顶部,则和 standard
一样,创建多个实例。假设任务的返回栈顺序是 A-B-C-D
,并且 D
位于顶部,再启动 D
时,示例对比:
- 如果
D
为standard
模式,将会新建D的实例,返回栈为A-B-C-D-D
- 如果
D
为singleTop
模式,不会创建D的实例,返回栈还是A-B-C-D
singleTask
系统根据 taskAffinity
来决定是否开启新任务,taskAffinity
如果不设置,默认值为包名。已有的任务中如果已经存在该 Activity
实例,下次调用时会把该任务的返回栈中位于这个实例上面所有的 Activity
全部结束掉,即最终这个 Activity
实例会位于该任务的堆栈顶端中。
taskAffinity
为默认值或包名,在原任务中装载该Activity
。如下示例:
1 | // 任务及返回栈信息 |
对应的生命周期流程:B:: onPause -> A:: onNewIntent -> A:: onRestart -> A:: onStart -> A:: onResume -> B:: onStop -> B:: onDestroy
taskAffinity
不是包名,开启新的任务并加载该Activity
到底部,后续新开启的Activity
也会在这个新任务中。
1 | // 启动流程仍然是MainActivity --> A --> B |
singleInstance
系统会启动一个新的任务来装载该 Activity
,并且该 Activity
始终是其任务唯一仅有的成员,也就是 Activity
和任务是一一对应的。
1 | // 启动顺序 MainActivity --> ActivityLifeCycleA --> ActivityLifeCycleB |
注意:
进入顺序为MainActivity --> ActivityLifeCycleA --> ActivityLifeCycleB
但是退出顺序为ActivityLifeCycleB --> MainActivity --> ActivityLifeCycleA
从 Log
中可以看出,MainActivity
和 ActivityLifeCycleB
是在同一个任务中的返回栈中,所以按返回键会先退出 ActivityLifeCycleB
,然后按照栈的顺序进入 MainActivity
,再次按返回键退出 MainActivity
后,前台任务完全退出,进入 ActivityLifeCycleA
所在的任务。
总结
- 实例数,
standard
标准模式会开启多个实例,singleTop
有条件单个实例,singleTask
和singleInstance
都是单个实例(单例模式)。 taskAffinity
决定了singleTask
是否开启新的任务
启动模式引入新的方法 onNewIntent
定义
protected void onNewIntent(Intent intent) {}
如果 Activity
已经实例化但需要重新加载时,会先执行 onNewIntent
,然后再执行生命周期函数。
示例
singleTop
中,如果B
已经在栈顶,执行顺序B:: onPause --> B:: onNewIntent --> B:: onResume
singleTask
和singleInstance
中,如果A
已经存在于任务中,则再次启动A
时,执行顺序A:: onNewIntent --> A:: onRestart --> A:: onStart --> A:: onResume
注意事项
重写 onNewIntent
,必须调用 setIntent
来更新 intent
,否则默认 getIntent
还是拿到之前的 intent
。参考源码注释:
1 | * <p>Note that {@link #getIntent} still returns the original Intent. You |
使用 Intent
标志定义启动模式
启动 Activity
时,可以通过在传递给 startActivity()
的 Intent
中加入相应的标志,修改 Activity
与其任务的默认关联方式。
注意:
Intent
中设置的标志优先级高于Activity
在清单文件中的定义
可用于修改默认行为的标志包括:
- FLAG_ACTIVITY_NEW_TASK
如果taskAffinity
的任务存在,则不用新开任务,不管该Activity
是否存在都会创建新实例。如果不存在则新开任务,比如在BroadcastReceiver
中开启Activity
就需要使用该标志 - FLAG_ACTIVITY_SINGLE_TOP
与singleTop
启动模式有相同的行为 - FLAG_ACTIVITY_CLEAR_TOP
如果Activity
已在当前任务中运行,则会销毁当前任务返回栈中顶部的所有Activity
,直到该Activity
位于栈顶
注意事项
- 屏幕旋转,需要有平滑的用户体验
- 用户数据在
Activity
跳转时不能丢失 - 特定场景下进程可能会被杀掉·