7.2 深入探索Activity从创建到销毁的过程

尽管窗口(Activity)的创建及显示简单地调用startActivity方法即可实现,销毁也可通过调用finish方法或按回退键实现。但这一过程却蕴藏着玄机。本节将为读者揭示窗口从创建到销毁这一过程中经历了哪些环节。通过本节的学习,会使读者可以完全驾驭窗口,对窗口的各种行为了如指掌。

7.2.1 任务(Task)与回退栈(Back Stack)

通常一个Android应用会包含多个窗口,而一般在程序启动时会先显示一个主窗口,其他窗口会通过不同的途径显示,例如,单击按钮弹出窗口;在广播接收器中显示窗口。当程序中的某一个窗口执行当前窗口对象的finish方法或按Android设备的回退键时,当前窗口会关闭,与其对应的窗口对象会被销毁(执行窗口对象的onDestroy方法)。读者可能在5.2节中关于Activity生命周期的描述中已经很熟悉这一过程了,这就是个标准的Activity生命周期过程。但在5.2节并没有介绍在这一过程中到底发生了什么,系统底层是如何处理Activity生命周期的。本节将为读者逐渐解开这些谜团。

Activity的生命周期离不开两个概念:任务与回退栈。每一个Android应用在运行时都会创建和维护一个属于自己的任务。一个任务包含了一个堆栈数据结构,该堆栈用于保存当前Android应用中所有已经创建的窗口对象由于系统不仅要创建窗口对象、显示窗口,还要维护这个回退栈。这也是在Android应用中只允许使用Intent和startActivity方法显示窗口,而不允许直接访问窗口对象的主要原因。。该堆栈称为回退栈(Back Stack)。位于回退栈栈顶的窗口会处于焦点状态。当Android应用开始运行时,系统为该应用建立任务和回退栈后,会将主窗口对象添加到该回退栈中,作为回退栈的第一个元素。当显示新的窗口时,会首先将该窗口的对象入栈。那么该窗口对象就位于当前程序的回退栈的栈顶,而位于栈顶的窗口会显示在最前面(获得焦点,可以与用户交互)。当关闭该窗口,系统会首先将该窗口对象出栈,并销毁该窗口对象。从前面的描述可以看出,窗口的显示和关闭就是一个入栈和出栈的过程,图7-14形象地描述了这一过程。

 

▲图7-14 窗口对象入栈(创建)和出栈(销毁)的过程

图7-14向我们展现了这样一幅情景。一开始回退栈中只有一个窗口对象(Activity 1,主窗口),当显示Activity 2时,系统会将Activity 2入栈,这时Activity 1位于Activity 2的下方。Activity 1的表现是失去焦点(调用onPause方法),并停止(调用onStop)。系统会为Activity 1保存所有的状态。这时Activity 2已经处于栈顶的位置,该窗口获得了焦点。接下来显示Activity 3,系统对回退栈的操作过程与显示Activity 2时类似,然后会关闭Activity 3。这时系统会将Activity 3出栈,并销毁Activity 3,而Activity2又重新回到了栈顶的位置,这也是为什么当Activity 3关闭后Activity 2能获得焦点的原因。当然,这一过程还可以继续,关闭Activity 2和关闭Activity 3在回退栈中的操作类似。当关闭Activity 1时,由于Activity 1是回退栈中最后一个元素,当该窗口出栈后,回退栈为空,这时回退栈被销毁,Android应用终止由于Android系统的机制,程序并没有完全退出内存,有一部分资源还会在内存中停留很长时间,如静态变量。。也就是说Android应用的启动伴随着回退栈的创建和主窗口入栈的过程,程序的终止则伴随着回退栈的清空和销毁的过程。

Android应用不仅仅可以显示程序内部的窗口,而且可以显示其他应用程序的窗口。但不管是显示内部的窗口,还是显示其他应用程序的窗口,都会将显示的窗口对象压入当前应用程序的回退栈。当关闭窗口时,会将窗口对象从回退栈弹出。

还有另外一种显示其他应用程序窗口的情况,就是正在使用当前程序的时候突然弹出了其他应用程序的窗口(并不是当前程序主动调用的),例如,正在使用微博客户端时突然来了一个电话,这时会弹出来电窗口。这一过程并不是将这个来电窗口添加到当前应用程序的回退栈,而是任务的切换。

前面讲过,每一个Android应用在启动的时候都会建立一个任务,回退栈就包含在这个任务中。当进行应用程序之间的切换时,也就是应用程序对应的任务之间的切换。这里的切换并不是指任务的创建和销毁,而是指将任务移到前台(foreground)或后台(background)运行。只有在前台运行的任务中的回退栈栈顶的窗口才能获得焦点。而同时只能有一个任务在前台运行,所以同时只能有一个窗口获得焦点。图7-15描述了任务之间的切换过程。在任务A中有两个窗口(Activity Z和Activity Y),任务B中也有两个窗口(Activity Y和Activity X),由于任务A当前正处于前台,所以位于栈顶的Activity Z处于焦点状态。而任务B的栈顶窗口已停止(状态由系统保存并维护)。当任务A切换到任务B时,任务B被移到前台运行,而任务A成为了后台任务,这时Activity Z会被停止(调用onStop方法)。

回退栈并不会将已经存在于栈中的窗口对象移动到栈的其他位置,而只会执行窗口对象的入栈和出栈的操作。因此在一个回退栈中可能会出现同一个窗口类的多个实例,当然,在多个回退栈中也会出现同一个窗口类的多个实例,图7-15所示的两个回退栈就都存在Activity Y。在一个回退栈出现同一个窗口类的多个实例的比较典型的例子是按Android设备的“Home”键,就会回到Android系统的Home界面,然后长按“Home”键,会出现一个程序列表这个程序列表包括两部分程序:按Home键停止的程序和按回退(Back)键退出或执行finish方法关闭的程序。前者可以立刻恢复运行(不会执行onCreate方法再次创建窗口),后者需要重新创建任务和主窗口,并将主窗口对象添加到回退栈中。,选择刚才的程序就会立刻回复到原来的窗口。实际上这一过程相当于将Home窗口Android系统的桌面本身也是一个程序(Launch2),Android系统在成功启动后,会首先执行Launch2,因此Android桌面程序的主窗口也可称为Home窗口(Home Activity)。我们可以将Launch2理解为Windows中的explorer.exe。Windows在每次启动后都会首先运行explorer.exe来显示桌面。添加到当前应用程序的回退栈中,如果多次按“Home”键,就会在回退栈中添加多个Home窗口对象,如图7-16所示。所以按“Home”键的过程也就是当前窗口与Home窗口之间切换的过程,相当于在当前应用程序中调用另外一个程序中的窗口。

 

▲图7-15 任务切换

 

▲图7-16 当前窗口与Home窗口之间的切换

