Android深入探索Activity生命周期与启动模式
Activity的生命周期和启动模式
参考资料:《安卓开发艺术探索》任玉刚
Activity生命周期全面分析
Activity的生命周期可以分为两部分内容,一部分是典型情况下的生命周期,另一部分是异常情况下的生命周期。典型情况下的生命周期,是指用户参与情况下,Activity经过的生命周期的改变。异常情况下的生命周期是指Activity被系统回收或者由于设备Configuration改变而导致的Activity被销毁重建。
典型情况下的生命周期分析
正常情况下,Activity的生命周期如下:
onCreate -> onRestart -> onStart -> onResume -> onPause -> onStop ->onDestroy
| 回调方法 | onCreate | onRestart | onStart | onResume | onPause | onStop | onDestroy |
|---|---|---|---|---|---|---|---|
| 描述 | 标识Activity正在被创建, | 表示Activity正在重新启动。从不可见变为可见时会调用 | 表示Activity正在启动,Activity已经变得可见,但可能还未获得焦点并位于前台与用户交互,Activity还在后台。 | 表示Activity已经可见。此时Activity才显示到前台。 | Activity正在停止,一般紧接着onStop就会被调用。 | 表示Activity即将停止 | Activity即将被销毁 |
| 用处 | 可以做一些初始化工作,比如加载界面布局资源 | 当用户按Home键切换到桌面会调用onPause和onStop,当用户又回到这个活动就会调用onRestart()方法 | 可以做一些存储数据的工作,但是不能太耗时以免影响新Activity的显示。 | 可以稍微做一些重量级的回收工作,但也不能太超时 | 可以做一些回收工作和最终的资源释放 |
这里附加几种启动情况:
- 针对一个特定的Activity,第一次启动,回调如下:
onCreate->onStart->onResume - 当用户打开新的Activity或者切换到桌面时,回调如下:
onPause->onStop。但是如果新的Activity时透明主题,当前Activity不会回调onStop - 当用户再次返回原Activity时,回调如下:
onRestart->onStart->onResume - 当用户按back键退回时,回调如下:
onPause->onStop->onDestroy - 当Activity被系统回收后再次打开,生命周期方法回调过程和 1 一样,即
onCreate->onStart->onResume。但只是生命周期方法一样,不代表所有过程一样,后面会详细说明。 - 在简单的屏幕熄灭和点亮过程中,通常只会触发
onPause->onResume这对“前台生命周期”的调用。尽管屏幕熄灭时对完全不可见,然而,从Android系统的窗口管理器(WindowManager)角度来看,该Activity的窗口(Window)仍然存在且处于活跃状态,它只是被锁屏窗口部分覆盖了。
最后再说一下onStart和onResume,onPause和onStop从描述上看差不多,对我们来说有什么实质性不同
onStart和onStop是从Activity是否可见进行回调的,而onResume和onPause是从Activity是否位于前台的角度进行回调的。
异常情况下生命周期分析
Activity除了受用户操作所导致的正常的生命周期方法调度,还可能有一些异常情况,比如当资源相关的系统配置发生改变以及系统内存不足时,Activity就可能被杀死。
在默认情况下,如果Activity不做特殊处理,那么系统配置发生改变后,Activity就会被销毁并重建,被销毁时其onPause、onStop、onDestroy均会被调用,同时由于Activity是在异常情况下终止的,系统会调用onSaveInstanceState来保存当前Activity的状态。方法调用时机在onStop之前,和onPause没事既定的时序关系,特别强调,这个方法只会在Activity被异常终止情况下调用,正常情况下系统不会回调这个方法。当Activity被重新创建后,系统会调用onRestoreInstanceState方法,并且把Activity销毁时onSaveInstanceState方法保存的Bundle对象作为参数传递给onRestoreInstanceState和onCreate方法。因此如果活动被重建了,我们就可以取出之前保存的数据并恢复,从时序上来说,onRestoreInstanceState的调用时机在onStart之后。
在onSaveInstanceState和onRestoreInstanceState方法中,系统自动为我们做了一定的恢复工作,当Activity在异常情况下需要重建时,系统会默认为我们保存当前Activity的试图结构并在Activity重启后为我们恢复这些数据,比如文本框用户输入的数据、ListView滚动的位置等,这些view相关的状态系统默认会替我们恢复,具体恢复哪些数据我们可以查看View的源码,和Activity一样,每个View都有onSaveInstanceState和onRestoreInstanceState这两个方法。看一下具体实现就能知道系统能自动为每个View恢复哪些数据。
关于保存和恢复View层次结构,系统的工作流程是:首先Activity被意外终止时,Activity会调用onSaveInstanceState去保存数据,然后Activity会委托Window去保存数据,接着Window再委托上面的顶级容器,顶层容器是一个ViewGroup,一般来说很可能是DecorView。最后顶层容器再一一通知它的子元素保存数据。这是一种典型的委托思想,上层委托下层、父容器委托子元素处理一件事情,这种思想在Android中有很多应用,比如View的绘制流程、事件分发等都采用类似思想。
Activity的启动模式
一个app默认只有一个任务栈。当用户点击应用图标启动应用时,系统会为该应用创建一个任务栈(通常以应用包名命名),并将主Activity作为根Activity放入栈中
Activity的LaunchMode
-
standard:标准模式,系统的默认模式。
当我们使用ApplicatonContext去启动standard模式的Activity会报错。这是因为当一个
standard模式的 Activity 被启动时,系统会期望将它放入启动它的那个 Activity 所在的任务栈中,但是ApplicationContext是一个与整个应用生命周期绑定的全局上下文,它自身并不关联任何任务栈。解决方案是添加一个标志位Intent.FLAG_ACTIVITY_NEW_TASKIntent intent = new Intent(getApplicationContext(), TargetActivity.class); // 添加标志位 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); getApplicationContext().startActivity(intent);添加这个标志位后,系统会为待启动的 Activity 创建一个新的任务栈,并将其放入其中。从行为上看,此时 Activity 的启动方式类似于
singleTask模式。 -
singleTop:栈顶复用模式。新Activity位于任务栈栈顶,那么此Activity不会被重建,同时
onNewIntent会被回调,通过此方法的参数可以取出当前请求的信息。注意,这个Activity的onCreate、onStart不会被系统调用,因为它没有发生变化。singleTop基本规则仍是进入启动它的 Activity 所在的任务栈。因此,直接用ApplicationContext启动同样会因缺乏任务栈而报错,也需要FLAG_ACTIVITY_NEW_TASK标志 -
singleTask:栈内复用模式,是一种单实例模式,理解该模式前先说一下任务栈Task Affinity,它可以理解为 Activity 的 “专属任务栈名称”(默认是应用包名),用来指定该 Activity 应该属于哪个任务栈。对于是否专门设置了任务栈,栈内复用分两种情况。
不设置 Task Affinity(用默认任务栈)
假设你的应用有 A、B 两个 Activity,B 的启动模式是 singleTask,默认任务栈名称是你的应用包名(比如 com.example.myapp):
第一次启动 B,系统会新建 com.example.myapp这个默认任务栈,把 B 作为 “栈底根 Activity” 放进去,B 实例创建完成。
之后从其他地方(比如 A 再次启动 B,或从其他应用启动 B)启动B时,系统检查有没有 com.example.myapp 这个任务栈(有,里面已经有 B 实例)于是系统不会再新建 B 实例,而是直接让默认任务栈中B活动上面的活动出栈并将B置于栈顶,同时调用 B 的 onNewIntent() 方法(用来接收新的启动意图)。
设置了 Task Affinity(指定专属任务栈)
现在给 B 的 Task Affinity 设为 com.example.newtask(不是默认包名),其他条件不变:
第一次启动 B时系统检查有没有 com.example.newtask 这个任务栈?(没有,因为这是自定义名称),于是系统会新建一个名为 com.example.newtask 的独立任务栈,把 B 作为根 Activity 放进去(此时你的应用可能有两个任务栈:默认的 com.example.myapp 和自定义的 com.example.newtask)之后再启动 B(不管从哪里启动):系统先检查有没有 com.example.newtask 这个任务栈?(有,里面有 B 实例),于是直接把 com.example.newtask 任务栈调到前台,复用 B 实例并调用它的 onNewIntent(),完全不新建实例。
<activityandroid:name=".TaskActivity"android:launchMode="singleTask"android:taskAffinity="com.example.newtask" />
这里再强调一下,如果D所需任务栈为S1,并且当前任务栈S1情况是ADBC,C为栈顶,根据栈内复用原则,D不会重新创建,系统会把D切到栈顶并调用onNewIntent方法。同时由于singleTask默认具有clearTop的效果,会导致栈内所有在D上面的Activity全部出栈最后S1中剩余AD
- singleInstance:单实例模式。除了具有singleTask模式所有特性外,还加强了一点,就是具有此种模式的Activity只能单独地位于一个任务栈中。
关于启动模式,再来一个比较绕的
现在有三个活动,MainActivity不进行设置,我们将SecondActivity和ThirdActivity都设成singleTask并指定它们的taskAffinity属性为“com.ryg.task1”,注意这个taskAffinity属性的值为字符串,且中间必须含有包名分隔符“.”。然后做如下操作,在 MainActivity中单击按钮启动SecondActivity,在SecondActivity中单击按钮启动ThirdActivity,在ThirdActivity中单击按钮又启动MainActivity,最后再在MainActivity中单击按钮启动SecondActivity,现在按两下back键,然后看到的是哪个Activity?
答案是回到桌面。因为在 ThirdActivity 中启动 MainActivity时,由于MainActivity是standard,系统会将它放入启动它的那个 Activity 所在的任务栈中,所以不会放入com.ryg任务栈中而是放在com.ryg.task1栈中,com.ryg一直只有一开始启动的MainActivity,因此第一次back会销毁SecondActivity然后将com.ryg作为前台任务栈展示MainActivity,再来一次back会直接回退到桌面。
Activity的Flags
Activity的Flags有很多,这里分析一下常用的标志位。标志位的作用很多,有的可以设定Activity的启动模式,还有的可以影响Activity的运行状态。大部分情况下,我们不需要为Activity指定标记位。在使用标记位时要注意有些标记位时系统内部使用的,应用程序不需要手动设置这些标记位以防出现问题
FLAG_ACTIVITY_CLEAR_TOP:具有此标记位的Activity启动时同一个任务栈中位于它之上的都需要出栈。singleTask启动模式默认就具有此标记位的效果。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:具有这个标记的Activity不会出现在历史Activity的列表中,某些情况下我们不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用。它等同于xml中设置android:excludeFromRecents="true"
IntentFilter的匹配规则
启动Activity分为两种,显式调用和隐式调用。原则上一个Intent不应该既是隐式调用又是隐式调用。隐式调用需要匹配目标组件的IntentFilter中设置的过滤信息,如果不匹配将无法启动目标Activity。IntentFilter中的过滤信息有action,category,data,下面是一个过滤规则的示例。
<activityandroid:name="com.ryg.chapter_1.ThirdActivity"android:configChanges="screenLayout"android:label="@string/app_name"android:launchMode="singleTask"android:taskAffinity="com.ryg.task1"><intent-filter><action android:name="com.ryg.charpter_1.c"/><action android:name="com.ryg.charpter_1.d"/><category android:name="com.ryg.category.c"/><category android:name="com.ryg.category.d"/><category android:name="android.intent.category.DEFAULT"/><data android:mimeType="text/plain"/></intent-filter></activity>
为了匹配过滤列表,需要同时匹配过滤列表中的action,category,data信息,否则匹配失败。一个过滤列表中的action、category和data可以有多个,所有action、category和data分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程,只有一个Intent能同时且完全匹配三个类别才能成功启动Activity,另外,一个Activity中可以有多个intent-filter,一个Intent只要匹配任何一组intent-filter即可成功对应Activity。
- action的匹配规则:action是一个字符串,action的匹配规则时Intent中的action必须能和过滤规则中的action匹配,这里的匹配是指action的的字符串值完全一致。一个过滤规则中可以有多个action,只要Intent中的action能和规律规则中任何一个action相同即可匹配成功。
- category的匹配规则:category是一个字符串,它要求Intent中如果含有category,那么所有的category必须和过滤规则中的每一个category相同。也就是说,Intent如果出现了category,不管有几个category,对于每个category来说,它必须是过滤规则中已经定义的category。当然,Intent中可以没有category,如果没有category,这个Intent仍然可以匹配成功。这里区别和action的匹配规则。action要求Intent中必须有一个action且必须能和过滤规则中某个action相同。而category一旦有了,不管有几个,每个都需要和过滤规则中任何一个category相同。至于不设置
category也可以匹配,是因为系统再调用startActivity时会默认加上"category.DEFAULT"这个category - data的匹配规则:data的匹配规则和action类似,如果过滤规则中定义了data,那么Intent中必须定义可匹配的data。一个
<intent-filter>也可以声明多个<data>元素,Intent只需匹配其中一个即可。在介绍data的匹配规则前,我们需要先了解一下data的结构,因为data有些复杂。data的语法如下
<data android:scheme="string"android:host="string"android:port="string"android:path="string"android:pathPattern="string"android:pathPrefix="string"android:mimeType="string" />
data由两部分组成,mimeType和URI。mimeType指媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*等,可以表示图片、文本、视频等不同的媒体格式。而URI比较复杂,下面是URI的结构<Scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
例如:http://www.baidu.com:80/search/info,这里再详细介绍一下每个数据的含义
scheme:URI模式,比如http、file、content等,如果URI没有指定scheme,那么整个URI是无效的Host:URI的主机名,比如www.baidu.com,如果Host未指定,那么整个URI也是无效的Port:URI中的端口号,比如80,仅当URI中指定了scheme和host参数时port才有意义- Path、pathPattern、pathPrefix:这三个参数表述路径信息。path表示完整的路径信息,pathPattern也表示完整的路径信息,但是它里面可以包含通配符
*,表示0个或多个任意字符。不过由于正则表达式的规范,如果想表示真实的字符串,那么*必须写成\\*,\要写成\\\\, pathPrefix表示路径的前缀信息。
隐式启动的判空
当我们隐式启动一个Activity时,可以做一下判断,看是否有Activity匹配我们的隐式Intent,如果不做判断就可能出现无法找到Activity的报错了。
判断方法有两种:采用PackageManager的resolveActivity方法或者Intent的resolveActivity方法,如果它们找不到匹配的Activity就会返回null,我们通过判断返回值就可以规避上述错误。另外,PackageManager还提供了queryIntentActivities方法,这个方法和resolveActivity方法不同的是:它不是返回最佳匹配的Activity信息,而是返回所有成功匹配的Activity信息。我们看一下这两个方法的原型:
public abstract List<ResolveInfo> queryIntentActivities(Intent intent, int flags);
public abstract ResolveInfo resolveActivity(Intent intent, int flags);
上述方法第一个参数就是Intent,第二个参数要注意,使用MATCH_DEFAULT_ONLY这个标记位,含义是仅仅匹配哪些intent-filter中声明了<category android:name="android.intent.category.DEFAULT"/>这个category的Activity。这个标记位的意义在于,因为不含有DEFAULT这个category的Activity是无法接收隐式Intent的,从而尽管action和data匹配成功也可能启动失败。在action和category中,有一类action和category比较重要:
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
这两个的共同作用是表明这是一个入口Activity并会出现在系统的应用列表中,少了任何一个都没有实际意义,二者缺一不可。
