Android的启动模式
1. 基本介绍
大家平时只要懂一点Android知识的话,都一定会知道,一个应用的组成,往往包含了许多的activity组件,每个activity都应该围绕用户的特定动作进行跳转设计。比如说,一个电话通讯录的应用可能有一个总体展示电话录上所有存储的姓名的activity,当用户选择指定的姓名时,可以启动另一个新的activity用来展示选中此姓名的详细内容。当然,一个activity也可以用来打开同一台手机但存在在其它应用的activity,比如,你的应用想要发送一份邮件时,可以定义一个intent来执行一个"send"动作并包含一个数据(地址和信息),另一个应用中此时有一个刚好可以处理这种intent的activity就会被打开(如果有多个activity支持同样的intent,那么系统就会让用户自行选择一个)。当email被发送后,你的activity被恢复并且看起来发送邮件的activity好像是你应用的一部分。即使两个activity来自不同的应用,Android系统也能将两个activity保存在同一个任务中来实现这种无缝隙的用户体验。这里,我们就引出了一个概念,究竟何为Android中的任务Tasks?它与启动模式又有什么关系?所谓的Back Stack又是什么?在本篇中,我主要分享一下Tasks与Back Stack的基本介绍,为之后介绍android的启动模式做一下铺垫。当然,若你想详细了解此内容,也可以访问官方文档:Task and Back Stack
2. Tasks与Back Stack
一般说来,Tasks是我们在执行某种工作时所交互的activity的集合,这些activity集合按照打开的顺序被放置在同一个栈中,这个栈叫作Back Stack(我称为后退栈)。当我们点击到launcher上的图标时,这个图标对应的应用的task则会被置换到前台。若这个应用不存在task,也说明没有打开过或者打开过但被销毁了,那么就会为这个应用创建一个新的task,此时这个应用的MainActivity则会被创建,然后作为根Activity被压入到这个task中。
当当前的Activity启动了另外一个activity之后,新的activity就会被压入栈顶,并拥有焦点。之前的activity仍然保存在栈中,但是状态是停止的。当activity处于栈中的时候,系统会保留当前界面的状态,当用户按下back键时,当前就activity就会从stack中弹出销毁,之前的一个activity就会从保存的状态中恢复。在栈中的顺序不能被重新安排,只允许在栈上执行压入和弹出。当创建新的activity的时候,压入栈中;当按下后退键的时候,弹出栈中。因此,后退栈是一个“后进先出”的结构体。如下图:
当然,如果我们不停的按后退键,栈中的activity会不停的被弹出,直到回到home界面(或者回到创建task的正在运行的activity)。当所有的activity都从栈中移除之后,这个task就被销毁了。
task可以被整体移到后台,当用户启动了一个新的task,或者按下了home按钮。后台task中的所有activity的状态都是停止的,task的stack中的内容被保存下来,只是task失去了焦点。
我们需要注意的是:后台可以保持多个Task同时存在,但是,若在同一时刻后台运行了太多的Task,这时系统要能会销毁后台的Activity,以用来回收内存,这会导致activity状态的丢失。
3. 保存Activity的状态
按照前面所说的,当Activity停止时(Stopped)时,系统默认会保存其状态。当我们通过back键回到这个Activity时就会恢复离开时候的界面。当然,当多个Tasks同时保存在后台时,系统也有可能会销毁后台的activity,以回收内存。在这种情况下,系统仍然会知道Activity在task中的位置,当通过back键回到这个Activity时,系统会重新创建(recreate)一个Activity,而不是像之前一样恢复(resume)它,因此,为了不丢失Activity的内容,我们可以通过实现onSaveInstanceState()方法来主动保存数据。
4. 总结
通过上面对Task与back stack的学习,相信对于一些概念有了更加清楚的认识,现在我们可以总结一下:
-
activity A 启动activity B,activity A 停止,但是系统还是保留着它的状态。当用户在activity B上按下后退按键,activity A会从保留的状态中的恢复运行。
-
当用户按下home键后离开一个task,这个task的当前activity停止,整个task进入到后台。系统保留着stack中的每一个activity的状态。如果用户点击launcher上的task图标,这个task就会被重新放到前台,task的栈顶activity也会恢复运行。
-
当用户按下后退按钮,栈顶的activity就会从栈中弹出销毁,之前的activity就会恢复运行。activity被销毁后,系统不会保持它的状态。
-
有的activity可以被实例化多次,甚至是从不同的task。
-
当后台中有同时存在多个Task时,系统可能会销毁保存在后台的activity以回收内存,为了不丢失activity中内容,我们可通过onSaveInstanceState()方法保存数据。
Android系统管理Task,是通过将所有的activity按照启动的顺序压入到一个Task中,若一个Activity被启动多次,会默认创建它的多个实例,然后将新的实例压入。当然,我们也可以打破这种默认的行为。可能你想在你应用的activity启动时开始一个新的任务(而不是放置到当前栈中);或者,当你启动一个activity,你想把已经运行的它的一个实例提到前台来(而不是创建一个新的实例放在后退栈的顶端);或者,你希望当用户离开任务时,你的后退栈清除除了根activity以外所有的activity。
activity在栈中默认不能重排,因此,应用中的一个activity可能被多次实例化并且压入同一个栈中,如图所示:
如果此时使用back键返回,activity的每个实例都将会按照打开的顺序重新出现。这势必会导致用户生体验效果,因此要改变这种现象或者解决上篇末尾提到的问题,对启动模式的了解必不可少,当然,若想了解得更加透彻的话,欢迎访问官方文档:Tasks and Back Stack。
定义启动模式
总的来说,启动模式决定了你的activity和task的关联性。当一个activity启动的时候,有两种方式指定它与task的关联:使用manifest文件和使用intent flag。当intent的flag和manifest所指定的启动模式发生冲突的时候,此时就以intent flag指定的模式为准。
-
使用manifest清单文件设置启动模式
通过在manifest文件中添加launchMode属性来指定启动模式:
-
standard,默认的LaunchMode,也是最容易理解的。如果某个Activity使用该 LaunchMode, 当这个Activity启动时,系统会创建一个该Activity的新的实例,并且传递 一个intent给它。该 Activity可以被实例化多次,各个实例可以属于不同的Task,一个Task 中也可以存在多个实例。
-
singleTop,如果这个Activity有一个实例已经存在于当前Task的顶部,那么系统就会传 递一 个intent给这个实例的onNewIntent()方法,而不会去重新创建一个新的Activity实例。 这个 Activity也可以被实例化多次,每个实例可以属于不同的task,但只有当Back Stack栈顶 的Activity实例不是该Activity的实例时,一个task中也可以存在多个实例。应该注意的是,
当一个Activity的新实例创建完毕后,用户可以按返回键返回前一个activity。但是当 Activity已有实例正在处理刚到达的intent时,用户无法用返回键回到onNewIntent()中 intent到来之前的Activity 状态。 -
singleTask,系统创建一个新的Task,并且实例化这个Activity作为这个Task的根 Activity。然而,若这个Activity已经存在了一个实例在一个Task中,那么系统就会将这个 Intent传递到这个Activity的onNewIntent()方法,而不是去重新创建一个实例。同一个时 间,只允许存在一个这样的Activity。注意的是,虽然系统创建了一个新的Task,但是只要按 下返回键还是会回到原来的Activity
-
singleInstance,和"singleTask"类似,不同的是,系统不会再该activity实例的task中,启动任何其他Activity到这个task中。这个Activity是它所在的task中唯一的成员。任何有这个activity启动的Activity都会放入到另外一个task中。
-
对于返回处理,不管activity是在一个新的task启动,还是在当前task中启动,只要一按下返回键,就会返回到之前的那个activity中。但是也是存在一种情况例外,就是当你启动一个启动模式设为singleTask的Activity时,如果这个activity在一个后台task中存在实例,那个这整个task将会被放置到前台,这时候,back stack就会包含这个task中所有的activities,并且它们都是放在栈顶。如下图:
-
使用Intent的flag设置启动模式
当你启动一个Activity时,你也可以动态的设置intent的flag,然后通过startActivity()方法启动activity,从而修改其启动的activity与它的task的关联模式。具体可以使用的flag有:
-
FLAG_ACTIVITY_NEW_TASK:对应之前的“singleTask”,在新的task中启动activity,如果一个你需要的activity的task已经存在,则将它推向前台,恢复其上一个状态,它通过onNewIntent()收到这个新的intent。
-
FLAG_ACTIVITY_SINGLE_TOP:对应之前的“singleTop”,如果被启动的activity是当前顶部的activity,则已经存在的实例会收到onIntent(),而不会重新去创建这个实例。
-
FLAG_ACTIVITY_CLEAR_TOP:这个行为在launchMode属性中没对应的属性值,若被启动的activity已经在当前task中运行,则不会创建它的新实例,而是的销毁在它之上的其他所有的activities,然后通过 onNewIntent()传递一个新的intent给这个恢复了的activity,它一般会与FLAG_ACTIVITY_NEW_TASK一起使用。值得注意的是,如果activity的启动模式是"standard",它自己也将被移除,然后一个新的实例将被启动。这是因为当启动模式是"standard"时,为了接收新的intent必须创建新的实例。
-
任务共用性的处理(Handling affinities)
一般来说,singleTask就是开启一个新的Task,但是在实际使用过程中,我们有时会发现,有时候并不是这样的,这是因为我们定义了affinities,也就是任务公用性。
affinity定义了一个Activity将被分配到哪一个Task中。默认情况下,同一个app中得所有activity有一个同样的affinity,因此,默认情况下同一个应用程序中得所有activity都在同一个task中。一个Task的affinity由这个Task的根Actiivty决定。然而,我们可以修改Activity默认的affinity,这样,不同应用程序的Activity可以共用同样的affinity,或者同一个程序的不同Activity分配不同的affinity。一个应用程序默认的affinity就是应用程序的包名,所以,如果我们想定义一个不同的affinity,必须和默认的affinity不同。我们可以通过中得android:taskAffinity修改整个程序的affinity,也可以通过的android:taskAffinity对单个Activity的affinity修改。
而affinity的使用通常有以下两种情况,
-
当android:launchMode是singleTask或者Intent中包含FLAG_ACTIVITY_NEW_TASK:
默认情况下,我们调用startActivity(),会实例化一个Activity,放入到与调用者相同的task。但是如果这个Activity的的启动模式是singleTask,或者启动它的Intent包含了
FLAG_ACTIVITY_NEW_TASK时,系统会进行如下的步骤:- 判断这个Activity有没有实例已经存在了,有的话,直接传递Intent到它的onNewIntent()方法中。
- 如果不存在,系统查找是否有与这个Activity相同affinity的Task已经存在,如果存在,那么就将这个Activity启动到这个Task中。
- 如果不存在这样的Task,那么系统就会创建一个新的Task,并且将这个Activity启动这个Task中,作为根Activity。
因此,从现在看来,只是单纯的用singleTask指定Activity,是不能开辟一个新的Task的,因为我们并没有给他指定affinity。而官方文档对于singleTask的描述,都是基于我们使用了不同的affinity的前提下,只不过是省略了这个描述。所以,我们要明白,singleTask的正确用法,应该是结合affinity使用的。
-
当一个Activity设置allowTaskReparenting属性为true:
这个属性定义了一个Activity,表示是否可以从一个启动它的task,切换到与它相同affinity的task中里去(当这个task切换到前台的时候)。true表示可以移动,false表示它必须呆在启动他得task里。
通常情况下,当一个Activity启动了,那么它就会存在于启动它的task中,并且在整个生命周期中都留在这个task中。但是,我们可以通过这个属性,做出如下改变,当这个Activity当前的Task处于后台,这个时候如果有一个该Activity具有相同affinity的Task被启动到前台,那么这个Activity就可以从它之前的Task,移动到这个新的Task显示。
通常,它的作用是将app中得Activity与app的main task结合起来,举个例子,如下:
有一个e-mail程序,他需要调用浏览器程序的某个Activity(假设为Activity A)来显示一些数据,这个Activity A的该属性设置为true。现在,e-mail程序调用了这个Activity A,在用户看来,好像这个Activity A就是e-mial程序的一部分,因为这个Activity A和这个e-mail程序在同一个task中。现在将e-mail退出到后台,启动浏览器程序,因为Activity A和浏览器程序有相同的affinity,所以Activity A从e-mail程序的Task移动到浏览器程序的Task,并显示在前台。当我们下次再启动e-mail程序时,Activity A就不会存在,因为他已经移动到浏览器程序的Task里去了。
清理Back Stack
如果用户离开一个task很久,系统就会清理这个task中除了根activity之外的所有activities。当用户返回到这个task,只有根activity会被恢复。但是我们可以设置一些activity的属性,用来改变这一行为:
-
alwaysRetainTaskState
如果这个属性在task的根activity中被设置为true,那么上面描述的默认行为不会发生,即便过了很长时间,task仍将会保持所有的activities。
-
clearTaskOnLaunch
如果这个属性在task的根activity中被设置为true,每次用户离开这个task,整个task都会被清到只剩根activity,这样用户只会永远返回到它最初的状态,即便离开的时间很短。
-
finishOnTaskLaunch
这个属性和上一个很像,不同的是,它只作用于单个activity,而不是整个task。它可以引起任何activity离开,包括根activity。当它被设置为true时,这个activity只在当前会话中属于这个task,如果用户离开后再返回,它也不会再出现。
开启一个Task
我们可以通过一个Activity指定一个intent过滤器,如下面:
1 <activity ...> 2 <intent-filter ... > 3 <action android:name="android.intent.action.MAIN" /> 4 <category android:name="android.inp tent.category.LAUNCHER" /> 5 </intent-filter> 6 ... 7 </activity>
这时这个activity就作为根activity存在于一个task中,这个activity也是进入这个task的入口点,同时,它的图标和标签也会被显示在应用启动界面上,这时用户就可以启动这个activity并且再次回到这个任务。因此,从这里我们可以看到,要使用“singleTask”与“singleInstance”,就必须这个activity应当也有ACTION_MAIN与CATEGORY_LAUNCHER过滤器。因为假如没有设置这两个过滤器的话,当一个intent启动一个"singleTask"的activity,在新的Task中进行初使化,运行一段时间后,用户突然按上了home键回到桌面,此时这个Task就被移到后台并且不可见,因为这个activity没有设置过滤器,所以不是应用启动的activity,那么用户也就无法返回到这个Task中了。
总结
今天的分享也差不多接近尾声了,有关Android的启动模式的主要部分的分析也大多涉及到了。我想,如果大家想要全面地了解Android的启动模式的话,我希望可以坚持看完这两篇文章,我相信,看完后你对Android的启动模式还有工作栈的理解应该有了很大的提高,对activity与Task的操作也会更加得心应手。当然,如果你觉得文章中有什么写得错误或者不了解的地方,欢迎留言交流,谢谢!