Android拾萃- Activity的生命周期和启动模式

一、概述

Activity 作为与用户交互的一个窗口,是使用非常频繁的一个基本组件。Android系统是通过Activity栈来管理Activity的,而Activity则是通过哦生命周期来进行自己的创建、活动与销毁等。所以掌握Activity生命周期很有必要。

二、金字塔模型

Activity生命周期

  官方的描述很形象,Activity 生命周期的每个阶段就是金字塔上的一阶。
  当系统创建新 Activity 实例时,每个回调方法会将 Activity 状态向顶端移动一阶。 金字塔的顶端是 Activity 在前台运行并且用户可以与其交互的时间点。
  当用户开始离开 Activity 时,系统会调用其他方法在金字塔中将 Activity 状态下移,从而销毁 Activity。
  在有些情况下,Activity 将只在金字塔中部分下移并等待(比如,当用户切换到其他应用时),Activity 可从该点开始移回顶端(如果用户返回到该 Activity),并在用户停止的位置继续。

这个模型中包含了Activity的六种状态:

Created:创建完成
Started:可见(不可交互)
Resumed:可见(活动)
Paused:部分可见(后台)
Stopped:不可见
Destroyed:销毁

在这六种状态当中,只有ResumedPausedStopped这几种状态在用户没有进一步操作时会保持在该状态,而其余的,都会在执行完相应的回调函数后快速跳过,很容易理解,resumed 状态就是在当前界面,后面两个状态是进入了另一个界面活动,如果打开一个dialog或者透明主题(dialog主题)的Activity,这个时候,只会进入paused状态,不会进入stoped状态。

一般情况下,您不得使用 onPause() 永久性存储用户更改(比如输入表格的个人信息)。 只有在您确定用户希望自动保存这些更改的情况(比如,草拟电子邮件时)下,才能在 onPause() 中永久性存储用户更改。但您应避免在 onPause() 期间执行 CPU 密集型工作,比如向数据库写入信息,因为这会拖慢向下一 Activity 过渡的过程(您应改为在 onStop() 间执行高负载关机操作)。另外一点就是,启动新的Activity,当前的Activity必须onpause进入后台,才会开始启动下一个Activity。

三、异常情况下的生命周期

在有些情况下,您的 Activity 会因正常应用行为而销毁,比如当用户按 返回按钮或您的 Activity 通过调用 finish()示意自己的销毁。 如果 Activity 当前被停止或长期未使用,或者前台 Activity 需要更多资源以致系统必须关闭后台进程恢复内存,系统也可能会销毁 Activity。

当您的 Activity 因用户按了返回 或 Activity 自行完成而被销毁时,系统的 Activity 实例概念将永久消失,因为行为指示不再需要 Activity。 但是,如果系统因系统局限性(而非正常应用行为)而销毁 Activity,尽管 Activity 实际实例已不在,系统会记住其存在,这样,如果用户导航回实例,系统会使用描述 Activity 被销毁时状态的一组已保存数据创建 Activity 的新实例。 系统用于恢复先前状态的已保存数据被称为“实例状态”,并且是 Bundle 对象中存储的键值对集合。

注意:每次用户旋转屏幕时,您的 Activity 将被销毁并重新创建。 当屏幕方向变化时,系统会销毁并重新创建前台 Activity,因为屏幕配置已更改并且您的 Activity 可能需要加载备用资源(比如布局)。

默认情况下,系统会使用 Bundle 实例状态保存您的 Activity 布局(比如,输入到 EditText 对象中的文本值)中有关每个 View 对象的信息。 这样,如果您的 Activity 实例被销毁并重新创建,布局状态便恢复为其先前的状态,且您无需代码。 但是,您的 Activity 可能具有您要恢复的更多状态信息,比如跟踪用户在 Activity 中进度的成员变量。

:为了 Android 系统恢复 Activity 中视图的状态,每个视图必须具有 android:id 属性提供的唯一 ID。

