Activity的生命周期和启动模式
参考资料《Android开发艺术探索》
Activity生命周期方法
1.onCreate():表示activity正在被创建,这是生命周期的第一个方法。一般在这个方法里面做一些初始化操作,例如调用setContentView(),初始化数据;
2.onStart():表示activity正在被启动,即将开始,这个时候activity已经可见了,但是还没有出现在前台,还无法和用户进行交互。可以理解为activity已经显示出来了,但我们还看不到;
3.onResume():表示activity已经可见了,并且出现在前台并开始活动;
onStart()和onResume()对比:两者都表示activity可见,但是onStart()的时候activity还在后台,onResume()的时候activity才显示到前台;
4.onRestart():表示activity正在重新启动。一般情况下,当当前activity从不可见重新变为可见状态时,onRestart()就会被调用。这个情况一般是用户行为导致的,例如home键退出到桌面然后,然后再次回到这个activity,就会执行这个方法;
5.onPause():表示activity正在停止,正常情况下,紧接着onStop()就会被调用。在这里可以做一些不太耗时的操作,这样不会影响到新的activity的创建,onPause()必须执行完,新activity的onResume()才会执行;
6.onStop():表示activity即将停止,可以做一些稍微重量级的回收工作,同样不能太耗时;
7.onDestory():表示activity即将被销毁,这是activity生命周期的最后一个回调,在这里,可以做一些回收工作和最终的资源释放;
创建两个Activity,并在每个生命周期函数中打印log
打开第一个activity,调用方法情况
image
关闭屏幕显示,调用方法情况
image
再次打开屏幕,调用方法情况
image
第一个activity跳转到第二个activity
image
第二个activity退回到第一个activity
image
结束第一个activity
image
总结:第一个activity启动第二个activity时,先执行第一个activity的onPause(),之后才会调用第二个activity的生命周期方法,所以在onPause()方法里不要做长时间的耗时操作,这样会影响到新的activity的创建。
异常情况下的生命周期分析
情况1:资源相关的系统配置发生改变导致activity杀死并重新创建
activity如果不做特殊的处理,那么在手机旋转屏幕的时候,由于系统配置发生了改变,默认情况下,activity就会被销毁并且重新创建。重新销毁和创建会调用对应的生命周期函数,同时由于activity是在异常情况下终止的,系统会调用onSaveInstanceState(Bundle outState)来保存当前的状态(注意,还有一个重载方法onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState),调用的并不是这个方法),会在onStop()之前调用,但是不一定在onPause()之前调用,也可能在onPause()之后调用,而且只在activity异常终止的时候调用。同时,在重新创建的时候,系统会调用onRestoreInstanceState(Bundle savedInstanceState),并把之前activity销毁的时候onSaveInstanceState(Bundle outState)所保存的Bundle对象作为参数传递给这个方法,这样我们就可以在此去除数据并且恢复,从时序上来看,onRestoreInstanceState调用时机在onStart()之后。
image
这时候发现,onCreate(Bundle savedInstanceState),onSaveInstanceState(Bundle outState),onRestoreInstanceState(Bundle savedInstanceState)的参数都是一个Bundle对象,那他们有什么对应关系呢?
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Book book = new Book(100, "android");
outState.putParcelable("book", book);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
Book book = savedInstanceState.getParcelable("book");
Log.e(TAG, book.toString());
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_a);
if (savedInstanceState != null) {
Book book = savedInstanceState.getParcelable("book");
Log.e(TAG, book.toString());
}
}
先通过onSaveInstanceState去保存一个Book类,然后在onCreate和onRestoreInstanceState分别打印出对应的值
image.png
可以看到,两个位置打印出来是相同的,说明onSaveInstanceState保存的值被这两个方法接收,所以理论上讲,在onCreate里也可以做恢复数据的操作,但是要做为空判断,否则会报错。
onSaveInstanceState方法会在什么时候被执行,有这么几种情况:
1、当用户按下HOME键时。
这是显而易见的,系统不知道你按下HOME后要运行多少其他的程序,自然也不知道activity A是否会被销毁,故系统会调用onSaveInstanceState,让用户有机会保存某些非永久性的数据。以下几种情况的分析都遵循该原则
2、长按HOME键,选择运行其他的程序时。
3、按下电源按键(关闭屏幕显示)时。
4、从activity A中启动一个新的activity时。
5、屏幕方向切换时,例如从竖屏切换到横屏时。
但上面的五种情况,之后第五种才会调用 onRestoreInstanceState方法,说明他俩并没有绑定出现可以理解为,前面四个只是暂停了目前的activity,并没有被销毁。而横竖屏切换时,不做处理默认操作的话,那么才会调用onRestoreInstanceState方法(例如设置 android:configChanges="orientation|keyboardHidden|screenSize",则不会调用onRestoreInstanceState)
onSaveInstanceState和onRestoreInstanceState方法中,系统已经默认为我们做了一定的恢复工作。例如异常状态下重新创建activity,在activity重启后为我们恢复这些数据,像文本框录入,listview的滚动位置。哪个view能保存哪个数据,可以看view的源码,里面有具体的实现。
关于保存和恢复view的层次结构,系统工作流程是这样的:首先,activity被终止的时候,回去调用onSaveInstanceState去保存数据,然后activity会委托window去保存数据,接着window在委托他上面的顶级容器去保存数据(向下传递?)。顶层容器是一个viewgroup,然后顶层容器在一一去通知他的子view来保存数据,这样整个数据保存的过程就完成了。上层托付下层,父容器委托子元素。
情况2:资源内存不足导致低优先级的activity被杀死
很难模拟但是数据的存储跟恢复的过程跟情况1完全一致,activity优先级从高到低,可分如下三种:
1.前台activity——正在和用户交互的activity,优先级最高
2.可见但非前台的activity——比如activity中弹出了一个对话框,导致activity可见但是位于后台无法和用户直接交互。
3.后台activity——已经被暂停的activity,比如执行了onStop(),优先级最低。
当系统内存不足的时候,就会按照上面的优先级去杀死目标activity所在的进程,并在后续通过onSaveInstanceState和onRestoreInstanceState来存储和恢复数据。
上面写完,问题来了,上面提到的,怎么才能在屏幕切换的时候不创建新的activity呢?这样好像省心很多,那就提到了activity的一个属性 “android:configChanges”,在清单文件中配置。当然android为我们提供了很多的配置项目,可以接受多个项目的配置,用“|”连接就可以了。常有的例如locale(设备的本地位置发生了变化,一般指切换了系统语言),orientation(屏幕方向发生了变化,这个是最常用的,必须旋转手机屏幕),keyboardHidden(键盘的可访问性发生了变化,比如用户调出了键盘),screenSize(当屏幕尺寸信息发生了改变,当旋转设备屏幕的时候,屏幕尺寸会发生变化)
配置 android:configChanges="orientation|screenSize"的时候,横竖屏切换的时候没有回调任何生命周期,但是回调了onConfigurationChanged(Configuration newConfig)方法
Activity的启动模式
1.standard
标准模式,这也是系统的默认模式。每次启动一个activity都会重新创建一个新的实例,不管这个实例是否已经存在。被创建的实例的生命周期符合典型情况下的生命周期。一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动的这个activity,那么这个activity就运行在启动它的activity所在的任务栈中。注意,当ApplicationContext去启动standard模式的activity会报错,因为非activity类型的context并没有指定的任务栈。这样的解决方法是为待启动的activity指定FLAG_ACTIVITY_NEW_TASK标记位,这样启动的时候就会为它创建一个新的任务栈,这时候启动的activity实际上是singleTask模式。
报错如图所示
2.singleTop
栈顶复用模式。在这种模式下,如果新的activity已经位于任务栈的栈顶,那么此activity不会重新被创建,同时他的onNewIntent方法会被调用,通过此方法我们可以取出当前请求的信息。注意,这个activity的onCreate()和onStart()不会被系统调用,因为他并没有发生改变。如果新的activity实例已存在但并不是位于栈顶,那么新activity仍然会被重新创建。
3.singleTask
栈内复用模式。这是一种单实例模式,在这种模式下,只要activity在一个栈中存在,那么多次启动这个activity都不会重新创建实例,跟singleTop一样,系统也会回调其onNewIntent,不会回调生命周期方法的onCreate(),但是会回调onStart()与onResume()。
4.singleInstance
单实例模式,这是一种加强的singleTask模式,此模式的activity只能单独的运行在一个任务栈中,由于栈内复用的存在,后续的请求均不会创建新的activity,除非这个独特的任务栈被系统销毁了.在此类模式中启动新的activity,不会在这个栈中创建新的实例,而会把新的实例创建在调用这个activity的activity所在的栈中。
Activity的Flags
Activity的Flags有很多,标记为的作用很多,有的标记位可以设定activity的启动模式,有的可以影响activity的运行状态
FLAG_ACTIVITY_NEW_TASK
这个标记为的作用都是为activity指定singleTask启动模式,其效果和在xml文件中指定的该启动模式相同
FLAG_ACTIVITY_SINGLE_TOP
这个标记为的作用是为activity指定singleTop启动模式,其效果和在xml文件中指定的该启动模式相同
FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的activity,当它启动时,在同一个任务栈中的所有位于它上面的activity都要出栈。这个标记为一般会和singleTask模式一起出现,在这种情况下,被启动的activity的实例如果已经出现,那么系统会调用它的onNewIntent。如果被启动的activity采用standard启动模式,那么它连同它之上的activity都要出栈,系统会创建新的activity实例并放入栈顶。
举例,两个activity,AActivity中跳转BActivity,之后BActivity再跳转AActivity并指定intent.addFlags(FLAG_ACTIVITY_CLEAR_TOP),这个时候打印出生命周期方法,可以发现,BActivity跳转AActivity时,这两个activity都被干掉了,之后创建一个新的activity放入栈中,这个时候点击返回键,应用就会退回到桌面。
image.png
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有这个标记的activity不会出现在历史activity的列表中,当某些情况下我们不希望用户通过历史列表回到我们的activity的时候这个标记比较有用。他等同与在xml中指定activity的属性android:excludeFromRecents="true"。
例子:假设现在有这样一个场景:有4个Activity ABCD,其中B是SingleInstance模式,其他都是Standard模式。如果A启动B,B启动C,C启动D,这时候我连续按返回键,会出现什么现象?
D 返回 C 返回 A 返回 B,B与其他三个不在同一个栈中
IntentFilter的匹配规则
启动activity分两种,一种显示调用,一种隐式调用,显示调用要明确的指出被启动对象的组件信息,包括包名类名,而隐式调用则不需要指出明确的组件信息。隐式调用需要intent匹配目标组件的IntentFilter所设置的过滤信息,如果不匹配则无法启动目标activity。IntentFilter的过滤信息有action,category,data。
为了匹配过滤列表,需要同时匹配过滤列表中的action,category,data信息,否则匹配失败。一个过滤列表中的action,category和data可以有多个,所有的action,category,data分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程。只有一个Intent同时匹配action类别,category类别,data类别才算完成匹配,只有完成匹配才能启动目标activity。activity中可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可启动对应的activity
action的匹配规则
action是一个字符串,系统预定义了一些action,同时我们也可以在应用中定义自己的action。action的匹配规则是Intent中的action必须能够和过滤规则中的action匹配,这里的匹配是指action的字符串值完全一样。一个过滤规则中可以有多个action,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功。如果Intent中没有指定action,那么匹配失败。总结,action的匹配要求Intent中的action存在且必须和过滤规则中的其中一个action相同。另外,action区分大小写,大小写不同字符串相同的action会匹配失败。
category的匹配规则
category是一个字符串,系统预定义了一些category,同时我们也可以在应用中定义自己的category。category的匹配规则和action不同,它要求Intent中如果含有category,那么所有的category都必须和过滤规则中的其中任何一个category相同。换句话说,Intent中如果出现了category,不管有几个category,对于每个category来说,它必须是过滤规则中已经定义了的category。Intent中可以没有category,这样仍然可以匹配成功。为什么不设置category也能匹配成功呢?原因在于系统在调用startActivity或startActivityForResult的时候会默认为Intent加上“android.intent.category.DEFAULT”这个category。同时,为了我们的activity能够接收隐式意图,就必须在intent-filter中指定"android.intent.category.DEFAULT"这个category。
data的匹配
data的匹配规则和action类似,如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data。
data由两部分组成,mimeType和URI。mimeType指媒体类型,比如image/jpeg,audio/mprg4-generic和video/*等,可以表示图片,文本,视频等不同的媒体格式,而URI包含的数据就比较多了,下面是URI的结构
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
举个例子
content://com.example.project:100/fold/subfolder/etc
http://www.baidu.com:20/search/info
Scheme:URI的模式。如果URI中没有指定Scheme.那么整个URI无效。
Host:URI的host。比如www.baidu.com。如果host未指定,那么整个URI的其他参数无效,这也意味着URI是无效的。
Port:URI端口,当URI指定了scheme 和 host 参数时port参数才有意义。
path,pathPrefix,pathPattern:用来匹配完整的路径。