Activity 四种启动模式分析

前记

我们已经知道,一个任务(Task)是由多个 activities 组成的,这个任务叫做任务栈或者返回栈(Back Stack),是一种「后入先出」的数据结构。在默认情况下,当我们多次启动同一个 Activity 的时候,系统会创建多个实例并把它们一一放入任务栈中,我们可以想到这是一种耗时费力的做法。所以,Android 的设计人员给我们提供了其他的启动模式方便我们修改系统默认的行为,使得我们可以合理地设置 Activity 的启动模式让程序运行得更有效率、用户体验更好。


Intent 标志位启动模式(了解)

除了上述四种在配置文件中为 Activity 设置启动模式,我们还可以通过在 Intent 中设置标志位来为 Activity 指定启动模式。这两种方式都可以为 Activity 指定启动模式,但是二者还是有区别的。首先,优先级上,后者的优先级要高于第一种,当两种同时存在时,以第二种方式为准;其次,二者在限定范围上也有所不同。比如,前者无法直接为 Activity 设定FLAG_ACTIVITY_CLEAR_TOP标识,而后者无法为 Activity 指定 singleInstance 模式。

标记位的作用很多,有的标记位可以设定 Activity 的启动模式,比如:FLAG_ACTIVITY_NEW_TASKFLAG_ACTIVITY_SINGLE_TOP等;有的标记位可以影响 Activity 的运行状态,比如:FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NO_HISTORYFLAG_ACTIVITY_EXCLUDE_FROM_RECENTS等。大部分情况下,我们不需要为 Activity 指定标记位,因此,对于标记位理解即可。在使用标记位的时候,要注意有些标记位是系统内部使用的,应用程序不需要去手动设置这些标记位以防出现问题。


直截了当清空任务栈(了解)

系统同样提供了清空任务栈的方法来让我们将一个 Task 全部清除。通常清空下,可以在配置文件中的标签中使用一下几种属性来清理任务栈:

  • clearTaskOnLaunch:顾名思义,每次返回该 Activity 时,都将该 Activity 之上的所有 Activity 都清除。通过这个属性,可以让这个 Task 每次在初始化的时候都只有这一个 Activity 实例。
  • finishOnTaskLaunch:当用户离开这个 Activity 所处的 Task,那么再返回时,该 Activity 会被干掉。
  • alwaysRetainTaskState:该属性给了 Task 一道免死金牌,如果将 Activity 的这个属性设置为 True,那么该 Activity 所在的 Task 将不接受任何清理命令,一直保持当前的 Task 状态。

四种启动模式分析(掌握)

standard 启动模式

顾名思义,standard 启动模式又叫标准模式,这也是系统默认的 Activity 启动模式。每次启动一个 Activity 都会创建一个新的实例并处于栈顶,不管这个实例是否已经存在。如下图所示:
标准启动模式

这种启动模式的缺点是:如果一个 Activity 以 standard 模式启动,那么将会使它所依附的应用消耗更多的系统资源。

singleTop 启动模式

这种模式又叫栈顶复用模式。如果指定启动 Activity 为该模式启动,那么在启动时,系统会判断当前栈顶 Activity 是不是要启动的 Activity 。如果不是则创建新的 Activity;如果是则不创建新的 Activity 而直接引用这个 Activity 实例。并且会调用该实例的onNewIntent()方法将 Intent 对象传递到这个实例中进而我们可以取出当前请求的信息,需要注意的是,这个 Activity 的 onCreate、onStart 方法不会被系统调用,因为它并没有发生改变。这种启动模式通常适用于接收到消息后显示的页面,例如 QQ 接收到消息后弹出 Activity,如果一次来10条消息,总不能一次弹出10个页面,这样就太不人性化了。

singleTask 启动模式

又称栈内复用模式,这种启动模式比较常用。如果一个 Activity 设置了该启动模式,那么在一个任务栈中只能有一个该活动的实例。如果任务栈中还没有该 Activity,会新创建一个实例并放在栈顶。如果已经存在 Activity ,系统会销毁处在该 Activity 上的所有 Activity 实例,最终让该活动实例处于栈顶,同时回调该 Activity 的onNewIntent()方法。需要注意的是,这里是指在同一个 App 中启动这个 singleTask 的 Activity,如果是其他程序以该模式启动这个 Activity,那么它将创建一个新的任务栈,如果此 Activity 已经在后台一个任务栈中,那么启动后,后台的这个任务栈将一起呗切换到前台。该过程可以用官网上的一幅图来说明:
singleTask 启动模式

也就是说,使用这个模式创建的 Activity 不是在新的任务栈中被打开,就是将已打开的 Activity 切换到前台,所以这种启动模式通常可以用来退出整个应用

singleInstance 启动模式

又称单实例模式,这是一种加强的 singleTask 模式,具有此种模式的 Activity 只能单独地位于一个新的任务栈中,而且该任务栈中只存在这一个 Activity,可以使多个应用共享该唯一的 Activity 实例。也就是说被该实例启动的其他 Activity 会自动运行于另一个任务中,当再次启动该实例时,会重用已存在的任务和实例,并且会调用该实例的onNewIntent()方法,将 Intent 实例传递到该实例中。这种启动模式通常用于需要与程序分离的页面