要保存有关 Activity 状态的其他数据,您必须替代 onSaveInstanceState() 回调方法。当用户要离开 Activity 并在 Activity 意外销毁时向其传递将保存的 Bundle 对象时,系统会调用此方法。 如果系统必须稍后重新创建 Activity 实例,它会将相同的 Bundle 对象同时传递给 onRestoreInstanceState() 和 onCreate() 方法。

如果Activity A 启动 B 在启动 C,如果A和B被回收了,这个时候C返回,B会重绘(实例被回收了,但是栈还是在的)

四、由重建引发的窗体泄露

Android的每一个Activity都有个WindowManager窗体管理器,构建在某个Activity之上的对话框、PopupWindow也有相应的WindowManager窗体管理器。因为Dialog、PopupWindown不能脱离Activity而单独存在着,所以当承载某个Dialog或者某个PopupWindow正在显示的Activity被finish()后,而Dialog(或PopupWindow)没有正常退出的话,就会抛Window Leaked错误了,因为这个Dialog(或PopupWindow)的WindowManager已经没有谁可以附属了,所以它的窗体管理器就泄漏了。

在进入新的Activity时突然转屏(哥们开发的sdk支持横竖屏切换),因为在AndroidManifest.xml中没有配置android:configChanges属性,此时Activity会重新调用onCreate方法,即会重新调用整个生命周期,而此时的Dialog已经显示并没有dismiss,所以造成了窗体泄漏。解决的方法就变得如此简单,在AndroidManifest.xml中配置android:configChanges属性,这样当我们横竖屏切换的时候会调用Activity的onConfigurationChanged方法,不会重新调用整个生命周期了。

android:configChanges的一些属性

1、不设置Activity的android:configChanges时,切屏会重新调用整个生命周期,切横屏时会执行一次,切竖屏时会执行两次

2、设置Activity的android:configChanges="orientation"时,切屏还是会重新调用整个生命周期,切横、竖屏时只会执行一次

