重拾Android之路之Activity的四种启动模式


引言

对Android的启动模式还有些印象,现在项目的一个需求是:登录页是一个登录选择页,包含了帐号密码登录、手机号登录、微信第三方登录几种选择,选择某种登录方式意味着进入某个登录页面,点击“返回键”能返回到登录选择页,某个注册/登录页面注册/登录成功后当前页面需要销毁,登录选择页面也需要销毁。

带着这样的需求,我们进入主题。


Activity的四种启动模式:

Android的启动模式(android:launchMode,我们在AndroidMainfest中,给Activity添加这个属性,就可以设置启动模式):

  • standard(默认启动模式):在默认启动模式下,无论我们之前创建过这个Activity,还是没有创建过,他都会重新创建一个。

  • singleInstance(全局单一模式):每一个Activity都存放到一个新的任务栈(Task)中,所以他们都是位于栈顶。

  • singleTop(栈顶复用模式)

  • singleTask(栈内复用模式)


Task
  • 一个后进先出的栈结构,用来保存调度activity
standard (默认的启动模式)
  • 默认只在当前task启动
  • 启动时会新建一个activity实例放到栈顶
singleTop(栈顶复用模式)
  • 默认只在当前task启动

  • 当 当前task的栈顶已经有一个相同的Activity,则不会创建新的activity,而是会调用栈顶activity的onNewIntent()方法

  • 栈顶不存在相同Activity时,会新建一个activity实例放到栈顶

singleTask
  • 默认可以在其他task启动,但是到底会不会在其他task启动需要依赖taskAffinity具体分析

  • 当 当前task中已经存在一个相同的Activity时,会将task中位于该Activity实例上方的activity出栈直到该Activity实例位于栈顶(类似Intent中添加FLAG_ACTIVITY_CLEAR_TOP的效果)

  • 对一个singleTask的Activity类,系统中只会存在一个实例

singleInstance(独享task)
  • 默认新建task,并独占该task

  • singleInstance的Activity中启动的task会在其他task

  • 系统中同样只会存在一个实例


絮叨一下基本概念

一个应用程序当中通常都会包含很多个Activity,每个Activity都是一个具有特定的功能,并且可以让用户进行操作的组件。

另外,Activity之间可以相互启动,当前应用Activity甚至可以去启动其他应用Activity。比如你的应用希望去发送一封邮件,你就可以定义一个具有"send"动作的Intent,并且传入一些数据,如对方邮箱地址、邮件内容等。这样,如果另外一个应用程序中的某个Activity声明自己是可以响应这种Intent的,那么这个Activity就会被打开。当邮件发送之后,按下返回键仍然还是会回到你的应用程序当中,这让用户看起来好像刚才那个编写邮件的Activity就是你的应用程序当中的一部分。所以说,即使有很多个Activity分别都是来自于不同应用程序的,Android系统仍然可以将它们无缝地结合到一起。那这一切是怎么实现的呢?这就要讲到Activity任务栈以及Activity启动模式了。

任务栈是什么

任务栈Task,是一种用来放置Activity实例容器,它是以的形式进行盛放,也就是所谓的先进后出,主要有2个基本操作:压栈出栈,其所存放的Activity是不支持重新排序的,只能根据压栈和出栈操作更改Activity的顺序。

启动一个Application的时候,系统会为它默认创建一个对应的Task,用来放置根Activity。默认启动Activity会放在同一个Task中,新启动的Activity会被压入启动它的那个Activity的栈中,并且显示它。当用户按下回退键时,这个Activity就会被弹出栈,按下Home键回到桌面,再启动另一个应用,这时候之前那个Task就被移到后台,成为后台任务栈,而刚启动的那个Task就被调到前台,成为前台任务栈Android系统显示的就是前台任务栈中的Top实例Activity。

任务栈的作用

以往基于应用(application)的程序开发中,程序具有明确的边界,一个程序就是一个应用,一个应用为了实现功能可以采用开辟新线程甚至新进程来辅助,但是应用与应用之间不能复用资源和功能。而Android引入了基于组件开发软件架构,虽然我们开发android程序,仍然使用一个apk工程一个Application的开发形式,但是对于Aplication的开发就用到了Activityservice等四大组件,其中的每一个组件,都是可以被跨应用复用的,这就是android的神奇之处。虽然组件可以跨应用被调用,但是一个组件所在的进程必须是在组件所在的Aplication进程中。由于android强化了组件概念,弱化了Application的概念,所以在android程序开发中,A应用的A组件想要使用拍照或录像的功能就可以不用去针对Camera类进行开发,直接调用系统自带的摄像头应用(称其B应用)中的组件(称其B组件)就可以了,但是这就引发了一个新问题,A组件运行在A应用中,B组件运行在B应用中,自然都不在同一个进程中,那么从B组件中返回的时候,如何实现正确返回到A组件呢?Task就是来负责实现这个功能的,它是从用户角度来理解应用而建立的一个抽象概念。因为用户所能看到的组件就是Activity,所以Task可以理解为实现一个功能而负责管理所有用到的Activity实例的栈。