综合上述,可以得出如下4条结论。

从ActivityA显示ActivityB时(可能是在ActivityA中主动调用了ActivityB,也可能是其他程序弹出的ActivityB,如来电窗口),ActivityA被停止(调用ActivityA.onStop方法),系统会保存ActivityA的状态(例如,滚动位置、文本框中输入的内容等)。如果用户在ActivityB获得焦点时按下回退(Back)按钮,ActivityB将关闭并销毁,ActivityA会恢复焦点状态。

用户在ActivityA处于焦点状态时按“Home”键切换到Android桌面,ActivityA会停止(调用ActivityA.onStop方法),该窗口所在的任务也会进入后台运行,系统会保存ActivityA的所有状态。这时Home窗口所在的任务(Launch2建立的任务)会进入前台运行。用户可以重新单击应用程序图标或长按“Home”键从程序列表中恢复ActivityA所在的任务。任务一旦恢复(重新回到前台运行),ActivityA又会重新获得焦点(如果ActivityA还在回退栈的栈顶的话)。

如果用户按下“Back”按钮,当前处于焦点的窗口会从回退栈中弹出并销毁,而在被弹出窗口下面的窗口这时变成了栈顶元素,因此该窗口获得了焦点。当一个窗口被销毁后,系统不会再保存该窗口的状态,也就是说窗口销毁后,窗口的所有状态将消失。

窗口可以被多次实例化,并且同一个窗口的多个实例可以位于一个或多个回退栈中。

7.2.2 保存窗口(Activity)状态

源代码目录:src/ch07/ActivityState

在上一节讲过,当两个任务切换时,被移到后台运行的任务的回退栈栈顶的窗口会被停止,这时系统会自动保存窗口的状态,等该任务重新切换到前台运行后,该窗口恢复焦点的同时也会恢复所有的状态。尽管这一过程看似很完美,但在这些被停止的窗口对象并不意味着会永久地驻留在内存中。当Anroid设备的内存耗得差不多时,或某些Android应用急需大量内存时,Android系统会销毁长期被停止的窗口对象(包括该窗口对象被系统保存的所有状态)。当然,尽管窗口对象被销毁了,这并不意味着窗口对象会从回退栈中弹出。被释放的窗口对象仍然占着回退栈原来的位置,只是该对象并不指向任何一个窗口对象。当系统将该任务重新移到前台运行时,就应该将该窗口恢复焦点和状态。但由于该窗口对象已经被系统销毁,所以系统只能重新创建该窗口对象,这就意味着窗口原来的状态全部丢失。发生这种情况对于大多数应用是不允许的,因此就需要在窗口对象释放之前保存要恢复的状态,等窗口对象重建时再恢复这些状态。

窗口对象被销毁的情况很多,除了按Back键或执行finish(或类似的方法)外,其他的销毁情况基本都需要保存状态。例如,窗口横竖屏切换时就是窗口对象销毁再创建的过程假设窗口从横屏切换到竖屏。系统会首先销毁处于横屏状态的窗口对象,然后再重新创建竖屏状态的窗口对象。。当然,保存窗口状态并不困难,困难的是什么时候保存窗口状态。像有一些窗口对象销毁的情况(如窗口的横竖屏切换)可以检测到,但有一些情况通过常规的方法根本无法检测到(如系统释放回退栈中的窗口对象)。

不过读者也不用担心,Android SDK为我们提供了非常简单的API保存窗口状态。系统本身并不会真正保存状态变量(因为系统根本不知道要保存什么),而只是使用户可以把握保存窗口变量的时机。这个时机就是Activity.onSaveInstanceState方法。当窗口对象被系统销毁必须是系统销毁的窗口对象才会调用onSaveInstanceState方法,用户自己按Back键或执行finish方法销毁的窗口对象不会调用该方法。之前,就会调用该对象的onSaveInstanceState方法。用户可以通过onSaveInstanceState方法的参数保存任意多的变量值。通常会通过Activity.onCreate方法的savedInstanceState参数获取被保存的状态变量值。

savedInstanceState参数值只在窗口对象重新创建时传入,窗口对象第一次创建按Back键或执行Activity.finish方法关闭窗口再创建窗口对象仍然属于第一次创建该窗口对象。时savedInstanceState参数值为null。

本节的例子将演示如何使用onSaveInstanceState方法和savedInstanceState参数保存和恢复状态变量。本例的主界面如图7-17所示。

 

▲图7-17 存取状态变量

本例的实现方法是定义一个非静态的有初始值的类成员变量(value),然后在onSaveInstanceState方法中保存该变量的值,最后在onCreate方法中通过savedInstanceState参数获取被保存的变量值。如果savedInstanceState参数值为null,则窗口对象第一次创建,仍然使用变量的默认值。

基本的测试方法是运行本例。单击“输出value字段值”按钮,这时应该输出value变量的默认值(default)。然后单击“设置value字段值”按钮,会将value变量的值设为“newValue”,这时按Ctrl+F12组合键改变Android模拟器的屏幕状态,或直接旋转Android设备。最后再单击“输出value字段值”按钮,如果输出了“newValue”,表明value变量值成功保存并恢复。下面看一下本例的完整代码。

源代码文件:src/ch07/ActivityState/src/mobile/android/activity/state/SaveActivityState.java

public class SaveActivityState extends Activity

{  

  // 定义value变量,并设置了默认值

  private String value = "default";

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.main);

    // 判断当前窗口对象是否第一次创建,如果savedInstanceState为null,

    // 当前窗口对象第一次创建,这时使用value变量的默认值,否则恢复value变量的值

    if (savedInstanceState != null)

    {

      value = savedInstanceState.getString("value");

    }

  }

  // “设置value字段值”按钮的单击事件方法,用于设置value变量的值

  public void onClick_SetFieldValue(View view)

  {

    value = "newValue";

  }

  // “输出value字段值”按钮的单击事件方法,用于将value变量的值输出到LogCat视图

  public void onClick_OutputFieldValue(View view)

  {

    Log.d("value", String.valueOf(value));

  }

  // 该方法用于保存状态变量,当前对象被系统销毁之前会调用该方法

  @Override

  protected void onSaveInstanceState(Bundle outState)

  {

    super.onSaveInstanceState(outState);

    // 保存value变量

    outState.putString("value", value);

  }

}

现在执行该程序,并按着上面的测试方法进行测试,可以在LogCat视图中查看输出的value变量值。

7.2.3 管理任务和回退栈

前面两节详细介绍了Android系统如何管理任务和回退栈。对于Android应用来说,只需要使用startActivity方法显示窗口即可。系统会自动对要显示、停止和关闭的窗口执行入栈、任务切换和出栈的操作。这一切我们都不需要操心,再说操心也没用,普通的应用程序无法干预这些行为。