3、设置Activity的android:configChanges="orientation|screenSize"一起设置的时候才是moshi有效的(原因看下面的表格)。虽然不会重建Activity,但是会回调Activity里面的一个方法: onConfigurationChanged(Configuration config)
在这里你可以监听了,Activity的什么改变了,比如方向,比如弹出了键盘还是隐藏了moshi键盘(清单文件的Activity 添加android:configChanges="keyboard|keyboardHidden“),如果有需要监控其他属性的需求,请参考底下的表格进行属性添加

附上开发艺术探索的一张图(侵删)android:configChanges属性解释:

configchange的项目和含义.png

五、Activity的启动模式

任务栈

我们知道系统使用栈来管理Activity,而栈根据是否在前台,可以划分为前台栈和后台栈(实际没有区别,根据当前的Activity划分,即前台只有一个,后台可能有多个)。

  1. Android任务栈又称为Task,它是一个栈结构,具有后进先出的特性,用于存放我们的Activity组件。
  2. 我们每次打开一个新的Activity或者退出当前Activity都会在一个称为任务栈的结构中添加或者减少一个Activity组件,因此一个任务栈包含了一个activity的集合, android系统可以通过Task有序地管理每个activity,并决定哪个Activity与用户进行交互:只有在任务栈栈顶的activity才可以跟用户进行交互。
  3. 在我们退出应用程序时,必须把所有的任务栈中所有的activity清除出栈时,任务栈才会被销毁。当然任务栈也可以移动到后台, 并且保留了每一个activity的状态. 可以有序的给用户列出它们的任务, 同时也不会丢失Activity的状态信息。
  4. 需要注意的是,一个App中可能不止一个任务栈,某些特殊情况下,单独一个Actvity可以独享一个任务栈。还有一点就是一个Task中的Actvity可以来自不同的App,同一个App的Activity也可能不在一个Task中。

那么系统是怎么划分Activity是在同一个栈里呢?这个时候就要说下TaskAffinity这个属性了。

TaskAffinity属性

TaskAffinity(任务相关性),这个参数标识了一个Activity所需要的任务栈的名字,默认情况下,所有的Activity所需的任务栈的名字为应用的包名.
可以为每个Activity都单独指定TaskAffinity属性,不同的名字代表不同的任务栈android:taskAffinity="属性值为字符串"。
TaskAffinity如何生效

  • TaskAffinity + singleTask (其实就是把singletask放到和包名不一样的栈,singletask单独使用,不代表不能在包名这个栈,他只表示一旦创建之后,只允许一个栈存在一个实例)
  • TaskAffinity + allowTaskReparenting(允许了其他应用的某个Activity无缝迁移进入我们应用的Activity栈,一旦再次打开其他应用的时候,又会迁移回去,具体见下面的图)
  • 其他情况是无效的

可以通过 adb shell dumpsys activity activities 命令查看栈的情况
command + K是terminal的清屏快捷键
l 在adb命令中显示的launchMode代表的数值
standard : launchMode = 0
singleTop : launchMode=1
singleTask: launchMode= 2
singleInstance: launchMode=3

allowTaskReparenting = true 的迁移行为,如下图(来源于网络,侵删)

TaskAffinity_allowTaskReparenting.png

不过有点需要说明的是allowTaskReparenting仅限于singleTopstandard模式,这是因为一个activity的affinity属性由它的taskAffinity属性定义(代表栈名),而一个task的affinity由它的root activity定义。所以,一个task的root activity总是拥有和它所在task相同的affinity。由于以singleTask和singleInstance启动的activity只能是一个task的root activity,因此allowTaskReparenting仅限于以standard 和singleTop启动的activity

四种启动模式

我们应用中有多个Activity组件,之间经常会进行跳转,也有可能需要在本应用中打开其它应用的的Activity。当我们返回上一个组件时,我们更希望复用这个Activity。
但Android系统的stander模式每次都会为我们创建一个新的Activity并添加到Task中。另外,我们开启一次页面,它的数据和信息状态都会被保留,这样会造成数据冗余, 重复数据太多, 最终还可能导致内存溢出的问题(OOM)。

为了解决这些问题,android系统提供了一套Activity的启动模式来修改系统Activity的默认启动行为。目前启动模式有四种,分别是standardsingleTopsingTasksingleInstance,接下来我们将分别介绍这四种模式。

  • Standard 模式
      又称为标准模式,也是系统的默认模式(可以不指定),在这样模式下,每启动一个Activity都会重新创建一个Activity的新实例,并且将其加入任务栈中,而且完全不会去考虑这个实例是否已存在。

  • singleTop 模式
      又称栈顶复用模式,顾名思义,在这种模式下,如果有新的Activity已经存在任务栈的栈顶,那么此Activity就不会被重新创建新实例,而是复用已存在任务栈栈顶的Activity。这里重点是位于栈顶,才会被复用,如果新的Activity的实例已存在但没有位于栈顶,那么新的Activity仍然会被重建。需要注意的是,Activity的onNewIntent方法会被调用,方法原型如下:

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
}

通过此方法的参数,我们可以获取当前请求的相关信息,此时Activity的onCreate、onStart方法不会被调用,因为Activity并没有被重建。
  这种模式通常比较适用于接收到消息后显示的界面,如qq接收到消息后弹出Activity界面,如果一次来10条消息,总不能一次弹10个Activity,是吧?再比如新闻客户端收到了100个推送,你每次点一下推送他都会进入某个activiy界面(显singleTask 模式
   又称为栈内复用模式。这是一种单例模式,与singTop点类似,只不过singTop是检测栈顶元素是否有需要启动的Activity,而singTask则是检测整个栈中是否存在当前需要启动的Activity,如果存在就直接将该Activity置于栈顶,并将该Activity以上的Activity都从任务栈中移出销毁,同时也会回调onNewIntent方法。示新闻只用一个activity,只是内容不同而已),这时也比较适合使用singleTop模式。

  • singleTask 模式
       又称为栈内复用模式。这是一种单例模式,与singTop点类似,只不过singTop是检测栈顶元素是否有需要启动的Activity,而singTask则是检测整个栈中是singleTask 模式否存在当前需要启动的Activity,如果存在就直接将该Activity置于栈顶,并将该Activity以上的Activity都从任务栈中移出销毁,同时也会回调onNewIntent方法。