Activity 的最佳实践(进阶)

知晓当前界面对应 Activity 是哪一个

在实际开发中我们常会遇到这样的窘况:我们需要在某个页面上修改一些很简单的东西,但可能花费半天时间也找不到这个界面对应的活动是哪一个。这时我们的需求是:如何根据当前程序的界面就能判断出这是哪一个活动?

我们的方案是:让这些界面对应的 Activity 都继承自同一个 BaseActivity(此活动需继承 Activity 或其子类以使其他所有界面仍完全继承了 Activity 中所有特性),并在这个活动的onCreate()方法中使用日志 API,比如:Log.d("BaseActivity", getClass().getSimpleName())。这样的话,每当我们进入到一个活动的界面,该活动的类名就会被打印出来,这样我们就可以时刻知晓当前界面对应的是哪一个活动了。代码入下:

1
2
3
4
5
6
7
8
public class BaseActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d("BaseActivity", getClass().getSimpleName());
}
}

简单粗暴有效地退出程序

有时我们还有这样的需求:想随心所欲地方便地退出程序,而不是多次点击返回按钮或者简单地按 Home 键仅仅将程序挂起。解决思路是:只需要一个集合类对所有的活动进行管理就可以了

  • 新建一个 ActivityCollector 类作为活动管理器,在其中我们通过一个 List 来暂存活动,然后提供一个addActivity()方法用于向 List 中添加一个活动,提供一个removeActivity()方法用于从 List 中移除活动,最后提供一个 finishAll()方法用于将 List 中存储的活动全都销毁掉。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public class ActivityCollector {

    public static List<Activity> activities = new ArrayList<Activity> ();

    public static void addActivity(Activity activity) {
    activities.add(activity);
    }

    public static void removeActivity(Activity activity) {
    activities.remove(activity);
    }

    public static void finishAll() {
    for (Activity activity : activities) {
    if (!activity.isFinishing()) {
    activity.finish();
    }
    }
    }

    }
  • 然后修改BaseActivity中的代码,在 BaseActivity 的onCreate()方法中调用 ActivityCollectoraddActivity()方法,表明将当前正在创建的活动添加到活动管理器中。然后在 BaseActivity中重写onDestroy()方法,并调用ActivityCollectorremoveActivity()方法,表明将一个马上要销毁的活动从活动管理器里移除。代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class BaseActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("BaseActivity", getClass().getSimpleName());
    ActivityCollector.addActivity(this);
    }

    @Override
    protected void onDestroy() {
    super.onDestroy();
    ActivityCollector.removeActivity(this);
    }
    }
  • 之后,不管我们想在哪个活动界面直接退出程序,只需在按钮的事件监听器里调用ActivityCollector.finishAll()方法就可以了。当然我们还可以在销毁所有活动的代码后面再加上杀掉当前进程的代码以保证程序完全退出。这里用到了委托的思想,或者是代理模式。

启动一个 Activity 的最佳写法

通常情况下,我们启动一个活动的代码都是这样写的:

1
2
3
4
Intent intent = new Intent(FirstActivity.this, SecondActivity.class);
intent.putExtra("param1", "data1");
intent.putExtra("param2", "data2");
startActivity(intent);

不管是从语法上还是规范上这样写都是完全正确的,但是在实际项目的开发中,我们常会遇到这样的情况:比如 SecondActivity 并不是我们开发的,但现在我们负责的部分需要有启动 SecondActivity 这个功能,但我们却不清楚启动这个活动需要传递哪些数据,这时候我们要么自己去阅读要启动的活动的代码,要么去询问负责编写这个活动的同事。实际上,这两种方法都很低效。那么我们的解决思路是:修改要启动的活动的代码,使要启动的活动所需要的数据全都在方法参数中体现出来,一目了然。代码如下:

1
2
3
4
5
6
7
8
9
public class SecondActivity extends BaseActivity {

public static void actionStart(Context context, String data1, String data2) {
Intent intent = new Intent(context, SecondActivity.class);
intent.putExtra("param1", data1);
intent.putExtra("param2", data2);
context.startActivity(intent);
}
}
1
2
3
4
5
6
button1.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
SecondAction.actionStart(FirstActivity.this, "data1", "data2");
}
});

养成给编写的每个活动都添加类似的启动方法的好习惯,可以让启动变得相对简单,同时还可以节省同事询问的时间,何乐不为。


后记

我们使用 Activity 任务栈的各种启动模式和清理方法,是为了更好地使用 App 中的 Activity,合理地设置 Activity 的启动模式会让程序运行更有效率,用户体验更好。但不能滥用,否则可能会导致整个应用程序的栈管理的混乱,不仅不利于以后的程序的扩展,而且容易出现由于任务栈导致的显示异常,这样的 Bug 是很难调试的,所以要谨慎应用程序的栈管理。