尽管我们对Android系统的这些行为属于旁观者,但却可以改变系统的默认行为。例如,在默认情况下只要显示窗口,不管该窗口的实例在回退栈中是否存在,都会将窗口对象压栈,但在某些情况下,如果要显示的窗口已经在回退栈的栈顶(要显示的窗口已经获得了焦点),就不再创建新的窗口对象了。要达到这个目的,就需要使用<activity>标签中的一些属性或通过Intent对象设置一些标志。

虽然在7.1节已经详细介绍了<activity>标签的大部分属性,但仍然有少部分属性未涉及。这些属性都是与Android系统处理任务和回退栈的行为有关的。在本章后面的部分会逐渐向读者展示如何利用这些属性来改变系统处理任务和回退栈的默认行为。

<activity>标签中与任务和回退栈相关的属性及描述(详细的示例演示会在后面的部分给出)如下:

taskAffinity:指定窗口属于哪一个任务。

launchMode:设置窗口的创建模式。

allowTaskReparenting:允许当前窗口移到taskAffinity属性指定的任务中的回退栈的栈顶。

clearTaskOnLaunch:要求回退栈保持初始状态。

alwaysRetainTaskState:要求回退栈保持最近的状态,也就是说系统不会自动释放回退栈中的窗口对象。

finishOnTaskLaunch:功能与clearTaskOnLaunch类似,只是针对的是单个窗口,而不是针对整个回退栈。该属性指定某一个窗口只在当前会话有效。如果任务被换到后台运行后又回到前台,该窗口不会再显示。

除了上述几个属性外,还有如下几个标志可以通过Java代码对窗口的行为进行设置。

FLAG_ACTIVITY_NEW_TASK:相当于launchMode属性的singleTask模式。

FLAG_ACTIVITY_SINGLE_TOP:相当于launchMode属性的singleTop模式。

FLAG_ACTIVITY_CLEAR_TOP:如果要显示的窗口已经在回退栈中,压在该窗口上面的所有窗口对象将全部被销毁,该窗口会被设为栈顶并显示。该标志在<activity>标签中没有对应的属性。

在接下来的几节将详细介绍这些属性的标志的用法。

7.2.4 Activity的4种创建模式

源代码目录:src/ch07/LaunchMode、src/ch07/Single

默认情况下,每调用一次startActivity方法,就会创建一个新的窗口对象,并将该窗口对象压入当前任务的回退栈中。但在一些特殊情况下,并不希望无限制地创建窗口对象,因此就需要使用<acitivty>标签的launchMode属性修改窗口的默认创建模式。该属性是枚举类型,可以设置的值如表7-5所示。

 

表7-5 launchMode属性的值

 

续表

下面的部分会用实际的代码来演示launchMode属性的4个属性值的用法和效果。本节的例子由两个Android工程组成:LaunchMode和Single。核心的代码都在LaunchMode工程中,Single只是辅助用来测试的。下面先看一下LaunchMode的主界面,如图7-18所示。

 

▲图7-18 测试窗口创建模式的主界面

在图7-18所示界面的标题栏中显示了当前窗口所在任务的ID每次启动程序时任务ID可以不同。。下面是4个按钮,后面会介绍这4个按钮的作用。下面看一下主界面的实现代码。

源代码文件:src/ch07/LaunchMode/src/mobile/android/launch/mode/MainActivity.java

public class MainActivity extends Activity

{  

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main);

    // 将当前窗口所在任务的ID显示在标题栏中

    setTitle("Task ID:" + getTaskId());

  }

  // “standard”按钮的单击事件方法

  public void onClick_Standard(View view)

  {

    Intent intent = new Intent(this, StandardActivity.class);

    startActivity(intent);

  }

  // “singleTop”按钮的单击事件方法

  public void onClick_SingleTop(View view)

  {

    Intent intent = new Intent(this, SingleTopActivity.class);

    startActivity(intent);

  }

  // “ActivityA”按钮的单击事件方法

  public void onClick_ActivityA(View view)

  {

    Intent intent = new Intent(this, ActivityA.class);

    startActivity(intent);

  }

  // “single”按钮的单击时间方法

  public void onClick_SingleInstance(View view)

  {

    Intent intent = new Intent("mobile.android.ACTION_SINGLE");

    // 显示Single程序中的SingleActivity窗口

    startActivity(intent);

  }

}

从上面的代码可以看出,主界面的4个按钮分别显示了4个窗口,其中单击“singleInstance”按钮会显示另一个程序中的窗口。现在我们会继续学习本程序如何测试窗口的4中创建模式。

1.测试standard模式

单击主界面的“standard”按钮,会显示StandardActivity窗口,该窗口并未设置launchMode属性,所以系统会使用launchMode属性的默认值处理该窗口的创建方式。StandardActivity类的实现代码如下:

源代码文件:src/ch07/LaunchMode/src/mobile/android/launch/mode/StandardActivity.java

public class StandardActivity extends Activity

{   

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_standard);

    // 将StandardActivity所在任务的ID和窗口对象的hashCode显示在标题栏中

    setTitle("Task ID:" + getTaskId() + " hashcode:" + this.hashCode());

  }

  // “standard”按钮的单击事件方法

  public void onClick_Standard(View view)

  {

    Intent intent = new Intent(this, StandardActivity.class);

    // 再次显示StandardActivity

    startActivity(intent);

  }

}

在StandardActivity窗口中有一个“standard”按钮,如图7-19所示。单击该按钮,会不断显示StandardActivity。在该窗口的标题栏中显示了任务ID和当前窗口的hashcode。不管显示多少次StandardActivity,任务ID始终和主窗口的任务ID一致,这说明StandardActivity对象是压入到与主窗口相同的回退栈中的。而hashcode会不断变化,每一个StandardActivity类的实例都不相同,这说明每一次使用startActvity方法显示StandardActivity时都会创建不同的实例。

显示StandardActivity的过程可用图7-20所示的回退栈示意图表示。

 

▲图7-19 StandardActivity窗口

 

▲图7-20 窗口在standard模式下的压栈方式

回退栈一开始只有主窗口(MainActivity),当单击“standard”按钮后,第一个StandardActivity对象创建并压栈,当再次单击“standard”按钮,会再创建一个StandardActivity对象并压栈。按Back键回退时这些窗口对象会一个个出栈。如果创建了两个StandardActivity对象,需要按两次Back键才能返回MainActivity。

2.测试singleTop模式

测试singleTop模式涉及一个SingleTopActivity类,在声明该类时将android:launchMode属性设置为singleTop,代码如下:

源代码文件:src/ch07/LaunchMode/AndroidManifest.xml

<activity

  android:name=".SingleTopActivity"

  android:launchMode="singleTop" />