singleTask 模式比较适合应用的主界面activity(频繁使用的主架构),可以用于主架构的activity,(如新闻,侧滑,应用主界面等)里面有好多fragment,一般不会被销毁,它可以跳转其它的activity 界面再回主架构界面,此时其他Activity就销毁了。

  • singleInstance 模式
      在singleInstance模式下,该Activity在整个android系统内存中有且只有一个实例,而且该实例单独尊享一个Task。换句话说,A应用需要启动的MainActivity 是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A单独在这个新的任务栈中,如果此时B应用也要激活MainActivity,由于栈内复用的特性,则不会重新创建,而是两个应用共享一个Activity的实例。

Activity启动模式的使用方式

如何给Activity指定启动模式呢?事实上共有如下两种方式:
1. 通过AndroidMenifest.xml文件为Activity指定启动模式,代码如下:

<activity android:name=".ActivityC" 
          android:launchMode="singleTask" />

2. 通过在Intent中设置标志位(addFlags方法)来为Activity指定启动模式,示例代码如下:

Intent intent = new Intent();
intent.setClass(ActivityB.this,ActivityA.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

那么什么是标志位呢?常用的标志位有哪一些?

启动标记 Intent Flag

  • Intent.FLAG_ACTIVITY_NEW_TASK
    该标志位表示使用一个新的Task来启动一个Activity,相当于在清单文件中给Activity指定“singleTask”启动模式。

  • Intent.FLAG_ACTIVITY_SINGLE_TOP
      该标志位表示使用singleTop模式来启动一个Activity,与在清单文件指定android:launchMode="singleTop"效果相同。

  • Intent.FLAG_ACTIVITY_CLEAR_TOP
    该标志位表示使用singleTask模式来启动一个Activity,与在清单文件指定android:launchMode="singleTask"效果相同。

  • Intent.FLAG_ACTIVITY_NO_HISTORY
      使用该模式来启动Activity,当该Activity启动其他Activity后,该Activity就被销毁了,不会保留在任务栈中。如A-B,B中以这种模式启动C,C再启动D,则任务栈只有ABD。

  • Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
      使用该标识位启动的Activity不添加到最近应用列表,也即我们从最近应用里面查看不到我们启动的这个activity。与属性android:excludeFromRecents="true"效果相同。

启动模式中singleTask的特殊情景

特殊情景一

前面我们在分析singleTask模式时,提到过singleTask模式有些比较特殊的场景,现在我们就来了解了解它们。
特殊情景一:现在我们假设有如下两个Task栈,分别为前台任务栈和后台任务栈

前台任务栈和后台任务栈

从图中我们看出前台任务栈分别为AB两个Activity,后台任务栈分别为CD两个任务栈,而且其启动模式均为singleTask,此时我们先启动CD,然后再启动AB,再有B启动D,此时后台任务栈便会被切换到前台,而且这个时候整个后退列表就变成了ABCD,请注意我们这里强调的是后退列表,而非栈合并。因此当用户点击back键时,列表中的Activity会依次按DCBA顺序出栈,如下图所示:


back.png
特殊情景二:

如果上面B不是请求启动D而是请求启动C,那么又会是什么情况呢?其实这个时候任务栈退出列表变成C->B->A,其实原因很简单,singleTask模式的ActivityC切换到栈顶时会导致在他之上的栈内的Activity出栈。其他情况都一样。

特殊场景三:
  • StartActivityForResult的时候,requestCode必须>0,否则收不到result
    看下以下关系图
android 5.0之前
Android 5.0之后

这是为什么呢?

这是因为ActivityStackSupervisor类中的startActivityUncheckedLocked方法在5.0中进行了修改。在5.0之前,当启动一个Activity时,系统将首先检查Activity的launchMode,如果为A页面设置为SingleInstance或者B页面设置为singleTask或者singleInstance,则会在LaunchFlags中加入FLAG_ACTIVITY_NEW_TASK标志,而如果含有FLAG_ACTIVITY_NEW_TASK标志的话,onActivityResult将会立即接收到一个cancle的信息,而5.0之后这个方法做了修改,修改之后即便启动的页面设置launchMode为singleTask或singleInstance,onActivityResult依旧可以正常工作,也就是说无论设置哪种启动方式,StartActivityForResult和onActivityResult()这一组合都是有效的。所以如果你目前正好基于5.0做相关开发,不要忘了向下兼容,这里有个坑请注意避让。

TaskAffinity与allowTaskReparenting和singleTask结合时可能发生的应用场景

TaskAffinity与singleTask应用场景

假如现在有这么一个需求,我们的客户端app正处于后台运行,此时我们因为某些需要,让微信调用自己客户端app的某个页面,用户完成相关操作后,我们不做任何处理,按下回退或者当前Activity.finish(),页面都会停留在自己的客户端(此时我们的app回退栈不为空),这显然不符合逻辑的,用户体验也是相当出问题的。我们要求是,回退必须回到微信客户端,而且要保证不杀死自己的app.这时候我们的处理方案就是,设置当前被调起Activity的属性为:

LaunchMode=""SingleTask" taskAffinity="com.tencent.mm"

其中com.tencent.mm是借助于工具找到的微信包名,就是把自己的Activity放到微信默认的Task栈里面,这样回退时就会遵循“Task只要有Activity一定从本Task剩余Activity回退”的原则,不会回到自己的客户端;而且也不会影响自己客户端本来的Activity和Task逻辑。

TaskAffinity与allowTaskReparenting应用场景

一个e-mail应用消息包含一个网页链接,点击这个链接将触发一个activity来显示这个页面,虽然这个activity是浏览器应用定义的,但是activity由于e-mail应用程序加载的,所以在这个时候该activity也属于e-mail这个task。如果e-mail应用切换到后台,浏览器在下次打开时由于allowTaskReparenting值为true,此时浏览器就会显示该activity而不显示浏览器主界面,同时actvity也将从e-mail的任务栈迁移到浏览器的任务栈,下次打开e-mail时并不会再显示该activity

清空任务栈

Android系统除了给我提供了TaskAffinity来指定任务栈名称外,还给我提供了清空任务栈的方法,在一般情况下我们只需要在<activity>标签中指明相应的属性值即可。
  如果用户离开一个task很久,系统就会清理这个task中的所有activities,除了根activity。当用户返回到这个task,只有根activity会被恢复。
  有一些activity的属性,你可以用来改变这一行为:

android:clearTaskOnLaunch

这个属性用来标记是否从task清除除根Activity之外的所有的Activity,“true”表示清除,“false”表示不清除,默认为“false”。这里有点我们必须要注意的,这个属性只对任务栈内的root Activity起作用,任务栈内其他的Activity都会被忽略。如果android:clearTaskOnLaunch属性为“true”,每次我们重新android:clearTaskOnLaunch进入这个应用时,我们只会看到根Activity,任务栈中的其他Activity都会被清除出栈。
  比如一个应用的Activity A,B,C,其中A 的clearTaskOnLaunch设置为true,C为默认值,我们依次启动A,B,C,点击HOME,再在桌面点击图标。启动的是A,而B,C将都被移除当前任务栈。也就是说,当Activity的属性clearTaskOnLaunch为true时将被优先启动,其余的Activity(B、C)都被移除任务栈并销毁,除非前面A已经finish销毁,后面的已注册clearTaskOnLaunch为true的activity(B)才会生效。
  特别地,如果我们的应用中引用到了其他应用的Activity,这些Activity设置了android:allowTaskReparenting属性为“true”,则它们会被重新宿主到有共同affinity的task中。

android:finishOnTaskLaunch

finishOnTaskLaunch属性与clearTaskOnLaunch 有些类似,它们的区别是finishOnTaskLaunch是作用在自己身上(把自己移除任务栈,不影响别的Activity),而clearTaskOnLaunch则是作用在别人身上(把别的Activity移除任务栈),如果我们把Activity的android:finishOnTaskLaunch属性值设置为true时,离开这个Activity所依赖的任务栈后,当我们重新返回时,该Activity将会被finish掉,而且其他Activity不会受到影响。

android:alwaysRetainTaskState

alwaysRetainTaskState实际上是给了当前Activity所在的任务栈一个“免死金牌”,如果当前Activity的android:alwaysRetainTaskState设置为true时,那么该Activity所在的任务栈将不会受到任何清理命令的影响,一直保持当前任务栈的状态。

应用场景:

  1. singleTop适合接收通知启动的内容显示页面。例如,某个新闻客户端的新闻内容页面,如果收到10个新闻推送,每次都打开一个新闻内容页面是很烦人的。聊天的对话窗口,
  1. singleTask适合作为程序入口点。例如浏览器的主界面。不管从多少个应用启动浏览器,只会启动主界面一次,其余情况都会走onNewIntent,并且会清空主界面上面的其他页面。之前打开过的页面,打开之前的页面就ok,不再新建。
    singleTask:a界面购物,b界面确认订单,c界面付款,如果付款成功会跳到a,如果不付款则返回b,这时候重启a就会用到singleTask.

  2. singleInstance适合需要与程序分离开的页面。例如闹铃提醒,将闹铃提醒与闹铃设置分离。singleInstance不要用于中间页面,如果用于中间页面,跳转会有问题,比如:A -> B (singleInstance) -> C,完全退出后,在此启动,首先打开的是B。

  3. standard 标准的启动模式,也是默认的启动模式。

隐式启动Activity,intentFilter匹配规则

这一部分参考的 http://www.jianshu.com/p/151640add690
启动activity分为两种,显式启动和隐式启动。显式:明确指出被调用activity的包名类名,隐式调用不需要明确信息。显式和隐式原则上是不共存的,如果共存以显示为主。隐式启动匹配信息在AndroidManifest的activity中的<intent-filter>,三种过滤信息:action,category,data。三个信息可同时存在多个。intent-filter也可同时存在多个,匹配其中一组intent-filter的三种信息各一种即可。

匹配规则
action

区分大小写,action系统有自定义一些,action匹配字符串必须一样。若intentFilter定义了action属性,隐式启动至少匹配其中一个。

category

Intent未指定category时,系统会默认给Intent增加category属性:<category android:name="android.intent.category.DEFAULT" ,所以如果你隐式启动activity且不想指定category在AndroidManifest总定义隐式启动时,需加上<category android:name="android.intent.category.DEFAULT"。
Intent指定category,指定一个必须正确匹配一个,多个必须正确匹配多个。

data

intentFilter配置data,Intent隐式启动必须匹配至少一个,和action类似
先介绍一种结构 URI:

<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

例如
content://com.example.test:100/folder/subfolder/test
http://www.baidu.com:80/search/info

data的所有匹配属性如下:

<data   
     android:mimeType="string"  
     android:scheme="string"  
     android:host="string"  
     android:port="string"  
     android:path="string"  
     android:pathPrefix="string"  
     android:pathPattern="string"/> 

主要分为两种,一种是mimeType,一种是URI中的其中任何之一属性。

属性简介:

mimeType:媒体类型,image/jpeg,image/png,image/* 、video/等等
Scheme:URI模式,http、file、content等,URI无此参数URI无效
Host:URI主机名,www.baidu.com等,URI无此参数URI无效
Port:URI中端口号
Path/PathPrefix/PathPattern:路径信息,path和pathPattern表示完整的路径信息,pahPatten可包含通配符"",PathPrefix路径的前缀信息。
设置方法三种:

mIntent.setType(mType)
mIntent.setData(mUri)
mIntent.setDataAndType(mUri,mType)

若先setType再setData,mimeType会被清空
若先setData再setType,data会被清空
原因看源码,setType和seData类似

public Intent setData(Uri data) {
        mData = data;
        mType = null;
        return this;
    }

如下两种属性同时使用,标明这是一个入口activity,并且会出现在系统应用列表中

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

推荐阅读更多精彩内容