今天想来说说Android的启动模式,一来自己做一个总结,二来如果能帮助到别人就更好了~
首先来看一个实际的业务场景。我之前在公司实习的时候,我所在的部门只负责一个品类(国际机票),那用户从机票首页开始搜索机票到最终完成订单并支付大致分为以下几个流程(实际流程因为考虑的问题比较多,所以要比这稍微复杂些):
1. 用户在机票首页确认好出发、到达目的地、日期及人数后,就可以点击“搜索”进入到搜索结果页
2. 用户根据意愿选择相应航班并点击进入确认订单页
3. 用户确认好订单内容点击下单,开始支付,支付完成跳支付完成页
4. 用户可以选择回到首页,或者去公共订单系统查看订单详情(这属于公共部分,不在我们讨论的范围内了)
经过上面4个步骤整个购票流程就算结束了。但是,这里有个问题,就是用户从首页到最后的支付完成页之间经历了这么多的Activity,那用户完成了支付后想回到首页是不是要一层一层的往回退才行?如果是这样的话,那就太不人性化了,那怎样才能在完成支付后一键回到首页呢?有人可能会说,那就直接从支付完成页跳转到首页不就可以了吗?这样当然是可以的,但是这样做那之前打开的那么多Activity怎么办呢?难道就让它们待在Activity任务栈中吗?这样岂不是太浪费内存了!请看我画的示意图:
如上图所示,如果我们在支付完成之后启动首页Activity那页面1-页面5之间的所有Activity对用户来说都是无用的,并且占用的内存,极大地浪费这本来可能就很紧张的Android内存资源。那有没有一些优雅的方式来解决这个问题呢? 我们可以想象一下,有没有可能在从页面4跳转首页的时候把原来处于首页之上的所有Activity全部干掉呢?这样不就刚好解决了我们刚刚所说浪费资源的问题了吗?就像下图一样:
很显然,无论从用户体验的角度还是从内存优化的角度来看第二种方式都是最为优雅的。
那怎么实现这种需求呢?有人可能会说,可以将任务栈中页面1之上的所有Activity一层一层地执行finish()方法销毁掉,这样就可以回退到页面1。这样确实是可行的,但是有个问题:如果希望从页面4跳转到页面1时传递一些数据回去,比如说订单号、支付完成等信息,还需要在finish()执行前执行setResult(int resultCode, Intent intent)将要往回传递的数据放在Intent里面。
这样做其实是有风险的,因为Android系统并不保证Activity任务栈中那些不可见的Activity的状态是一直被保存着的,如果出现系统内存不足的情况,Android系统是可以回收那些处于不可见状态的Activity的。也就是说,一旦中间有一Activity被销毁了,那这个传递链就失去功效了。
那应该怎么办呢?别着急,Google的Android工程师早就已经为我们想到这种需求了。那应该怎么做呢?其实就是通过设置Activity的启动模式来实现。好,问题抛出了,下面让我们一步一步来看。
Activity有哪几种启动模式?
Activity一共有4种启动模式,分别是:
- standard
- singleTop
- singleTask
- singleInstance
下面我来分别做介绍。
一、standard
顾名思义,standard英文意思就是“标准的”。
也就是说这种启动模式是默认的,我们平时在开发中使用最多的就是Standard模式的。
如果一个Activity的启动模式被设置成standard,那么它可以无限制的创建。你每一次通过Intent去启动这种模式的Activity都会重新创建一个。
大家可以想象一下邮箱里的收件箱(假设我们将打开邮件的Activity的启动模式设置为Standard,当然这也是默认的模式)里有10封邮件。我们给查看邮件的Activity起名为CheckEmailActivity,我点击第一封邮件将会打开一个CheckEmailActivity,当我看完之后点击下一封邮件,另一个CheckEmailActivity又会被创建,这样如果我们将10封邮件全部看完,那在Activity任务栈中将会有10个CheckEmailActivity,而且如果我想回到收件箱页面还必须点10次返回键!想想是不是很可怕?
所以说standard模式虽然很常用,但也不是适用于任何场合。
另外说一点,standard模式在Android 5.0(Lollipop)之前和之后是有区别的。
** Android Lollipop之前**
standard模式的Activity总是会被创建在启动它的Activity同一个任务栈中顶端(任务栈是一个栈结构,先进后出 First In Last Out),就算他们来自不同的应用。
想象一个场景,如果你在A应用中要分享一个本地图片,这样会打开系统的图片查看应用中的图片选择器Activity,虽然这两个Activity来自不同的应用,但Android系统仍将会把他们放在同一个任务栈中,即A应用的任务栈中。
Android Lollipop之后
如果将要启动的Activity和启动它的Activity来自同一个应用,那没话说,和Lollipop之前一样,新的Activity会被创建在当前任务栈中的顶端。
但是如果它们来自不同的应用,那就会创建一个新的任务栈,再把要启动的Activity放在新的任务栈中,这时这个新启动的Activity就是新创建的任务站点的根Activity。如下图所示:
二、singleTop
顾名思义,singleTop的意思就是“在顶部只能有一个”。
这种启动模式非常类似于standard,但是也有一些 区别:
如果在启动这种模式的Activity的时候,当前任务栈的顶端已经存在了相同的Activity,那系统就不会再创建新的,而是回调任务栈中已经存在的该Activity的onNewIntent( )方法。请看下面的示意图:
也正因为SingleTop启动模式的特殊性,所以在开发时,如果指定了一个Activity的启动模式是singleTop的那就应该既要重写onCreated()方法用于应对第一次创建的情况,也要重写onNewIntent( )方法来应对重复创建的情况。
其实大家可以想象一下,这种启动模式的应用场景。Android既然提供了这种启动模式,说明肯定有应有场景需要这样的方式。其实最常用的场景就是搜索,比方说我们在搜索框中输入想要搜索的内容点击搜索进入SearchResultActivty(搜索结果页)查看搜索的结果(一般我们也会在搜索结果页提供搜索框,这样用户无需点击返回键回到上一个页面再在搜索框中输入搜索内容点击搜索),如果此时用户还想搜点别的东西,就可以直接在当前的搜索结果页SearchResultActivty中的搜索框输入搜索内容继续搜索。
大家想象一下,如果我们把SearchResultActivty的启动模式设置为Standard的话会是什么样的景象。比如我们连着搜了10个内容,那就会启动10个不同的SearchResultActivty,然而这些SearchResultActivty功能完全一样,完全没有必要创建这么多,而且还有一个和上一节中的邮箱一样的问题,就是用户搜索结束想回到首页,那就还得按10次返回键才能回到首页,- -!
这时,singleTop启动模式就派上用场了,我们首先把SearchResultActivty的启动模式设置为singleTop,这样用户在SearchResultActivty页面中继续搜索的时候,我们只需把用户要搜索的内容放在Intent里面然后启动SearchResultActivty,这时系统并不会重新创建新的SearchResultActivty,而是回调当前任务栈栈顶的SearchResultActivty的onNewIntent()方法来接收带有用户搜索内容信息的Intent,然后我们拿到用户搜索内容后调搜索接口,并根据接口返回内容重新刷新布局即可,似不似很神奇?其实我们在上一节提到的邮箱的问题,也是用这种方式来解决的,原理和搜索一样的。
三、singleTask
这种启动模式的Activity在Android系统中只允许存在一个实例。
如果系统中已经存在了该种启动模式的目标Activity,则系统并不会重新创建一个目标Activity,而是首先将持有目标Activity的整个任务栈都会被置于前台(用户可见),并且通过onNewIntent( )方法将启动目标Activity的Intent传递给目标Activity,置于目标Activity拿到这个Intent之后要做什么操作,系统就不管了,随便你拿来干什么,哼~。
但是这里有个问题,就是目标Activity和源Activity是不是来自同一应用。
源Activity和目标Activity来自同一个应用
这种情况还要分两种情况说:
当前系统中还没有目标Activity的实例
这种情况最简单,直接在当前的任务栈中创建SingleTask模式的Activity并置于栈顶即可。
当前系统中已经存在目标Activity的实例
这种情况比较特殊,因为系统会把任务栈中目标Activity之上的所有Activity销毁,以让目标Activity处在栈顶的位置。
这里还要还要再提醒大家的是,因为目标Activity已经存在,系统不会重新创建,而是通过onNewIntent()的方式把Intent传递过来,这点和singleTop模式有些类似。注意了,这里让我们回想一下文章开头的我所说的场景,如何让用户在支付完成页直接跳转到首页,并把不需要的Activity销毁?SingleTask启动模式是不是刚好和我们的需求一致?请看下面的示意图:
源Activity和目标Activity来自不同应用
这种情况也要分两种情况说:
当前系统中还没有目标Activity的实例
这时系统首先会看任务管理器中是否有目标Actvity所在应用的任务栈?如果有的话,那就直接在目标Activity所在应用的任务栈的栈顶创建即可。
如果任务管理器中没有目标Activity所在应用的任务栈,系统就会创建其所在应用的任务栈和目标Activity,并且把目标Activity作为新建任务栈的根Activity。如下图所示:
** 当前系统中已经存在目标Activity的实例**
目标Activity所在任务栈会被置于前台(即用户可见),而且也会把目标Activity之上的所有Actvity全部销毁。
四、singleInstance
这种启动模式和singleTask几乎一样,它也只允许系统中存在一个目标Activity,包括上面我们所说的SingleTask的一些特性singleInstance都有。唯一不同的是,持有目标Activity的任务栈中只能有目标Activity一个Actvitiy,不能再有别的Activity,对! 就是承包了这个任务栈!哈哈~。
其实从这种启动模式的名字也可以看出来它表示的意思,singleInstance直译过来就是“单一实例”,什么意思呢?这话啊有两层意思,我来给你分析分析:1. 跟系统说,“我是独一无二的,不许和我一样的人存在!”,这就是说系统中存在一个目标Activity。;2. 跟任务栈说,“我是独一无二的,不许你心里再装别的人!”,这就是说持有目标Activity的任务栈中只能有目标Activity一个Activity。这样说是不是好理解一些,哈哈~
所以,如果要启动singleInstance模式的Activity,那只能新创建一个任务栈用来放它,因为人家说了,“我是独一无二的!”。同样的,如果从这种启动模式的Activity中启动别的Activity,那不好意思,我不管你是不是和我处在同一个应用,我所在的任务栈只能拥有我一个人,您呐,另外让系统给你创建一个任务栈待着去吧。
好了,至此我们介绍了Activity的4种启动模式了,也大致了解了每种启动模式的特点了,那接下里的问题就是怎么使用呢?问题又抛出来了,好,让我们接着往下看。
怎么使用启动模式?
有两种方式来使用或者说设置Activity的启动模式:
方式1:在AndroidMenifest.xml文件中设置:
看到没有,在<activity>标签中设置android:launchMode="****"属性即可,****即我们上面所说的四种启动模式。
方式2:通过为Intent添加标识来设置
看到没有,这里使用Intent的addFlags()方法来添加一些标志,其实这个addFlags()不光可以用来设置Activity的启动模式,还能做很多事情,它的作用是给Intent添加一些附加属性。具体的可以参见Android api哈~
那我们想设置Activity的启动模式应该给addFlags()方法设置哪些参数呢?来,接着往下看:
FLAG_ACTIVITY_NEW_TASK
与"singleTask"启动模式的作用一样。FLAG_ACTIVITY_SINGLE_TOP
与"singleTop"启动模式的作用一样。FLAG_ACTIVITY_CLEAR_TOP
这个标识的意思比较特殊。它不对应于我们上面所说的启动模式中的任何一种,我们来看一下android api中对这个标识的说明:
“如果正在启动的 Activity 已在当前任务中运行,则会销毁当前任务顶部的所有 Activity,并通过onNewIntent()
将此 Intent 传递给 Activity 已恢复的实例(现在位于顶部),而不是启动该 Activity 的新实例。”
大家可能会发现,通过addFlags()的方式来设置启动模式有局限性,只能显示的设置“singleTask”和“singleTop”两种启动模式,而并没有对应“standard”和“singleInstance”启动模式的标识。是的,android api文档中确实只只有以上三种标识用来设置启动模式,而且第三种“**FLAG_ACTIVITY_CLEAR_TOP
**”还不对应任何一种启动模式,难道可以算作第5种启动模式?遗憾的是,我现在也不清楚是怎么回事。
但是实际开发中,我们一般都是在AndroidMenifest.xml文件中去设置Activity的启动模式。
好了,文章差不多就写到这吧。谢谢观看~
多说一句,关于Android任务和返回栈的相关知识可以参见Android api文档,上面说的很详细,其实我习惯把返回栈说成是任务栈,不知道这样是不是合适,嘿嘿~
参考文献:
https://developer.android.com/guide/topics/manifest/activity-element.html
https://developer.android.com/guide/components/tasks-and-back-stack.html
https://inthecheesefactory.com/blog/understand-android-activity-launchmode/en