SingleTopActivity与StandardActivity的界面风格类似,只是“standard”按钮变成了“singleTop”。SingleTopActivity类与StandardActivity类的代码大体相同,只是多了一个onNewIntent方法。当SingleTopActivity处于栈顶时,系统不会再创建SingleTopActivity类的实例,而是直接使用位于栈顶的SingleTopActivity实例,并调用该实例的onNewIntent方法。SingleTopActivity类的代码如下:

源代码文件:src/ch07/LaunchMode/src/mobile/android/launch/mode/SingleTopActivity.java

public class SingleTopActivity extends Activity

{  

  ……

  // 当SingleTopActivity位于栈顶时再次显示该窗口会调用onNewIntent方法

  @Override

  protected void onNewIntent(Intent intent)

  {

    super.onNewIntent(intent);

    // 向LogCat视图输出信息

    Log.d("singleTop", "onNewIntent");

  }

}

现在单击主窗口的“singleTop”按钮,会第一次显示SingleTopActivity,当再次单击“singleTop”按钮时会发现并没有显示新的SingleTopActivity窗口(hashcode并没有变),而由于调用了SingleTopActivity.onNewIntent方法,会在LogCat视图中找到相应的输出信息。

3.测试singleTask模式

singleTask模式需要使用多种测试方式。首先测试在同一个程序中的singleTask模式,这里需要两个窗口:ActivityA和ActivityB,这两个窗口与前面实现的两个窗口类的代码类似。ActivityA是singleTask模式,ActivityB是standard模式。这两个窗口的声明代码如下:

源代码文件:src/ch07/LaunchMode/AndroidManifest.xml

<activity

  android:name=".ActivityA"

  android:launchMode="singleTask" />

<activity android:name=".ActivityB" />

ActivityA实现了onNewIntent方法,在该方法中向LogCat视图中输出了信息。ActivityA类的代码如下:

源代码文件:src/ch07/LaunchMode/src/mobile/android/launch/mode/ActivityA.java

public class ActivityA extends Activity

{  

  ……

  // 当ActivityA已经在某一个回退栈中存在时会调用该方法

  @Override

  protected void onNewIntent(Intent intent)

  {

    super.onNewIntent(intent);

    Log.d("onNewIntent", "ActivityA_singleTask");

  }

}

ActivityB实现了onDestroy方法。如果ActivityB压在ActivityA的上面,显示ActivityA时ActivityB会首先出栈(ActivityB对象被销毁),然后才会显示处于栈顶的ActivityA。ActivityB类的代码如下:

源代码文件:src/ch07/LaunchMode/src/mobile/android/launch/mode/ActivityB.java

public class ActivityB extends Activity

{  

  ……

  @Override

  protected void onDestroy()

  {

    super.onDestroy();

    Log.d("onDestroy", "ActivityB_standard");

  }

}

现在单击主窗口的“ActivityA”按钮显示ActivityA。由于ActivityA和MainActivity在同一个程序中,而且目前ActivityA并不存在,所以ActivityA对象会被压入MainActivity所在的回退栈中,ActivityA的显示效果如图7-21所示。ActivityA和MainActivity都在ID为6的任务中。

 

▲图7-21 ActivityA 的界面

现在单击ActivityA窗口中的“ActivityB”按钮,会显示ActivityB窗口。然后单击ActivityB窗口中的“ActivityA”按钮,会再次显示ActivityA窗口。不过这个ActivityA窗口不是新创建的,而是从MainActivity显示的ActivityA窗口。在显示ActivityA的过程中发生了如下两件事。

调用了ActivityA.onNewIntent方法。

ActivityB对象会出栈,并被释放,因此会调用ActivityB.onDestroy方法。

从LogCat视图中可以看到onNewIntent和onDestroy方法中输出的信息。由于ActivityB已经被释放,而ActivityA目前在栈顶,而ActivityA下面的窗口是MainActivity,所以尽管刚才窗口的显示过程是ActivityA-ActivityB-ActivityA,但在第二次显示ActivityA时,ActivityB已经出栈。所以这时按Back按钮关闭ActivityA后会返回MainActivity,而不是ActivityB。

现在来测试用LaunchMode中的代码来调用Single中的窗口。Single程序中的SingleActvity窗口的创建模式是singleTask,声明代码如下:

源代码文件:src/ch07/Single/AndroidManifest.xml

<activity  

  android:name=".SingleActivity" 

  android:label="Single" 

  android:launchMode="singleTask" >

  <intent-filter>

    <action android:name="android.intent.action.MAIN" />

    <category android:name="android.intent.category.LAUNCHER" />

  </intent-filter>

  <intent-filter>

    <action android:name="mobile.android.ACTION_SINGLE" />

    <category android:name="android.intent.category.DEFAULT" />

  </intent-filter>

</activity>

首先运行Single程序,然后按Back键关闭Single程序。现在单击LaunchMode程序主界面的“single”按钮,就会显示SingleActivity窗口。但SingleActivity所在的任务的ID并不是6,而是其他任务的ID,如图7-22中的30。这充分说明在程序A中调用程序B中的singleTask模式的窗口,并且该窗口对象还未创建的情况下,会新建立一个任务(这个任务属于程序B),并将这个singleTask模式的窗口压入这个新任务中的回退栈。当然,如果这时切换到程序B,再显示B中其他的窗口,仍然会使用这个新创建的任务。

 

▲图7-22 SingleActivity 界面

还是重复刚才的实验,只是在运行Single后不要按Back键退出程序,而是按Home键切换到桌面,然后再运行LaunchMode程序,并单击“single”按钮,这时并不会新创建任务,而是使用SingleActivity所在的任务因为SingleActivity是Single的主窗口,所以启动Single程序后就会直接显示SingleActivity。,并进行任务切换。也就是说将MainActivity所在的任务放到后台运行,将SingleActivity所在的任务移到前台运行。这样SingleActivity就会像LaunchMode程序内部的窗口一样来显示。但与standard和singleTop的区别是,显示其他程序的singleTask模式的窗口后,按Home键切换到桌面后,再从程序列表(长按Home键显示)进入该程序,不会再显示singleTask模式的窗口,而是直接显示程序栈顶的窗口。这是因为系统在显示standard和singleTop模式的窗口时,会将窗口对象压入当前的回退栈。这样在从桌面切换回程序时仍然是刚才显示的窗口,而singleTask和singleInstance模式则并不会将要显示的窗口压入到当前的回退栈中,而是新任务中的回退栈。所以在任务切换后,无法回到刚才显示的窗口(因为这个窗口并没在调用该窗口的程序的回退栈中)。

singleTask模式非常有用,如果查看Android源代码,会发现有很多内置应用中窗口设置成了singleTask模式。下面是一个典型的应用案例。

