Activity的生命周期
生命周期和启动模式以及IntentFilter的匹配规则分析。
Activity的生命周期分为两个部分:
- 典型情况下的生命周期
- 异常情况下的生命周期
1.典型情况下的生命周期分析
- onCreate :表示
Activity
正在被创建。在这里可以做一些初始化的工作。 - onRestart :表示
Activity
正在重新启动。当当前Activity
从不可见重新变成可见状态。 - onStart :表示
Activity
正在被启动。已经可见,但不在前台,无法交互。 - onResume :表示
Activity
已经可见,并且出现在前台可以交互。 - onPause :表示
Activity
正在停止。在这里可以做一些储存数据,停止动画等工作,但不能太耗时,因为必须onPause
执行完成之后新的Activity
才能Resume
。 - onStop :表示
Activity
即将停止。可以进行一些稍微重量级的回收工作,不能太耗时。 - onDestroy :表示
Activity
即将被销毁。可以进行一些回收工作和最终的资源释放。
注意:
- onStart和onStop是从Activity是否可见这个角度来回调的
- onResum和onPause是从Activity是否在前台这个角度来回调的
2.异常情况下的生命周期分析
情况 1:资源相关的系统配置发生改变导致Activity被杀死并重新创建
比如说横屏手机和竖屏手机会拿到两张不同的图片(设定了landscape或者portrait状态下的图片)。本来手机在竖屏状态,突然旋转屏幕,由于系统配置发生了变化,在默认情况下,Activity会被销毁并且重新创建,当然我们也可以阻止系统重新创建我们的Activity。
当系统配置发生改变后,Activity会调用 onPause -> onStop -> onDestroy。
由于是异常情况终止,系统会在onStop
之前调用onSaveInstanceState
来保存当前Activity
的状态。(与onPause
没有时序关系)
当Activity
被系统重新创建后,系统会调用onRestoreInstanceState
,把之前onSaveInstanceState
方法所保存的Bundle
对象作为参数同时传给onRestoreInstanceState
和onCreate
方法。(从时序来说,onRestoreInstanceState
的调用时机在onStart
之后)
而在视图方面,当Activity在异常情况下需要重新创建时,系统会默认为我们保存当前Activity的视图结构,并且在Activity重启后为我们恢复这些数据。
其实每个View都有onSaveInstanceState
和onRestoreInstanceState
,关于保存和恢复View层级结构,系统的工作流程如下:
onSaveInstanceState方法,系统只会在Activity即将被销毁并且有机会重新显示的情况下才会去调用它。
情况 2:资源内存不足导致低优先级的Activity被杀死
其实这种情况的数据存储与恢复过程与情况 1完全一致。
Activity的优先级情况:
- 前台的
Activity
—— 正在和用户交互的Activity
,优先级最高 - 可见但非前台的
Activity
—— 比如Activity
中弹出了一个对话框,导致Activity
可见但是位于后台,无法和用户进行直接交互 - 后台的
Activity
—— 已经被暂停的Activity
,比如执行了onStop
,优先级最低
当系统内存不足时,系统就会按照上述优先级去杀死目标Activity
所在的进程,并在后续通过onSaveInstanceState
和onRestoreInstanceState
来存储和恢复数据。而将后台工作放入Service
中是一个比较好的方法。
当系统配置改变后,Activity如何不被重新创建
由于系统配置中有很多内容,如果当某项内容发生改变后,不想系统重新创建Activity
,可以给Activity
指定configChanges
属性:
android:configChanges="orientation|keyboardHidden"
项目 | 含义 |
---|---|
mcc | SIM卡唯一标识IMSI(国际移动用户识别码)中的国家代码,由3位数组成,中国为460.此项 标识mcc代码发生了改变 |
mnc | SIM卡唯一标识IMSI(国际移动用户识别码)中的运营商代码,由两位数字组成,中国移动TD系统为00,中国联通为01,中国电信为03。此项标识mnc发生改变 |
locale | 设备的本地位置发生了改变们一般指切换了系统语言 |
touchscreen | 触摸屏发生了改变,正常情况下无法发生,可以忽略它 |
keyboard | 键盘类型发生了改变,比如用户使用了外插键盘 |
keyboardHidden | 键盘的可访问性发生了改变,比如用户调出了键盘 |
navigation | 系统导航方式发生了改变,比如采用了轨迹球导航,很难发生,可以忽略 |
screenLayout | 屏幕布局发生了改变,很可能是用户激活了另一个显示设备 |
fontScale | 系统字体缩放比如发生了改变,比如用户选择了一个新字号 |
uiMode | 用户界面模式发生了改变,比如是否开启了夜间模式(API8新添加) |
orientation | 屏幕方向发生了改变,这个是最常用的,比如旋转了手机屏幕 |
screenSize | 当屏幕的尺寸信息发生了改变,当旋转设备屏幕时,屏幕尺寸会发生变化,这个选项比较特殊,它和编译选项有关,当编译选项中的minSdkVersion和targetSdkVersion 均低于13时,此选项不会导致Activity重启,否则会导致Activity重启(API13新添加) |
smallestScreenSize | 设备的物理屏幕尺寸发生了改变,这个项目和屏幕的方向没有关系,仅仅表示在实际的物理屏幕的尺寸改变的时候发生,比如用户切换到了外部的显示设备,这个选项和screenSize一样,当编译选项中的minSdkVersion和targetSdkVersion均低于13时,此选项不会导致Activity重启,否则会导致Activity重启(API13新添加) |
layoutDirection | 当布局方向发生变化,这个属性用的比较少,正常情况下无须修改布局的layoutDirection属性(API17新添加) |
如果我们没有在Activity
的configChanges
属性中指定该选项的话,当配置发生改变后就会导致Activity
重新创建。
最常用的只有locale
、orientation
和keyboardHidden
。
需要修改的代码很简单,只需要在AndroidMenifest.xml中加入Activity的声明即可:
<activity
android:name="com.dimon.MainActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
@Override
public void onConfigurationChanged(Configuration newConfig){
super.onConfigurationChanged(newConfig);
Log.d(TAG,"onConfigurationChanged,newOrientation:" + newConfig.orientation);
}
Activity
没有重新创建,并且没有调用onSaveInstanceState
和onRestoreInstanceState
来存储和恢复数据,而是系统调用了Activity
的onConfigurationChanged
方法,这个时候我们可以加入一些自己的特殊处理了。
Activity的启动模式
Activity 的 LaunchMode
复习一点:启动
Activity
时,系统会创造实例并把他们放入任务栈里,而任务栈是一种“后进先出”的栈结构。
Activity
的四种启动模式:
-
standard:标准模式、默认模式。每次启动一个
Activity
都会重新创建一个新的实例,不管这个实例是否已经存在。在这种模式下,某个Activity
启动了一号Activity
,那么一号Activity
就运行在启动它的那个Activity
所在的栈中。 -
singleTop:栈顶复用模式。如果新的
Activity
已经位于任务栈的栈顶,那么此Activity
就不会被重新创建,同时它的onNewIntent
方法会被回调,并且可以根据此方法的参数获得当前请求的信息。 -
singleTask:栈内复用模式。在这种单实例模式下,只要
Activity
在一个栈中存在,那么多次启动此Activity
都不会重新创建实例,系统也会调用其onNewIntent
。
- singleInstance:单实例模式。这是一种加强的singleTask模式,除了具有singleTask模式的所有特性外,还加强了一点,那就是具体此种模式的
Activity
只能单独地位于一个任务栈中。
注:在任何跳转的时候,首先调用本
Activity
的onPause
,然后跳转。如果被跳转的activity
由于启动方式而没创建新的实例,则会先调用onNewIntent
,然后按照正常的生命周期调用。
如:
- A→B,A:onPause;B:onCreate,onStart,onResume。
- A(singleTop)→A,A:onPause;A:onSaveInstanceState;A:onResume。
一些具体问题与情况
- 1:首先要说明:任务栈分为前台任务栈和后台任务栈,后台任务栈中的
Activity
位于暂停状态。
当在前台任务栈AB启动Activity D,而D正好是后台任务栈CD的栈顶。现在假设后台任务栈里的Activity的启动模式是singleTask
,请求启动D,那么整个后台任务栈都会被切换到前台,直接占据前台任务栈的栈顶,即ABCD。如果之前请求启动的是C,那么就会变成ABC。
singleTask
模式的Activity
切换到栈顶会导致在它之上的栈内的Activity
出栈。
- 2:
TaskAffinity
:任务相关性。标识一个Activity
所需要的任务栈的名字。adnroid:taskAffinity="com.dimon.task1"
默认情况下Activity
所需要的任务栈的名字为应用的包名。
如何给Activity指定启动模式呢?
第一种方法:通过AndroidMenifest
为Activity
指定启动模式。
<activity
android:name="com.dimon.SecondActivity"
android:configChanges="screenLayout"
adnroid:taskAffinity="com.dimon.task1"
android:launchMode="singleTask"
android:label="@string/app_name"/>
第二种方法:通过在Intent
中设置标志位来为Activity
指定启动模式。
Intent intent = new Intent();
intent.setClass(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
区别
- 优先级:第二种方法比第一种优先级高,两种都存在时,以第二种为准。
- 限定范围:第一种无法设置
FLAG_ACTIVITY_NEW_TASK
标识,而第二种无法指定singleTask
模式。
Acticity 中的一些比较常用的 Flags
FLAG_ACTIVITY_NEW_TASK
这个标记位的作用是为Activity
指定“singleTask
”启动模式,其效果和在XML中指定该启动模式相同。
FLAG_ACTIVITY_SINGLE_TOP
这个标记位的作用是为Activity
指定“singleTop
”启动模式,其效果和在XML中指定该启动模式相同。
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的Activity
,当它启动时,在同一个任务栈中所有位于他上面的Activity
都要出栈。singleTask
默认就具有这个标记位的效果。如果被启动的Activity
采用了standard
启动模式,那么它连通它之上的Activity
都要出栈,系统会创建新的Activity
实例并放入栈顶。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有这个标记的Activity
不会出现在历史Activity
的列表中,当某种情况下我们不希望用户通过历史列表回到我们的Activity
的时候这个标记比较有用。它等同于在XML中指定Activity的属性android:excludeFromRecents="true"
。
IntentFilter的匹配规则
action的匹配规则
action
的匹配要求Intent
中的action
存在且必须和过滤规则中的一个action
相同。区分大小写。
categoryd的匹配规则
如果Intent
中含有category
,那么所以的category
都必须和过滤规则中的其中一个category
相同。
如果Intent
中不含有category
,那么也能匹配,因为系统在调用startActivity
或者startActivityForResult
的时候会默认为Intent
加上“android.intent.category.DEFAULT
”这个category
,所以为了我们的Activity
能够接收隐式调用,就必须在intent-filter
中指定“android.intent.category.DEFAULT
”。
data的匹配规则
规则与action
相似。
<intent-filter>
<data android:mimeType="image/*"/>
...
</intent-filter>
以上规则说明Intent
中的mimeType
属性必须为“image/*
”才能匹配。虽然没有定义URI
,但是还是得有默认值,URI
的默认值为content
和file
。也就是说没有URI
也得在Intent
中的URI
部分的schema
必须为content
或者file
才能匹配。
intent.setDataAndType(uri.parse("file://abc"),"image/png");
如果要为Intent指定完整的data,必须调用setDataAndType方法,不能先调用setData再调用setType。因为这两个方法会彼此清除对方的值。源码如下:
public Intent setData(Uri data){
mData = data;
mType = null;
return this;
}
而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
的结构如下:
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
- Scheme:URI的模式,比如http、file、content等。
- Host:URI的主机名,比如www.baidu.com
- Port:URI的端口号。
- Path、pathPattern和pathPrefix:这三个表示路径信息,path完整路径信息;pathPattern也表示完整的路径信息,但是它里面可以包括通配符“*”,注意正则表达式;pathPrefix表示路径的前缀信息。
IntentFilter整合例子
<intent-filter>
<action android:name="com.dimon.1"/>
<action android:name="com.dimon.2"/>
<category android:name="com.dimon.1a"/>
<category android:name="com.dimon.2b"/>
<category android:name="android.intent.category.DEFAULF"/>\
<data android:mimeType="text/plain"/>
</intent-filter>
Intent intent = new Intent("com.dimon.1");
intent.addCategory("com.dimon.2b");
intent.setDataAndType(Uri.parse("file://abc"),"text/plain");
startActivity(intent)
Some Tips
使用隐式方式启动Activity
时,最好做一下判断,看看是否有Activity
能够匹配我们的隐式Intent
。
方法有两个:采用PackageManager
的resolveActivity
方法或者Intent
的resolveActivity
方法,如果他们匹配不了就返回null
。
另外PackageManager
还提供了queryIntentActivities
方法:它不是返回最佳匹配的Activity
信息而是返回所有成功匹配的Activity
信息。
public abstract List<ResolveInfo> queryIntentActivities(Intent intent, int flags);
public abstract ResolveInfo resolveActivity(Intent intent, int flags);
第二个参数我们使用MATCH_DEFAULT_ONLY
这个标记位,这个标记位仅仅匹配那些在intent-filter
中声明了<category android:name="android.intent.category.DEFAULT"/>
这个category
的Activity
。
只要上述两个方法不返回null
,那么startActivity
一定可以成功。因为不含有DEFAULT
这个category
的Activity
是无法接收隐式Intent
的。