是一个先进后出线性表,根据Activity在当前栈结构中的位置,来决定该Activity的状态。正常情况下,当一个Activity启动了另一个Activity的时候,新启动的Activity就会置于任务栈的顶端,并处于活动状态,而启动它的Activity虽然成功身退,但依然保留在任务栈中,处于停止状态,当用户按下返回键或者调用finish()方法时,系统会移除顶部Activity,让后面的Activity恢复活动状态。当然,世界不可能一直这么“和谐”,可以给Activity设置一些“特权”,来打破这种“和谐”的模式,这种特权,就是通过在AndroidManifest文件中的属性andorid:launchMode来设置或者通过Intentflag来设置的。

下面就先介绍下Activity的几种启动模式。


图解Activity的启动模式

standard

默认模式,可以不用写配置。在这个模式下,都会默认创建一个新的实例。因此,在这种模式下,可以有多个相同的实例,也允许多个相同Activity叠加。应用场景:绝大多数Activity。

standard模式

如果以这种方式启动的Activity被跨进程调用,在5.0之前新启动的Activity实例会放入发送Intent的Task的栈的顶部,尽管它们属于不同的程序,这似乎有点费解,看起来也不是那么合理,所以在5.0之后,上述情景会创建一个新的Task,新启动的Activity就会放入刚创建的Task中,这样就合理的多了。

singleTop

栈顶复用模式,如果要开启的activity在任务栈的顶部已经存在,就不会创建新的实例,而是调用 onNewIntent() 方法。避免栈顶的activity被重复的创建。应用场景:在通知栏点击收到的通知,然后需要启动一个Activity,这个Activity就可以用singleTop,否则每次点击都会新建一个Activity。当然实际的开发过程中,测试妹纸没准给你提过这样的bug:某个场景下连续快速点击,启动了两个Activity。如果这个时候待启动的Activity使用 singleTop模式也是可以避免这个Bug的。

singleTop

standard模式,如果是外部程序启动singleTop的Activity,在Android 5.0之前新创建的Activity会位于调用者的Task中,5.0及以后会放入新的Task中。

singleTask

栈内复用模式, activity只会在任务栈里面存在一个实例。如果要激活的activity,在任务栈里面已经存在,就不会创建新的activity,而是复用这个已经存在的activity,调用 onNewIntent()方法,并且清空这个activity任务栈上面所有的activity。应用场景:大多数App的主页。对于大部分应用,当我们在主界面点击回退按钮的时候都是退出应用,那么当我们第一次进入主界面之后,主界面位于栈底,以后不管我们打开了多少个Activity,只要我们再次回到主界面,都应该使用将主界面Activity上所有的Activity移除的方式来让主界面Activity处于栈顶,而不是往栈顶新加一个主界面Activity的实例,通过这种方式能够保证退出应用时所有的Activity都能销毁

在跨应用Intent传递时,如果系统中不存在singleTask Activity的实例,那么将创建一个新的Task,然后创建SingleTask Activity的实例,将其放入新的Task中。

1:假如目前有个任务栈T1中的情况是ABC,这个时候Activity D以singleTask模式请求启动,其所需要的任务栈正是T1,则系统会直接创建D的实例并将其入栈到T1中。

singleTask1

2:假如D Activity启动所需要的任务栈为T2,由于T2和D的实例均不存在,那么系统会先创建任务栈T2,然后再创建D的实例并将其入栈到T2中。我们可以通过设置Activity的taskAffinity属性来模拟这一场景。

<activity 
 android:name=".SingleTaskActivity" 
 android:label="singleTask launchMode" 
 android:launchMode="singleTask" 
 android:taskAffinity="">
</activity>

3:如果D所需的任务栈为T3,并且当前任务栈T3的情况为ADBC,根据栈内复用的原则,此时D不会重新创建,系统会把D切换到栈顶并调用其onNewIntent()方法,同时由于singleTask默认具有ClearTop的效果,会导致栈内所有在D上面的Activity全部出栈,于是最终T3的情况为AD。

4:假如目前有两个任务栈,前台任务栈T4的情况为AB,后台任务栈t4里存有CD,假设CD的启动模式均为singleTask,现在由B去启动D,那么整个后台任务都会被切换到前台,这个时候整个栈就变成了ABCD。

5:假如上面的其他条件不变,B启动的是C而不是D,那么整个栈的情况就变成了ABC,因为D在C上面,会被清理出栈。