一个地图导航程序调用了系统的地图窗口,并进行了一些标注,然后按Home键切换到桌面。再启动系统中的地图程序,会发现刚才的标注仍然保留。这种效果就是利用singleTask模式实现的,因为地图导航程序和系统内置的地图程序都使用了同一个地图显示窗口。

读者也可以在本例中做这个实验。单击“single”按钮显示SingleActivity窗口后,在该窗口的文本输入框中输入一些文本,然后按Home键切换到桌面,再切换到Single程序(不管该程序是否关闭),同样会显示刚才输入的文本,因为LaunchMode和Single使用的是用一个SingleActivity窗口。

多学一招:同时关闭多个窗口

窗口(Activity)是Android的4大应用程序组件另3个是Service、Broadcast Receiver和Content Provider,会在后面的章节详细介绍。之一,尽管窗口通常是我们学习的第1个组件,但却是最不好控制的一个组件。如果想灵活应用窗口,需要使用很多技巧。现在我们就来看看一个比较有意思的技巧:同时关闭多个窗口。

假设有一个需求,一个test.apk程序有4个窗口:A、B、C、D。其中A是主窗口,这4个窗口都已经创建了,也就是说回退栈已经存在这4个窗口类的对象了。A在栈底,B和C在中间,D在栈顶。现在D窗口上有一个按钮,我们希望单击该按钮后不仅会关闭D,还会同时关闭B和C,直接回到窗口A。

大家知道,Android SDK并没有提供直接获取窗口对象的API,因此也就无法在D中直接关闭B和C。当然,可以将B和C保存为静态变量并统一在D中调用B和C窗口对象的finish方法,关闭这两个窗口,但这样比较麻烦。所以比较好的做法是使用singleTask模式(只能使用该模式),也就是说将A设为singleTask模式,然后在D窗口的按钮中执行下面的代码显示A,根据表7-5对singleTask模式的描述,系统会对A前面的所用窗口(B、C、D)执行出栈操作(释放这些窗口对象),所以B、C、D就会被同时释放。

Intent intent = new Intent(this, A.class);

// 执行下面的代码后,系统会自动关闭B、C和D

startActivity(intent);

4.测试singleInstance模式

了解了singleTask模式,singleInstance模式就比较好理解了。singleInstance与singleTask模式的唯一区别是存放singleInstance模式窗口对象的回退栈不能有其他任何窗口对象。因此,在任何情况下,只要显示singleInstance模式的窗口,并且该窗口不存在,一定会新建立任务的。当然,如果该窗口已经存在,仍然会切换到该任务来显示singleInstance模式的窗口。

读者可以将LaunchMode程序中ActivityA窗口的创建模式改成singleInstance,并单击“ActivityA”按钮,这时就会新创建一个任务,然后再单击“ActivityB”按钮,由于ActivityA所在的回退栈只能有一个窗口对象,所以ActivityB仍然被压入到MainActivity窗口所在的回退栈。这时再单击“ActivityA”按钮来显示ActivityA窗口(任务切换)。如果这时按Back键关闭ActivityA,会回到ActivityB。如果ActivityA的创建模式是singleTask,则会直接返回MainActivity,因为ActivityB已经在第2次显示ActivityA时被系统出栈并释放了。

7.2.5 用Java代码设置窗口创建模式

源代码目录:src/ch07/IntentFlag

窗口的创建模式不仅可以在声明窗口时通过launchMode属性指定,也可以通过Intent.setFlags方法指定一个或多个标志进行设置。本节将介绍几个与窗口创建模式相关的标志。

FLAG_ACTIVITY_SINGLE_TOP。

FLAG_ACTIVITY_CLEAR_TOP。

FLAG_ACTIVITY_NEW_TASK。

FLAG_ACTIVITY_CLEAR_TASK。

FLAG_ACTIVITY_REORDER_TO_FRONT。

上面5个标志并不都是与launchMode属性的值对应,这些标志需要组合使用才能完全符合某一个launchMode属性值所满足的窗口创建模式。

本节给出一个例子来帮助读者更深入理解这些标志的作用。下面先看一下本例的主界面,如图7-23所示。图标后面的数字是任务ID,最后的数字是当前窗口的hashcode。

 

▲图7-23 IntentFlag 程序的主界面

1.FLAG_ACTIVITY_SINGLE_TOP标志

该标志与singleTop模式的作用相同。如果要显示的窗口正好处于当前回退栈的栈顶,则不再创建新的窗口对象,而是调用该窗口对象的onNewIntent方法。

单击主界面的“显示IntentFlagActivity窗口(singleTop)”按钮,仍然会显示当前的窗口(hashcode并没有变),并不会创建新IntentFlagActivity窗口(主窗口)。该按钮的单击事件方法代码如下:

源代码文件:src/ch07/IntentFlag/src/mobile/android/intent/flag/IntentFlagActivity.java

public void onClick_ShowIntentFlagActivity_SingleTop(View view)

{

  Intent intent = new Intent(this, IntentFlagActivity.class);

  // 设置FLAG_ACTIVITY_SINGLE_TOP标志

  intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);

  startActivity(intent);

}

注意

如果同时设置了launchMode属性和标志,标志动作将优先于相应的launchMode属性值。例如,如果声明IntentFlagActivity类时<activity>标签的launchMode属性值设为了singleInstance,使用上面的代码仍然按照singleTop方式处理。

2.FLAG_ACTIVITY_CLEAR_TOP标志

该标志用于释放回退栈中处于要显示窗口前面的所有窗口当然,如果窗口根本不存在,就直接创建窗口对象了。,可分如下两种情况使用(假设要显示的窗口为MyActivity)。

(1)MyActivity的launchMode属性值是standard,并且没有将FLAG_ACTIVITY_SINGLE_TOP与FLAG_ACTIVITY_CLEAR_TOP一起使用。在这种情况下,不仅压在MyActivity上面的所有窗口会被释放,而且MyActivity也将被释放,然后重新创建该窗口对象并压栈,也就是说不会调用onNewIntent方法。

(2)MyActivity的launchMode属性值是除了standard的其他3个值,或与FLAG_ACTIVITY_ SINGLE_TOP标志同时使用。在这种情况下相当于显示程序内部的singleTask模式的窗口,也就是说MyActivity前面的所有窗口将被出栈(释放),并调用栈顶窗口对象的onNewIntent方法。

测试FLAG_ACTIVITY_CLEAR_TOP标志需要另外一个MyActivity窗口(standard模式),该窗口的界面如图7-24所示。

 

▲图7-24 MyActivity 窗口

读者可以多单击几次MyActivity窗口中的“显示MyActivity窗口”按钮,然后单击“关闭所有的MyActivity窗口”按钮,就会立刻返回主窗口(IntentFlagActivity)。MyActivity类的实现代码如下:

源代码文件:src/ch07/IntentFlag/src/mobile/android/intent/flag/MyActivity.java

