Activity之理解Tasks和Back Stack

Activity之理解Tasks和Back Stack

Task是一组Activity的集合,用户与之交互用于完成一个特定的任务。所有的Activity被安排在一个stack中称之为back stack, Activity按照打开的顺序存放于其中。例如,一个邮件APP可以有一个用于显示接收的新邮件列表的Activity。当用户选择一封邮件,那一个新的Activity会打开显示其内容。这个新的Activity就会被添加到back stack。 如果用户按下Back按钮,那么这个新打开的Activity会被finish并从stack中弹出消失。

当App运行在Android7(API Level 24)或更高版本的多窗口的环境中,系统会为每一个窗口单独管理task;每一个窗口可以拥有多个task。 设备的Home屏幕是用于启动绝大多数task的地方。当用户触点Launcher桌面App图标时,App的task会进入前台运行。如果App对应的task并不存在(最近用户并未打开过App),就会创建一个新的task,把打开应用的入口Activity放在stack中作为根Activity。 如果当前Activity启动另外一个Activity,那么新打开的Activity会被压到stack的顶端并得到焦点。而前一个Activity会保留在stack中,并处于stopped状态。一个stop的Activity,系统会保留它与用户交互的状态。当用户触点Back按钮。当前的Activity会从栈顶弹出(Activity被销毁)而前一个Activity会重新进入可见状态(UI状态被恢复)。在stack中的Activity并不会被重新安排,仅只做弹出和压入的动作。back stack保持着“后进,先出”的数据结构。如下图所示

图1 

图1表示新的Activity是如何添加到back stack中的,当用户触点Back按钮时,当前Activity被销毁并使前一个Activity可见。

如果用户继续按Back按钮,那么stack中的Activity会顺次弹出并暴露出前一个Activity,直到用户返到Home屏幕(或者直到启动这个task的Activity),当所有的activity全部都从stack中移除后,stack也不复存在了。

在用户启动一个新task或者按home按钮返回到home屏幕时,task是一个可以整体移至后台的单位。当移入后台时,task中的所有activity都会进入stop状态,失去了焦点,显示如同图2

图2 (有两个task, taskB接受到用户的操作移到前台,同时taskA 移于后台等待重新恢复到前台)

 

task可以恢复到前台,用户得以可以继续操作,相反,例如,当前task A在stack中保存有三个activity,有两个activity在当前activity的下面。用户按下Home按钮,然后从Launcher桌面上启动一个新的app, 当home屏幕出现时,task A进入后台,当新的app启动时,系统为它启动一个新的task B用来存放stack, 在用户和app交互后,用户再次返回home屏幕,并再次选择原来启动task A的app。 现在,task A再次回到前台,在它的stack中依然保持着三个activity,stack顶部的activity变为可见。同理,用户也可以通过home键返回home屏幕,并再选择进和task B的app图标(或者从最近使用的APP)。以上是一个Android多task的例子。

注意:多task可以被保存后台,然而,如果用户在后台运行太多的task时,系统可以销毁后台的activity用于恢复内存,这会引发activity的状态丢失。

在stack中的activity重来不会被主动重新排序,但如果用户主动请求一个activity多次时,一个新activity实例会被创建并压入栈中(而不是把前面已创建的实例移于栈顶)。如图3

图3

 

如果用户用back按钮返回时,每一个activity实例都会得以展示(带有自己的ui状态)。然而,你可以修改这个行为,如果你不希望activity被实例化多次。如果做呢?见后面讨论的管理task。

总结一下activity和task的默认行为:

  • 当activity A启动activity B, Activity A被stop, 但系统会保存它的ui状态(如滚动条的位置,表单中输入的文字)。如果用户在Activity B按下Back按钮, Activity A会重新显示并恢复它的状态。
  • 当用户离开task通过按下home键,当前的activity会被stop,它对应的task会进入后台,系统会保留task中的每一个activity的状态,如果用户稍后选择app icon,可以重新切回task,task的栈顶activity变为可见
  • 如果用户按下back按钮,当前activity会被弹出栈顶划被销毁,栈中的前一个activity变为可见。当activity被销毁时,系统不再保留它的状态。
  • activity可以被创建多次,甚至可以位于不同的task

管理Task

