在编程范式中,应用是通过 main()
方法启动的,而 Android 系统与此不同,它会调用与其生命周期特定阶段相对应的特定回调方法 来启动 Activity
实例中的代码。
作为最重要的组件,Activity 充当了应用与用户互动的入口点。Activity 提供窗口供应用在其中绘制界面。此窗口通常会填满屏幕,但也可能比屏幕小,并浮动在其他窗口上面。通常,一个 Activity 实现应用中的一个屏幕。
通常,App会指定一个 Activity 为主 Activity,这是用户启动应用时出现的第一个屏幕。然后,每个 Activity 可以启动其他的 Activity,以执行不同的操作,从而形成了页面栈。
Intent 过滤器
Activity 通过 Intent 来启动,Intent 过滤器是 Android 平台的一项非常强大的功能。通过 Intent ,可以显式或隐式请求启动 Activity 。显式请求可以明确指定具体的 Activity ,隐式请求可能会先列举出所有符合条件的 Activity,再经过用户去选择具体用哪一个。
Intent 通过 action (必备)、 category(可选) 和 data(可选)来定义筛选条件。并且可以通过 putExtra(String, Any)
函数来传递一些数据。
putExtra 传递数据
java
private Bundle myExtras;
public Intent putExtra(String name, Object value) {
if (myExtras == null) {
myExtras = new Bundle();
}
myExtras.put(name, value);
return this;
}
putExtra(String, Any)
方法内部是通过将数据存入一个 Bundle
对象中,然后在 Activity 的 onCreate 生命周期中取出来供 Activity 使用。
java
public class Bundle(val mStr: String = "") {
private final Map<String, Object> myMap = new HashMap<String, Object>();
fun containsKey(String key): Boolean {
return myMap.containsKey(key);
}
fun put(String key, Object value) {
myMap.put(key, value);
}
fun <T: Parcelable> getParcelable(key: String): T {
Object o = myMap.get(key)
if (o == null) return null
try {
return (T) o;
} catch (ClassCastException ex) {
return null;
}
}
// ...
}
Bundle
Bundle 是一个 HashMap 的装饰类,并且要求 put 进来的元素是可以序列化的,当传入一个不可序列化的对象时,在编译期间会报错:None of the following functions can be called with the arguments supplied.
kotlin
class Model1()
class Model2(): Serializable
val model1 = Model1()
val model2 = Model2()
activity.startActivity(Intent(activity, BActivity::class.java).apply {
putExtra("A", model1) // error
putExtra("B", model2)
})
Bundle 不适合保留大量数据,因为它需要在主线程上进行序列化处理并占用系统进程内存。
Bundle 传递参数的数据最大是 1M 左右,数据过大会导致 App 崩溃。
生命周期
上面的是官方经典的生命周期流程图,由图中所示,可以看出 Activity 存在三种状态:
- Activity Launched,启动
- Activity Running,运行
- Activity shutdown,关闭
启动过程会经过 onCreate、onStart、onResume ,经过 onResume 后,Activity 处于正常运行状态,直到 onDestroy 执行完成,Activity 关闭。
但是从与用户的交互来看,可以分为三种状态:
- 可见并可以交互:这个一般指 onResume 到 onPause 之间的状态,也是常说的前台状态。
- 可见但不可交互:这个是指 onStart 到 onResume 之间和 onPause 到 onStop 之间的状态。
- 不可见不可交互:这个一般是指 onCreate 到 onStart 及 onStop 到 onDestroy 之间的状态。
用图像表示就是:
与用户交互的状态可以帮助我们来区分 Activity 可能处于的生命周期的状态。例如:在一个 Activity A 上打开了一个透明的新 Activity B,底部的 A 处于可见但不可交互的状态,所以生命周期只会走到 onPause ,不会 onStop。
生命周期方法
-
onCreate
Activity 会在创建后进入"已创建"状态,该方法在 Activity 的整个生命周期中只应发生一次。
-
onStart
Activity 进入"已开始"状态,Activity 对用户可见,此方法中的逻辑为 Activity 进入前台并支持交互做准备。
该方法会非常快速地完成,结束会立刻进入 onResume。
-
onResume
Activity 进入"已恢复"状态,应用会一直保持这种状态,直到某些事件发生,让焦点远离应用。
当发生中断事件时,Activity 进入"已暂停"状态。
-
onPause
Activity 进入"已暂停"状态。从该状态可以回到"已恢复"状态,此方法表示 Activity 不再位于前台(尽管在用户处于多窗口模式时 Activity 仍然可见)。
处于"已暂停"状态时不应继续(或应有节制地继续)的操作,以及您希望很快恢复的操作。
- 在 Android 7.0(API 级别 24)或更高版本中,有多个应用在多窗口模式下运行。无论何时,都只有一个应用(窗口)可以拥有焦点,因此系统会暂停所有其他应用。
- 有新的半透明 Activity(例如对话框)处于开启状态。只要 Activity 仍然部分可见但并未处于焦点之中,它便会一直暂停。
如果 Activity 变为完全不可见,系统会调用
onStop()
。进入"已停止"状态后,Activity 要么返回与用户互动,要么结束运行并消失。如果 Activity 返回,系统将调用
onRestart()
。如果Activity
结束运行,系统将调用onDestroy()
。 -
onStop
Activity 不再对用户可见,说明其已进入"已停止"状态。
-
onRestart
当 Activity 从 onStop 返回与用户互动时,会先调用该周期,并继续调用 onStart,主要是为了在 onStart 之前供用户做一些拦截处理。
-
onDestroy
销毁 Activity 之前,系统会先调用
onDestroy()
。两种情况会导致:- 用户关闭了 Activity 或调用了
finish()
方法。 - 由于配置更改,例如旋转屏幕或多窗口模式,系统暂时销毁 Activity。
- 用户关闭了 Activity 或调用了
经典问题:Activity A 启动 Activity B,两者生命周期是如何进行的?
A 启动 B:
A#onCreate >> A#onStart >> A#onResume >> A#onPause >> B#onCreate >> B#onStart >> B#onResume >> A#onSaveInstance >> A#onStop
A 先不可交互,然后 B开始进入前台,B进入前台后,A变为不可交互不可见。
B 返回 A:
B#onPause >> A#onRestart >>A#onStart >> A#onResume >> B#onStop >> B#onDestroy
Activity 状态和从内存中清除
系统会在需要释放内存时终止进程;系统终止给定进程的可能性取决于当时进程的状态。反之,进程状态取决于在进程中运行的 Activity 的状态。下面的表中展示了进程状态、Activity 状态以及系统终止进程的可能性之间的关系。
系统终止进程的可能性 | 进程状态 | Activity 状态 |
---|---|---|
较小 | 前台(拥有或即将获得焦点) | 已创建 已开始 已恢复 |
较大 | 后台(失去焦点) | |
最大 | 后台(不可见) 空 | 已停止 已销毁 |
保存和恢复瞬时界面状态
用户期望 Activity 的界面状态在整个配置变更(例如旋转或切换到多窗口模式)期间保持不变。但是,默认情况下,系统会在发生此类配置更改时销毁 Activity,从而清除存储在 Activity 实例中的任何界面状态。同样,如果用户暂时从您的应用切换到其他应用,并在稍后返回您的应用,他们也希望界面状态保持不变。但是,当用户离开应用且您的 Activity 停止时,系统可能会销毁该应用的进程。
当 Activity 因系统限制而被销毁时,通过 onSaveInstanceState()
方法,来保存瞬时状态。
当您的 Activity 开始停止时(在执行 onStop 前),系统会调用 onSaveInstanceState()
。
然后,实现在 onStart 方法后执行的 onRestoreInstanceState
方法,而不是在 onCreate 期间恢复状态。
任务与返回栈
任务是用户在执行某项工作时与之互动的一系列 Activity 的集合。这些 Activity 按照每个 Activity 打开的顺序排列在一个返回堆栈中。
Android 7.0(API 级别 24)及更高版本支持多窗口环境,当应用在这种环境中同时运行时,系统会单独管理每个窗口的任务;而每个窗口可能包含多项任务。
这种 push-pop 结构同样适用于 Fragment 。
在当前 Activity 启动另一个 Activity 时,新的 Activity 将被 push 到堆栈顶部并获得焦点。上一个 Activity 仍保留在堆栈中,但会停止。
当 Activity 停止时,系统会保留其界面的当前状态。当用户按返回按钮时,当前 Activity 会从堆栈顶部退出(该 Activity 销毁),上一个 Activity 会恢复(界面会恢复到上一个状态)。
堆栈中的 Activity 永远不会重新排列,只会被送入和退出,在当前 Activity 启动时被送入堆栈,在用户使用返回 按钮离开时从堆栈中退出。因此,返回堆栈按照"后进先出"的对象结构运作。下图借助一个时间轴直观地显示了这种行为。该时间轴显示了 Activity 之间的进展以及每个时间点的当前返回堆栈。
如果用户继续按返回,则堆栈中的 Activity 会逐个退出,以显示前一个 Activity,直到用户返回到主屏幕(或任务开始时运行的 Activity)。移除堆栈中的所有 Activity 后,该任务将不复存在。
这样的栈结构,会导致一个任务的 Activity 序列顺序是固定的,这样一来,同一个 Activity 会多次创建,当然你也可以通过一些设置,来实现回到其中一个或者是重启一个新的任务栈的功能。
管理任务栈
可以通过一些属性来控制 Activity 在任务栈中的行为,包括:
taskAffinity
launchMode
allowTaskReparenting
clearTaskOnLaunch
alwaysRetainTaskState
finishOnTaskLaunch
主要的 Intent 的 Flag 有:
FLAG_ACTIVITY_NEW_TASK
:在新任务中启动 Activity。如果您现在启动的 Activity 已经有任务在运行,则系统会将该任务转到前台并恢复其最后的状态,而 Activity 将在onNewIntent()
中收到新的 intent。FLAG_ACTIVITY_CLEAR_TOP
:如果要启动的 Activity 已经在当前任务中运行,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过onNewIntent()
将此 intent 传送给它的已恢复实例(现在位于堆栈顶部)。FLAG_ACTIVITY_SINGLE_TOP
:如果要启动的 Activity 是当前 Activity(即位于返回堆栈顶部的 Activity),则现有实例会收到对onNewIntent()
的调用,而不会创建 Activity 的新实例。
启动模式
launchMode 有四种属性值:
-
"standard"
(默认模式)系统在启动该 Activity 的任务中创建 Activity 的新实例,并将 intent 传送给该实例。Activity 可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例。
-
"singleTop"
如果当前任务的顶部已存在 Activity 的实例,则系统会通过调用其
onNewIntent()
方法来将 intent 转送给该实例,而不是创建 Activity 的新实例。Activity 可以多次实例化,每个实例可以属于不同的任务,一个任务可以拥有多个实例(但前提是返回堆栈顶部的 Activity 不是该 Activity 的现有实例)。
需要注意的是,如果 Activity 不在栈顶,那么就会创建新的 Activity 实例;如果在栈顶,才会调用其
onNewIntent()
方法将 intent 交给栈顶的对象来处理。 -
"singleTask"
系统会创建新任务,并实例化新任务的根 Activity。
但是,如果另外的任务中已存在该 Activity 的实例,则系统会通过调用其
onNewIntent()
方法将 intent 转送到该现有实例,而不是创建新实例。Activity 一次只能有一个实例存在。虽然 Activity 在新任务中启动,但用户按返回按钮仍会返回到上一个 Activity。
-
"singleInstance"
与
"singleTask"
相似,唯一不同的是系统不会将任何其他 Activity 启动到包含该实例的任务中。该 Activity 始终是其任务唯一的成员;由该 Activity 启动的任何 Activity 都会在其他的任务中打开。
无论 Activity 是在新任务中启动的,还是在和启动它的 Activity 相同的任务中启动,用户按返回 按钮都会回到上一个 Activity。但是,如果您启动了指定 singleTask
启动模式的 Activity,而后台任务中已存在该 Activity 的实例,则系统会将该后台任务整个转到前台运行。此时,返回堆栈包含了转到前台的任务中的所有 Activity,这些 Activity 都位于堆栈的顶部。
taskAffinity 亲和性
taskAffinity 表示 Activity 倾向于属于哪个任务。taskAffinity 属性采用字符串值,该值必须不同于 manifest 元素中声明的默认软件包名称,因为系统使用该名称来标识应用的默认任务亲和性。taskAffinity 实际上就是用来给 Activity 指定任务的标签。但是需要配合特殊场景使用。
taskAffinity 可在两种情况下发挥作用:
-
当启动 Activity 的 intent 包含
FLAG_ACTIVITY_NEW_TASK
标记时如果传递给
startActivity()
的 intent 包含FLAG_ACTIVITY_NEW_TASK
标记,则系统会寻找其他任务来容纳新 Activity。通常会是一个新任务,但也可能不是。如果已存在与新 Activity 具有相同 taskAffinity 的现有任务,则会将 Activity 启动到该任务中。如果不存在,则会启动一个新任务。 -
当 Activity 的
allowTaskReparenting
属性设为"true"
时在这种情况下,一旦和 Activity 有相同 taskAffinity 的任务进入前台运行,Activity 就可从其启动的任务转移到该任务。
使用场景
如果一个 APK 文件中包含了就用户角度而言的多个"应用",(一个 App 安装后会在 launcher 中存在多个图标)您可能需要使用 taskAffinity
属性为每个"应用"所关联的 Activity 指定不同的亲和性。
清除返回栈
如果用户离开任务较长时间,系统会清除任务中除根 Activity 以外的所有 Activity。当用户再次返回到该任务时,只有根 Activity 会恢复。系统之所以采取这种行为方式是因为,经过一段时间后,用户可能已经放弃了之前执行的操作,现在返回任务是为了开始某项新的操作。
您可以使用一些 Activity 属性来修改此行为:
-
alwaysRetainTaskState
如果在任务的根 Activity 中将该属性设为
"true"
,则不会发生上述默认行为。即使经过很长一段时间后,任务仍会在其堆栈中保留所有 Activity。 -
clearTaskOnLaunch
如果在任务的根 Activity 中将该属性设为
"true"
,那么只要用户离开任务再返回,堆栈就会被清除到只剩根 Activity。也就是说,它与alwaysRetainTaskState
正好相反。用户始终会返回到任务的初始状态,即便只是短暂离开任务也是如此。 -
finishOnTaskLaunch
该属性与
clearTaskOnLaunch
类似,但它只会作用于单个 Activity 而非整个任务。它还可导致任何 Activity 消失,包括根 Activity。如果将该属性设为"true"
,则 Activity 仅在当前会话中归属于任务。如果用户离开任务再返回,则该任务将不再存在。
Activity 的启动流程
- 当应用程序调用 startActivity() 方法时,它会创建一个 Intent 对象,并将其传递给 AMS。
- AMS 接收到 Intent 后,会根据 Intent 的内容查找能够处理该 Intent 的所有 Activity,并确定要启动哪个 Activity。
- AMS 会为要启动的 Activity 创建一个 ActivityRecord 对象,用于表示 Activity 的状态和相关信息。
- AMS 会检查要启动的 Activity 是否已经在运行。如果该 Activity 已经在运行,则 AMS 会将其带到前台;否则,AMS 会启动一个新的进程来运行该 Activity。
- AMS 会向 ZygoteServer 进程的 socket 发送一个消息,请求 fork 一个应用程序进程。
- 如果 AMS 启动了一个新的进程,则该进程会创建一个新的 Application 对象,并调用其 onCreate() 方法来初始化应用程序。
- 应用程序进程会创建一个新的 Activity 实例,并调用其 onCreate() 方法来初始化 Activity。
- 应用程序进程会调用 Activity 的 onStart()、onResume() 等方法,以便将 Activity 置于前台并显示给用户。
- WMS 会与应用程序进程协作,为 Activity 创建一个窗口并将其显示在屏幕上。
配置多应用入口和任务栈
在应用的 AndroidManifest.xml
文件中为不同的 Activity 添加 intent-filter
,以便在 Launcher 中为它们创建不同的入口。例如,假设您有两个 Activity:A 和 B。如果您希望它们在 Launcher 中有不同的入口,则可以这样配置:
xml
<activity android:name=".ActivityA">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ActivityB">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
在这个例子中,我们为 Activity A 和 Activity B 都添加了一个 intent-filter
,指定了它们都可以作为应用的入口。intent-filter
中的 action
和 category
元素指定了这些 Activity 可以响应 MAIN
操作和 LAUNCHER
类别。
当您安装应用后,Launcher 会根据这些配置为 Activity A 和 Activity B 创建两个不同的入口。用户可以点击其中一个入口来启动相应的 Activity。
总之,您可以在应用的 AndroidManifest.xml
文件中为不同的 Activity 添加 intent-filter
,以便在 Launcher 中为它们创建不同的入口。
您可以在应用的 AndroidManifest.xml
文件中为每个 Activity 配置不同的图标和标签,以便在 Launcher 中为它们创建不同的入口。例如,假设您有两个 Activity:A 和 B。如果您希望它们在 Launcher 中有不同的入口,则可以这样配置:
xml
<activity android:name=".ActivityA"
android:label="@string/activity_a_label"
android:icon="@drawable/activity_a_icon">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ActivityB"
android:label="@string/activity_b_label"
android:icon="@drawable/activity_b_icon">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
在这个例子中,我们为 Activity A 和 Activity B 都配置了 android:label
和 android:icon
属性,分别指定了它们在 Launcher 中显示的标签和图标。此外,我们还为它们都添加了一个 intent-filter
,指定了它们都可以作为应用的入口。
当您安装应用后,Launcher 会根据这些配置为 Activity A 和 Activity B 创建两个不同的入口。用户可以点击其中一个入口来启动相应的 Activity。
总之,您可以在应用的 AndroidManifest.xml
文件中为每个 Activity 配置不同的图标和标签,并添加 intent-filter
来指定它们都可以作为应用的入口。这样,在 Launcher 中就会为它们创建不同的入口。
使用 adb shell dumpsys activity
命令来查看设备上所有任务的详细信息。