public class MyActivity extends Activity

{  

  @Override

  public void onCreate(Bundle savedInstanceState)

  {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_my);

    // 在标题栏上显示任务ID和hashcode

    setTitle(getTaskId() + " " + String.valueOf(hashCode()));

 

  }

  // “显示MyActivity窗口”按钮的单击事件方法

  public void onClick_ShowMyActivity(View view)

  {

    Intent intent = new Intent(this, MyActivity.class);

    startActivity(intent);

  }

  // “关闭所有的MyActivity窗口”按钮单击时间方法

  public void onClick_CloseAllMyActivity(View view)

  {

    Intent intent = new Intent(this, IntentFlagActivity.class);

    // 同时使用了FLAG_ACTIVITY_CLEAR_TOP和FLAG_ACTIVITY_SINGLE_TOP标志

    // 相当于将IntentFlagActivity设为singleTask模式

    intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP

            Intent.FLAG_ACTIVITY_SINGLE_TOP);

    startActivity(intent);  

  } 

} 

3.FLAG_ACTIVITY_NEW_TASK标志

如果只使用该标志,相当于显示另一个Android程序中singleTask模式的窗口。测试该标志需要使用上一节的Single程序,所以在运行本例之前需要先安装Single(不需要运行或运行后按Back键退出程序)。在Single中有一个NewActivity窗口(standard模式),声明代码如下:

<activity

  android:name=".NewActivity"

  android:label="NewActivity" >

  <intent-filter>

    <action android:name="mobile.android.ACTION_NEW_ACTIVITY" />

    <category android:name="android.intent.category.DEFAULT" />

  </intent-filter>

</activity>

现在分别单击主界面的“显示其他程序的窗口(standard)”和“显示其他程序的窗口(standard)”按钮,前者按standard模式显示NewActvity,后者按singleTask模式显示NewActivity。这两个按钮的单击事件方法代码如下:

源代码文件:src/ch07/IntentFlag/src/mobile/android/intent/flag/IntentFlagActivity.java

// “显示其他程序的窗口(standard)”按钮单击事件方法

public void onClick_OtherAppActivity_Standard(View view)

{

  try

  {

    Intent intent = new Intent("mobile.android.ACTION_NEW_ACTIVITY");

    startActivity(intent);

  }

  catch (Exception e)

  {

    Toast.makeText(this, "未安装Single程序!", Toast.LENGTH_LONG).show();

  }

}

// “显示其他程序的窗口(singleTask)”按钮单击事件方法

public void onClick_OtherAppActivity_SingleTask(View view)

{

  try

  {

    Intent intent = new Intent("mobile.android.ACTION_NEW_ACTIVITY");

    // 设置FLAG_ACTIVITY_NEW_TASK标志

    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    startActivity(intent);

  }

  catch (Exception e)

  {

    Toast.makeText(this, "未安装Single程序!", Toast.LENGTH_LONG).show();

  }

}

如果未设置FLAG_ACTIVITY_NEW_TASK标志,会使用standard模式显示NewActivity窗口。系统会将新创建的NewActivity对象压入当前的回退栈中,所以NewActivity窗口的标题栏显示的任务ID就是IntentFlagActivity窗口标题栏显示的任务ID。如果设置了FLAG_ACTIVITY_NEW_TASK标志,系统会使用singleTask模式显示NewActivity窗口。该窗口标题栏会显示一个新的任务ID。

4.FLAG_ACTIVITY_CLEAR_TASK标志

FLAG_ACTIVITY_CLEAR_TASK标志用于释放当前回退栈中所有的窗口对象(包括要显示的窗口对象),然后再重新创建要显示的窗口对象。例如,当前回退栈中有3个窗口对象:A-B-C。C是栈顶元素,在C中使用FLAG_ACTIVITY_CLEAR_TASK标志显示A,那么A、B、C都会出栈并被释放,然后系统会重新创建A窗口对象并入栈。

FLAG_ACTIVITY_CLEAR_TASK必须和FLAG_ACTIVITY_NEW_TASK标志一起使用才起作用。例如,下面的代码显示了窗口A。系统的基本操作过程是释放了压在A上面的窗口对象及A对象,并重新创建了A对象后将其压栈。

Intent intent = new Intent(this, A.class);

// FLAG_ACTIVITY_CLEAR_TASK和FLAG_ACTIVITY_NEW_TASK必须同时使用

Intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK

     Intent. FLAG_ACTIVITY_NEW_TASK);

startActivity(intent);

5.FLAG_ACTIVITY_REORDER_TO_FRONT标志

该标志有些类似FLAG_ACTIVITY_SINGLE_TOP,但并不限于栈顶的窗口,只要当前回退栈中的任何位置有要显示的窗口对象,就不再为该窗口创建新对象,而是将栈中该窗口的对象提到栈顶,而窗口前面的窗口对象并不出栈(自然也不会释放这些窗口对象)。例如,当前回退栈中有4个窗口对象:A、B、C、D,其中D是栈顶元素。现在要创建一个B窗口对象,如果在显示B窗口时通过Intent.setFlags方法设置了FLAG_ACTIVITY_REORDER_TO_FRONT标志,回退栈仍然包含4个窗口对象,但顺序却变为A、C、D、B,然后会调用B窗口对象的onNewIntent方法。当然,如果不使用任何标志,而且B窗口的创建模式是standard,那么回退栈中的窗口对象就会变成A、B、C、D、B。

7.2.6 窗口的乾坤大挪移(affinity)

源代码目录:src/ch07/Affinity、src/ch07/TestAffinity

本节涉及<activity>标签的如下两个属性。

taskAffinity:字符串类型,默认值是当前应用程序的package名称。该属性指定了当前窗口对象要使用哪个回退栈。

allowTaskReparenting:布尔类型,默认值是false。该属性允许窗口对象在不同回退栈之间移动。

1.taskAffinity属性

如果在A.apk中使用FLAG_ACTIVITY_NEW_TASK标志显示B.apk中的MyActivity窗口,当MyActivity窗口不存在时,会新创建一个任务,当MyActivity窗口存在时,会直接切换到MyActivity窗口所在的任务。总之,MyActivity窗口并不在A.apk的默认回退栈中,如果从桌面切换到A.apk所在的任务,MyActivity是不会再显示的,切换到B.apk的任务时才会显示MyActivity。那么现在我们要实现一个完全相反的功能,也就是说切换到A.apk会显示MyActivity,而切换到B.apk不会显示MyActivity,就像MyActivity属于A.apk一样(实际上MyActivity属于B.apk)。

实现这个功能的方法也很简单,就是将MyActivity窗口的taskAffinity属性值指向A.apk的任务名。Android应用程序的默认任务名称就是该应用程序的package名,在AndroidManifest.xml文件的<manifest>标签的package属性中指定。由于A.apk中没有窗口指定其他的任务名称,所以所有的窗口都会在默认任务的回退栈中,因此MyActivity只要在声明时将taskAffinity属性设为A.apk的package名称即可。