如上所述,Android管理task和stack的方式就是把所有的activity一连串的放在同一个task中并按“后进,先出”的原则管理。你可以打破这一惯例,也许你希望一个activity启动时时放在一个新的task中(而不是当前task),或者,你希望启动一个已经存在的activity,而不是在栈顶再创建一个新的activity实例。或者你希望在用户离开task时只保留栈底的根activity,并清除掉其它的activity

你可以做到或做到更多,通过在manifest的<activity>声明中带上属性或者通过在startActivity()时使用Intent带上标记。

相关的属性如下:

  • taskAffinity
  • launchMode
  • allowTaskReparenting
  • clearTaskOnLaunch
  • alwaysRetainTaskState
  • finishOnTaskLaunch

相关的标记如下:

  • FLAG_ACTIVITY_NEW_TASK
  • FLAG_ACTIVITY_CLEAR_TOP
  • FLAG_ATIVITY_SINGLE_TOP

下面的章节你会看到如何用属性和标记定义activity和task的关联,以及activity在stack中的行为。

定义Launch mode

Launch mode允许你定义activity实例与当前task如何关联,你可以定义不同的启动方式,通过下面介绍的两种方式:

  • 用manifest
  • 用Intent flag

有时可能会出现这种情况,例如Activity A 启动Activity B,Activity B可以在manifest中定义它如何与当前task关联,而Activity A也可以通过Intent来请求Activity B如何与当前task关联。这时,后者优先。

注意:有一些Launch mode在manifest中有效,而在flag中无效,反之亦然。

使用manifest配置

通过launchMode属性,它指定activity应该在哪一个task中启动,有四种不同的launch mode属性值:

  • standard(默认值) 系统在当前task中创建一个activity实例。activity可以被实例化多次,每一个实例可以属于不同的task,一个task中也可以有多个实例。

  • singleTop 如果一个activity实例已经位于当前task顶端,系统会发送给它intent并调用onNewIntent(), 而不是创建一个新的实例。Activity可以实例化多次,每一个实例可以属于不同的task,每一上task可以拥有多个实例(仅出现在stack顶端不存在activity的实例时)

例如,task的栈由根activity A,B,C, D在顶部,一个itent到达Activity D, 如果D是standard属性,一个新的实例会创建,stack成为A-B-C-D-D,然而,如果D是singleTop属性,那么D会收到intent并执行onNewIntent(),这时栈保持A-B-C-D。然而,如果itnent是发给B的,B如果是singleTop,那么B的实例会加入栈。

注意:当一个新的activity实例被创建,用户可以按back键返回前一个activity. 但是当存在的activity处理了新的intent执和onNewIntent(),那用户不能通过back返回到以前的状态.

  • singleTask 系统创建一个新的task并实例activity放在task的栈底。然而,如果activity实例已存在一个task中,系统会发送intent并调用onNewIntent(),而不是创建一个新的实例。在任一时刻,只有一个activity的实例。

注意:虽然activity在一个新的task中启动,back操作仍可以返回前一个activity.

  • singleInstance 如同singleTask,除了系统不会将其它activity放入保存当前的task中,这个activity永远单独存在于自己的task中;这个activity启动任意其他activity都会在其它的task打开.

举一个例子,Android浏览器app声明的web activity应该总是在它自己的task中打开--通过指定singleTask属性。这意味着,如果你的APP打开Android Browser,web activity不会出现在你的app页面对应的task,不论web activity是否已经存在。

不管activity是否启动新task还是和启动它的Activity在同一个task,back键总是将用户带到前一个activity.然而,如果你启动activity用singleTask,并且如果activity已经存在于一个后台task,那么整个task会移置前台。从这一点上讲,stack现包括了从task转移所有的activity。图4说明这一场景。

 

图4说明singleTask的activity如何被加入stack,如果activity已经是后台task的自有stack中,那么整个stack都被移到前台,在当前task的头部。

使用Intent标记

有如下标记可用:

  • FLAG_ACTIVITY_NEW_TASK 启动一个activity在一个新的task. 如果task已经存在并运行着一个你想启动的Activity,那么这个task被置于前台,并恢复activity的状态,调用onNewIntent() 这个行为与上面讨论的singTask Launch model相同。

  • FLAG_ACTIIVTY_SIGNLE_TOP 如果activity已经启动并且位于stack顶部,那么会被调用onNewIntent(),而不是创建一个新的实例。 这个行为同singleTop Launch model

  • FLAG_ACTIVITY_CLEAR_TOP 如果activity已经在当前task中运行,那么就不会新建实例,在其上的所有activity都会被销毁,这个activity会移到stack顶部并调用onNewIntent 这个值在launchMode中无对应配置