singleInstance

单一实例模式,整个手机操作系统里面只有一个实例存在。不同的应用去打开这个activity 共享公用的同一个activity。它会运行在自己单独独立任务栈里面,并且任务栈里面只有它一个实例存在。应用场景:呼叫来电界面。这种模式的使用情况比较罕见,在Launcher中可能使用。或者你确定你需要使Activity只有一个实例。建议谨慎使用。

设置Intent的Flag

系统提供了两种方式来设置一个Activity的启动模式,除了在AndroidManifest文件中设置以外,还可以通过IntentFlag来设置一个Activity的启动模式,下面我们简单介绍一些Flag

  • FLAG_ACTIVITY_NEW_TASK

使用一个新的Task来启动一个Activity,但启动的每个Activity都将在一个新的Task中。该Flag通常使用在从Service中启动Activity的场景,由于Service中并不存在Activity栈,所以使用该Flag来创建一个新的Activity栈,并创建新的Activity实例。

  • FLAG_ACTIVITY_SINGLE_TOP

使用singletop模式启动一个Activity,与指定android:launchMode=“singleTop”效果相同。

  • FLAG_ACTIVITY_CLEAR_TOP

使用SingleTask模式来启动一个Activity,与指定android:launchMode=“singleTask”效果相同。

  • FLAG_ACTIVITY_NO_HISTORY

Activity使用这种模式启动Activity,当该Activity启动其他Activity后,该Activity就消失了,不会保留在Activity栈中。

LaunchMode与StartActivityForResult

我们在开发过程中经常会用到StartActivityForResult方法启动一个Activity,然后在onActivityResult()方法中可以接收到上个页面的回传值,但你有可能遇到过拿不到返回值的情况,那有可能是因为Activity的LaunchMode设置为了singleTask。5.0之后,android的LaunchModeStartActivityForResult的关系发生了一些改变。两个Activity,A和B,现在由A页面跳转到B页面,看一下LaunchMode与StartActivityForResult之间的关系:

这是为什么呢?

这是因为ActivityStackSupervisor类中的startActivityUncheckedLocked方法在5.0中进行了修改。在5.0之前,当启动一个Activity时,系统将首先检查Activity的launchMode,如果为A页面设置为SingleInstance或者B页面设置为singleTask或者singleInstance,则会在LaunchFlags中加入FLAG_ACTIVITY_NEW_TASK标志,而如果含有FLAG_ACTIVITY_NEW_TASK标志的话,onActivityResult将会立即接收到一个cancel的信息,而5.0之后这个方法做了修改,修改之后即便启动的页面设置launchMode为singleTask或singleInstance,onActivityResult依旧可以正常工作,也就是说无论设置哪种启动方式,StartActivityForResultonActivityResult()这一组合都是有效的。

所以如果你目前正好基于5.0做相关开发,不要忘了向下兼容,这里有个坑请注意避让。

实际需求示例

现在有个这样的需求,登录页是个选择登录页,里面含有帐号密码登录方式B、手机号登录/注册方式C、微信第三方登录D等几种选择,视为activity A。点击A中的某个button实现activity的跳转,比如B,在B中返回能跳到登录选择页。当B/C/D成功登录时,应跳转到主页面MainAcitivity E,此时点返回键应该是直接跳出应用,而不是又跳转会A/B/C/D页面去。单单是想让B/C/D页面消失,其实很容易,在做activity跳转时,finish当前页面即可。但页面A是没有办法清楚的,解决方案如下:

Intent intent = new Intent(A.this,B.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

且看看FLAG_ACTIVITY_CLEAR_TASK 的注释:

/**
     * If set in an Intent passed to {@link Context#startActivity Context.startActivity()},
     * this flag will cause any existing task that would be associated with the
     * activity to be cleared before the activity is started.  That is, the activity
     * becomes the new root of an otherwise empty task, and any old activities
     * are finished.  This can only be used in conjunction with {@link #FLAG_ACTIVITY_NEW_TASK}.
     */
    public static final int FLAG_ACTIVITY_CLEAR_TASK = 0X00008000;

源码中明确说明如果在startActivity的时候传递FLAG_ACTIVITY_CLEAR_TASK这个标志,那么这个标志将会清除之前所有已经打开的activity.然后将会变成另外一个空栈的root,然后其他的Activitys就都被关闭了.这个方法必须跟着{@link #FLAG_ACTIVITY_NEW_TASK}一起使用.

总结

实际开发过程中如果采用比较合理的Activity启动模式来做好任务栈的管理,可以事半功倍。在launchMode的选择上首先要搞清楚当前的Activity的作用,以及实际使用场景来做出合理选择。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容