现在再回到本节的例子。首先看TestAffinity程序。在该程序中有一个MyActivity2窗口,该窗口在声明时通过taskAffinity属性指定了Affinity程序的默认任务名称(mobile.android.affinity),声明代码如下:

源代码文件:src/ch07/TestAffinity/AndroidManifest.xml

<activity

  android:name=".MyActivity2"

  android:label="@string/title_activity_my_activity2"

  android:taskAffinity="mobile.android.affinity">

  <intent-filter>

    <action android:name="mobile.android.ACTION_MYACTIVITY2" />

    <category android:name="android.intent.category.DEFAULT" />

  </intent-filter>

</activity>

接下来看看单击如图7-25所示Affinity程序主界面的“taskAffinity”按钮时执行的代码。

 

▲图7-25 Affinity 程序的主界面

源代码文件:

src/ch07/Affinity/src/mobile/android/affinity/AffinityActivity.java

public void onClick_TaskAffinity(View view)

{

  try

  {

    Intent intent = new Intent("mobile.android.ACTION_MYACTIVITY2");

     // 设置了FLAG_ACTIVITY_NEW_TASK标志

    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

    startActivity(intent);

  }

  catch (Exception e)

  {

    Toast.makeText(this, "TestAffinity未安装", Toast.LENGTH_LONG).show();

  }

}

单击“taskAffinity”按钮后会显示MyActivity2。我们会看到MyActivity2窗口的标题栏中显示的任务ID与Affinity主窗口标题栏显示的任务ID是相同的,这说明MyActivity2所在的任务与主窗口的任务相同。如果将android:taskAffinity属性去掉,并且TestAffinity在关闭的情况下,系统会为MyActivity2创建一个新的任务。如果TestAffinity未关闭,MyActivity2会使用与TestAfinity主窗口相同的任务。

注意

<activity>和<application>标签都有taskAffinity属性。如果<activity>标签未设置taskAffinity属性,而<application>标签设置了taskAffinity属性,那么<application>标签中声明的所有窗口都会使用<application>标签的taskAffinity属性指定的任务。

2.allowTaskReparenting属性

如果某窗口的allowTaskReparenting属性值为true,当该窗口所在的任务被切换到后台时,一旦有另一个与该窗口具有相同affinity窗口的affinity就是<activity>标签的taskAffinity属性值,而任务的affinity就是创建任务时压入的第一个窗口对象的<activity>标签的taskAffinity属性值。的任务切换到前台,那么该窗口会从原来的任务移到新的任务。

注意

尽管allowTaskReparenting属性会造成窗口对象在任务之间的移动,但并不会调用窗口对象的onCreate或onNewIntent方法,只是简单的Java对象迁移,而且不管该任务中有多少窗口对象,都会移动到另外一个任务。例如,TaskA与TaskB的affinity是相同的。TaskA目前是在后台运行,而TaskB即将进入前台运行。TaskA的回退栈中有3个窗口:TaskA_1 - TaskA_2 – TaskA_3,其中TaskA_3是栈顶窗口,TaskB只有一个主窗口(TaskB_Main)。当TaskB切换到前台运行后,系统会将TaskA_1、TaskA_2 和TaskA_3都移到TaskB中,TaskA将销毁,而TaskB的回退栈中的窗口变成了4个:TaskB_Main - TaskA_1 - TaskA_2 – TaskA_3。所以当TaskB切换到前台后,首先看到的是窗口TaskA_3。TaskA_3关闭后会看到TaskA_2,以此类推,最后一个显示的是TaskB_Main。

现在仍然用本节的两个程序(Affinity和TestAffinity)测试allowTaskReparenting属性。在TestAffinity中有两个窗口(MyActivity1和MyActivity2),在声明这两个窗口时将allowTaskReparenting属性值设为true,并且指定taskAffinity属性值为mobile.android.affinity,该值是Affinity程序的主窗口所在的任务名称。也就是说,MyActivity1和MyActivity2的affinity与主窗口所在任务的affinity相同。MyActivity1和MyActivity2的声明代码在介绍taskAffinity属性时已经给出了MyActivity2的声明代码,不过没有设置allowTaskReparenting属性,这里给出完整的声明代码。是否设置allowTaskReparenting属性都不影响taskAffinity属性的测试。如下:

源代码文件:src/ch07/TestAffinity/AndroidManifest.xml

<activity

  android:name=".MyActivity1"

  android:allowTaskReparenting="true"

  android:label="MyActivity2"

  android:taskAffinity="mobile.android.affinity">

  <intent-filter>

    <action android:name="mobile.android.ACTION_MYACTIVITY1" />

    <category android:name="android.intent.category.DEFAULT" />

  </intent-filter>

</activity>

<activity

  android:name=".MyActivity2"

  android:allowTaskReparenting="true"

  android:label="MyActivity2"

  android:taskAffinity="mobile.android.affinity">

  <intent-filter>

    <action android:name="mobile.android.ACTION_MYACTIVITY2" />

    <category android:name="android.intent.category.DEFAULT" />

  </intent-filter>

</activity>

在TestAffinity的主窗口的onCreate方法中显示MyActivity1,在MyActivity1.onCreate方法中显示MyActivity2,所以在运行TestAffinity程序后MyActivity1和MyActivity2实际上都已经在自己的任务的回退栈中了。

并不需要在Affinity程序中添加任何代码。现在先运行TestAffinity,然后按Home键切换到桌面,最后再运行Affinity程序。这时看到的并不是Affinity的主窗口,而是MyActivity2。关闭MyActivity2会显示MyActivity1,关闭MyActivity1才会显示Affinity的主窗口。

要使用allowTaskReparenting属性必须了解如下几点。

切换到Affinity所在的任务时需要重新按一下程序图标,而不是按Home键从列表中切换,否则不会进行窗口的移动。

如果要移动的窗口的launchMode属性值是standard或singleTop,窗口移动的规则符合前面的描述。但如果launchMode属性值是singleTask或singleInstance,尽管窗口也会移动,但就不是从一个任务移动了。由于singleTask模式会根据taskAffinity属性值决定是否创建新的任务。由于TestAffinity的主窗口的taskAffinity属性值与MyActivity1和MyActivity2的taskAffinity属性值不同,所以如果MyActivity2的创建模式是singleTask,那么MyActivity2必然会创建一个新的任务。所以当Affinity程序运行后,会分别从MyActivity1所在的任务(与TestAffinity的主窗口在同一个任务)和MyActivity2所在的任务分别移动MyActivity1和MyActivity2。也就是说不管有多少个任务在后台,只要这些任务中的窗口的taskAffinity属性值与前台运行的任务的affinity相同,就会移动这些任务中的相应窗口。singleInstance模式与singleTask有相同的效果,至于移动顺序,经笔者测试,如果MyActivity2是singleTask模式,移动后的当前回退栈窗口顺序是:主窗口-MyActivity2-MyActivity1。如果是singleInstance模式,窗口顺序是:主窗口-MyActivity1-MyActivity2。