FLAG_ACTIVTY_CLEAR_TOP通常与FLAG_ACTIVITY_NEW_TASK连用,用于定位一个在其他task中已存在的activity,并移于task头部。

注意:如果launchMode设置为standard, 它也会被从stack中移除,而是重新创建一个新的实例,那是因为在standard模式下一个新的Intent就会导致创建一个新的activity实例。

处理affinity

affinity指task应该归属于哪个task. 默认情况下,同一个app的所有activity都有相同的affinity。所以默认情况下,同一个app的所有activity都属于同一个task,然而,你可以修改这一行为。不同app的activity可以共享affinity,所以同一app的activity也可以指定不同的affinity.

你可以修改<activity>的taskAffinity属性。 的taskAffinity属性是一个字符串值,它必须区分与<manifest>中定义的包名,因为系统使用这个名字做为默认affinity的值

affinity在两个状态下使用

  • 当启动activity的Intent包括FLAG_ACTIVITY_NEW_TASK标记。默认情况下,一个activity调用startActivity()会导致,一个新的activity实例创建并放在同一个task的stack中,然而如果intent带有FLAG_ACTIVITY_NEW_TAS标记, 系统会寻找一个新的task存放新的Activity,通常会新建一个task,然而也可以不必创建,只要已经存在一个task且task的affinity与新activity的affinity相同,那么新的activity就会被放在task中,而不用新建。 如果activity放在新建的task中,那么用户点击home按钮离开activity时,就必须有留一条路让用户可以重新导航回来。例如,通知管理器总是启动activity在一个新的task中,因为它发出的intent总是带有FLAG_ACTIVITY_NEW_TASK, 如果你的activity可以从外部调用可以使用这种方式,如通过launch icon重新返回。

  • 当activity的allowTaskReparenting属性设为true 在这种情况下,activity可以根据affinity从一个后台的task移到一个前台的task

举例,假设一个旅行内容的APP,它有一个根据选择的城市显示天气的activity, 这个Activity与应用里其他activity具有相同的affinity,并且它的allowTaskReparenting属性设为true。当你的APP调用天气的activity时,它会进入你activity对应的task, 然而,当旅行app置入前台时,它又会重新回到原来的task中。

清除stack

如果用户离开task较长时间,系统会清除掉task中所有的actvity,除了根activity,当用户再次返回task,只有根activity可以恢复状态,因为系统认为超出一定的时间,用户很可能已经放弃他们以前浏览的页面,返回时会新建页面。

有一些activity属性可以让你修改这一行为。

  • alwaysRetainTaskState 如果一个task的根activity的属性设为true, 那么默认的行为就不会发生。task会保留所有的activity在Stack中,即使用户离开很长时间。

  • clearTaskOnLaunch 如果一个task的根activity的属性设为true, 那么Stack中activity会被清除,但会保留根activity,换句话说,他是alwaysRetainTaskState相反的特性,用户返咽task时总是会发生又重新回到初始状态,即使用户只是离开了一小会儿。

  • finishOnTaskLaunch 这个属性比较象clearTaskOnLaunch,但是它只操作在一个activity上,而不是作用于整个task, 它可能让任何一个activity离开,包括根activity。当他设置为true时,task中只保留部分activity,当前activity不存在了。

启动task

你可以设置一个activity做为task的入口点,以"android.intent.action.MAIN" action和 "android.intent.category.LAUNCHER" category, 这种类型的Intent filter会导致一个icon和标题显示在桌面上,让用户可以启动它并再次返回task.

第二个能力很重要,用户可以离开task后再次点icon返回task,因为这个原因,有两个launchMode标识的Activity总是会初始化一个task, "singleTask" 和 "singleInstance",这两个launchMode只应该在有 ACTION_MAIN和CATEGORY_LAUNCHER filter时使用。想象一下,如果filter缺失会发生什么:一个Intent启动一个singleTask的activity, 初如化一个task,然后用户在task工作了一段时间,用户按下home键,task进入后台不可见。现在用户再也无法返回task,因为他没有出现在launch桌面上。 为了某些原因,你不希望用户可以重新返回Activity, 你可以设置<activity>的属性finishOnTaskLaunch为true.