答疑解惑:FLAG_ACTIVITY_NEW_TASK为什么、在什么情况下会创建新任务

上一节已经讲过,如果显示其他应用程序的窗口时设置了FLAG_ACTIVITY_NEW_TASK标志,并且该窗口所在的应用程序没有启动,系统就会为该窗口创建一个新的任务,而显示本程序内部窗口时会直接利用当前的任务。可能读者看到这样的疑问,为什么显示其他程序窗口和本程序内部的窗口会有不一样的行为呢?其实这与taskAffinity属性值有关。

一般在声明窗口时并不会设置taskAffinity属性,因此所有的窗口都会使用默认的任务。当使用FLAG_ACTIVITY_NEW_TASK标志显示窗口时(不管是程序内部还是程序的窗口)都会根据taskAffinity属性值来建立任务或使用已经存在的任务其实FLAG_ACTIVITY_NEW_TASK这个标志名起得并不是很恰当,如果标志名为FLAG_ACTIVITY_NEW_USING_TASK会更好点,因为使用该标志显示窗口并不总是创建新的任务,还会使用已经存在的任务。。由于MyActivity2的taskAffinity属性值是mobile.android.affinity,而该名称正好是Affinity程序的任务名,所以系统会在Affinity程序的任务中创建MyActivity2对象。

那么如果未指定taskAffinity属性会发生什么情况下呢?在这种情况下,MyActivity2的taskAffinity属性值实际上就是mobile.android.test.affinityTestAffinity程序的package名。。由于MyActivity2指定要使用的任务与Affinity的任务(mobile.android.affinity)不同,所以系统创建MyActivity2对象时自然不会使用Affinity的任务了。假设这时TestAffinity程序还未启动,那么名称为mobile.android.test.affinity的任务根本就不存在,所以系统会为MyActivity2新创建一个任务。如果这时在MyActivity2显示后按Home键回到桌面,然后再启动TestAffinity,仍然会显示MyActivity2(因为MyActivity2所在的任务就是TestAffinity的默认任务)。但有一点要说明,就是在任务的回退栈中如果已经有一个窗口对象(MyActivity2),那么即使AndroidManifest.xml中声明了主窗口,系统也不会再创建这个主窗口了。所以启动TestAffinity后按Back键关闭MyActivity2,TestAffinity也就关闭了(因为MyActivity2是回退栈中唯一的窗口对象)。

当然,如果Affinity程序在显示MyActivity2之前TestAffinity就已经启动(任务在后台执行),那么名为mobile.android.test.affinity任务已经存在了,所以创建MyActivity2对象时会直接使用该任务。

总结:FLAG_ACTIVITY_NEW_TASK只在taskAffinity属性指定的任务不存时才创建新的任务。

7.2.7 销毁不再使用的窗口

如果用户长时间离开某个任务(该任务长时间处于后台休眠状态),系统就会销毁该任务中除了根窗口根窗口就是指栈底的窗口。通常根窗口就是主窗口,但有时也可以是其他的窗口,所以根窗口更能准确表示回退栈的栈顶窗口。外的所有窗口。当用户再次回到该任务时,无论以前回退栈有多少个窗口,都会只显示根窗口。Android系统之所以这样处理,是因为如果一个任务长时间在后台休眠,系统就会认为用户很可能已经放弃了这些任务在程序中曾经做的工作,为了节省内存,系统就会释放不必要的窗口资源,所以当用户再次返回该任务时就只有根窗口恢复了。

Android系统的这个默认行为有时却很讨厌,不过幸好可以通过如下3个<activity>标签的属性来改变系统的默认行为。

1.alwaysRetainTaskState属性

如果要用户系统无论离开当前任务多长时间都不会销毁回退栈中的任何窗口,那么就将alwaysRetainTaskState属性设为true。

alwaysRetainTaskState属性的默认值为false。

2.clearTaskOnLaunch属性

该属性与alwaysRetainTaskStates属性正好相反,只要当前任务被切换到后台,哪怕是很短的时间,系统都会销毁除了根窗口外的所有窗口。该属性仅对根窗口有意义。

clearTaskOnLaunch属性的默认值为false。

3.finishOnTaskLaunch属性

该属性与clearTaskOnLaunch类似,只不过该属性只针对一个窗口,而clearTaskOnLaunch针对整个任务中的所有窗口(不包括根窗口)。例如,回退栈(所在任务是TaskA)中有3个窗口:A-B-C,C是栈顶窗口。如果将C的finishOnTaskLaunch属性设为true,当TaskA在切换到后台时只有C被销毁。现在回退栈中包含的窗口对象及其顺序是A-B,所以当前显示的是窗口B,该属性不会作用于根窗口。

finishOnTaskLaunch属性的默认值是false。

答疑解惑:为什么clearTaskOnLaunch属性不起作用

可能有的读者按前面的解释测试这几个属性后发现clearTaskOnLaunch属性好像不起作用。例如,现在有一个test.apk程序,该程序中有3个窗口:A、B和C。A是主窗口,而且A的clearTaskOnLaunch属性值是true。单击A中的按钮会显示窗口B,单击B中的按钮会显示窗口C。当C显示后,目前回退栈窗口顺序是A-B-C,C是栈顶窗口。现在按Home键回到桌面,然后再长按Home键显示程序历史列表,选中刚才运行的程序回到A、B和C窗口所在的任务。会发现仍然显示了C,退出C或显示B,再退出B后显示A。但按着前面的描述,如果根窗口将clearTaskOnLaunch属性值设为true,当再次回到当前任务时除了根窗口外,其他的窗口都会被销毁,但是为什么B和C没有被销毁呢?正常应该是直接显示A才对。

实际上clearTaskOnLaunch还是起作用的,只是刚才的操作不对。当窗口所在的任务回到前台的方法不应该是按Home键,应该再次按程序列表中的程序图标官方文档的解释有出入,实际上按Home键回到任务并不能销毁任何窗口,必须要像第一次启动程序一样按程序图标才行。,这样当程序再次启动时,就会看到直接显示窗口A了。在测试alwaysRetainTaskState属性时也应采用与clearTaskOnLaunch同样的方法,只是需要等待较长的时间再重新启动程序。

对于finishOnTaskLaunch属性,按着clearTaskOnLaunch的做法当然没问题,不过经笔者测试,使用按Home键回到任务的方法也同样可以销毁finishOnTaskLaunch属性值为true的窗口。