第一章 Activity生命周期和启动模式
1. Activity的生命周期全面分析启动
Activity有四种生命状态 ,分别是:running、paused、stopped、killed。
1.1 正常生命周期
-
onCreate(): 表示此时activity正在创建,通常进行一些初始化的方法。
-
onStart(): 表示此时activity已经被创建,但没有位于前台,处于可见但不可交互的状态。
-
onResume(): 表示此时activity处于前台running的状态,既可见又可交互的状态。
-
onPause(): 表示此时activity处于paused的状态,一般紧接着会调用onStop(),此时处于可见但不可交互。在这个方法中进行一些存储、暂停的工作,但不能耗。
-
onStop(): 表示Activity处于stopped状态,此时位于后台,处于不可见也不可交互状态。这个方法中能继续做一些不耗时的回收工作。
-
onDestroy(): 表示Activity即将被销毁。在这个最终的方法中,进行剩余所有资源的释放和回收。
Q&A
1.在Activity A中打开Activity B 调用方法顺序是什么?
这个问题的核心其实在于 Activity A的onPause()和Activity B 的onCreate谁先执行的问题。在Activity启动流程中,可以看到在新activity创建入栈之前,原栈顶的activity会先调用onPause,然后新activity依次调用onCreate onStart onResume。最后Activity B入栈,Activity A失去栈顶的位置,退入后台调用onStop。
因此我们可以看到,onPause方法应该尽快结束,避免影响后面Activity的创建,所以不能执行耗时的操作。
2.如果是透明主题或者dialog呢?
如果是透明或者dialog的话,则不会执行onStop方法,核心在于是否可见的问题。此时虽然Activity A虽然被遮盖,但是由于透明或者是dialog不能完全遮盖Activity A,因此处于可见也不可交互的状态,也就是会调用onPause(),而不会调用onStop。
1.2异常声明周期
除了正常的生命周期以外,还有一些异常情况的生命周期,以下分析两种较为常见的情况。
(1)资源相关的系统配置改变导致Activity被杀死并重新创建。
比如一张图片放在drawable里,为了兼容不同的手机分辨率,可能需要在其他不同的目录比如drawable-mdpi、drawable-hdpi、drawable-land等等。这样当app启动时,会自动根据手机的配置加载不同的图片。可是当发生一些配置的改变时,比如横屏和竖屏显示的图片肯定是不一样的,此时Activity会被销毁并创建,根据当前的配置重新加载合适的图片。当然我们也可以阻止发生配置改变时,销毁activity。
Activity的生命周期图如图所示,当发生意外情况时,首先会调用onSaveInstanceState方法的作用顾名思义,显然是存储此时activity中的数据,防止销毁后数据丢失,便于之后的重建复原,然后原Activity被销毁。这个方法会在onStop之前调用,和onPause没有先后的关系。当Activity被重建后,会调用onRestoreInstanceState,显然这个是用来恢复数据的,从时序上来看,这个方法发生在onStart之后。这两个方法通过bundle进行存取数据。
这两个方法系统会为我们自动做一些存储和恢复工作,保存当前的视图结构,比如说文本框内用户输入的数据、Listview滚动的当前位置等等。具体哪一些View会为我们做那些恢复工作可以查看源码,所有的view都重写了这两个方法,可以查看一下具体实现就知道如何恢复了。这里不进行一一的列举,简单看一下比如ListView的源码,ListView本身没有重写onSaveInstanceState,而是使用它的父类AbsListView的onSaveInstanceState方法,由于代码很长,就只贴一小部分,如下所示。
@Override
public Parcelable onSaveInstanceState() {
/*
* This doesn't really make sense as the place to dismiss the
* popups, but there don't seem to be any other useful hooks
* that happen early enough to keep from getting complaints
* about having leaked the window.
*/
dismissPopup();
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
if (mPendingSync != null) {
// Just keep what we last restored.
ss.selectedId = mPendingSync.selectedId;
ss.firstId = mPendingSync.firstId;
ss.viewTop = mPendingSync.viewTop;
ss.position = mPendingSync.position;
ss.height = mPendingSync.height;
ss.filter = mPendingSync.filter;
ss.inActionMode = mPendingSync.inActionMode;
ss.checkedItemCount = mPendingSync.checkedItemCount;
ss.checkState = mPendingSync.checkState;
ss.checkIdState = mPendingSync.checkIdState;
return ss;
}
…………
}
可以看到,主要包括了选择的item 当前位置 ,选择item的id等信息,具体的这些参数看不太懂,如果有人懂得话可以分享一下谢谢。
(2)资源内存不足时,会先回收低优先级的Activity。
这种情况下,一般按照 后台activity->可见但不可交互的activity(比如被对话框遮盖)->前台Activity的优先级进行回收。
Q&A
1.如何禁止因为资源配置,而重建Activity?
比如不想因为屏幕旋转的时候重新创建,就可以通过配置configChanges添加orientation这个值。
android:configChanges="orientation"
如果想同时指定多个值,可以通过|进行连接。这个下面的属性值有很多,常用的有locale(代表地理位置的改变,一般指切换了系统语言)、orientation(当手机屏幕发生旋转)、keyboardHidden(用户调出键盘等),其余的就不列举了,需要的时候查询一下即可。如果api大于13的话,还需要加上screenSize(屏幕尺寸发生改变),所以最终如下。
android:configChanges="orientation|screenSize">
此时旋转屏幕时,不会进行activity的重建,自然也不会用调用onSaveInstanceState和onRestoreInstanceState,而是会调用onConfiguarationChanged,顾名思义就是当配置发生改变进行回调的方法
2. Activity的启动模式
首先有一个android的概念叫做任务栈,系统会为每一个应用程序都分配一个任务栈,它就是我们常见的后进先出的栈结构。每次创建一个新的activity就入栈,每次按一下back键activity就出栈。
2.1LaunchMode
Activity的启动模式有四种分别是standard、singleTop、singleTask、singleInstance。
(1)standard
这也是默认的启动模式,每次启动activity都会新建一个实例入栈。谁启动了一个新的activity,那个新的activity就会入谁所在的栈。
(2)singleTop
standard存在一个问题,就是启动几次activity,就有几个新的实例,显然一些场景下是不合理的,因此有后面的几种启动模式。singleTop模式采用栈顶复用的情况,如果此时activity位于栈顶,启动自己时不会重新创建实例入栈,同时onNewIntent方法会被回调。注意如果不在栈顶,仍然会进行重建。
(3)singleTask
singleTop会出现一个问题就是,如果Actvity A 启动了Activity B,然后在Activity B中又启动Activity A。此时任务栈中的顺序是 A B A,这显然也是不合理的。用户从A中按一次back回到B,再按一次B又回到了A,显然是很奇怪的用户体验,因此singleTask便是如果activity处于栈内,就会将该activity调到栈顶,并且由于singleTask默认具有clearTop的效果,还附带将它顶部的所有activity全部出栈,然后调用onNewIntent。如果不在栈内的话,就新建一个入栈。如果需要的栈不存在,就新建一个栈然后再入栈。
(4)singleInstance
如果说singleTask是栈内唯一实例的话,singleInstance就是全局唯一,会为该实例单独分配一个任务栈,因此singleInstance也是。同一时间内,系统只会存在这一个实例。
TaskAffinity
此外存在一个属性 TaskAffinity,可以通过设置这个属性,来指定activity需要所在的任务栈。如果此时系统内不存在该任务栈,就会新建一个任务栈,从而实现一个应用程序多个栈的情况。默认情况下,所有activity所需要的任务栈名称为应用的包名,如果需要指定的话,必须不能和包名相同。该属性必须和singleTask或者allowTaskReparenting配对使用。
allowTaskReparenting和TaskAffinity配合使用时,如果allowTaskReparenting设置为true时,能够实现activity的迁移。比如app A 启动了app B中的某个activity C,此时C处于app A的任务栈中,但是如果一旦系统中出现了app B的任务栈(比如此时打开了app B),那么C就会从A的任务栈中,转移到B的任务栈,此时打开app B直接是activity c【突发奇想,是不是类似于有人在朋友圈分享知乎,然后点进去打开知乎就直接是该界面是不是就是这种实现思路?】。用生活中的形象比喻就是,你在路上捡到了一条走丢的狗,可以带回家养着,但是如果狗主人出现了,你还是得把狗还给原主人。
另外任务栈分为前台任务栈和后台任务栈,后台任务栈的activity处于paused状态。
2.2Flags
除了在manifest中设置launchMode以外,还可以在代码中调用addFlags指定启动模式。另外后者的优先级大于前者,两者同时指定时,addFlags会覆盖launchMode.
android:launchMode="singleTask"
Intent intent=new Intent();
intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
Activity的Flags有很多,这里主要介绍比较常用的。标记位的作用很多,除了能指定启动方式以外,还能影响运行状态。
1.FLAG_ACTIVITY_SINGLE_TOP
效果等同于直接指定 singleTop
2.FLAG_ACTIVITY_NEW_TASK
效果等同于singleTask
3.FLAG_ACTIVITY_CLEAR_TOP
具有此标志位的activity启动时,通常会将此标识位和FLAG_ACTIVITY_NEW_TASK配合使用。
4.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有此标志位的activity不会出现在历史activity的列表中,简而言之就是按back不会回退到该activity。
3 IntentFilter匹配规则
actvitiy分为两种启动模式,分别是显式调用和隐式调用。隐式调用需要指定IntentFilter,其中包含了action、category、data。只有三个同时匹配时(不需要三个必须同时存在,如果没有则默认匹配成功),才能算IntentFilter匹配成功。此外一个activity可以有多个IntentFilter,一个intent只需要匹配成功一个就算匹配成功,能够启动对应的activity。
action
action是一个字符串。一个IntentFilter可以有多个action,只需要匹配任意一个action,就算action项匹配成功。action的匹配规则是,intent中的action字符串必须和IntentFilter中的action的字符串值完全相同,并且区分大小写。
category
category也是一个字符串。系统预定义了一些category,也可以自定义category。一个IntentFilter中可以有多个category,但在匹配时,intent中添加的所有category必须匹配所有的category才能算匹配成功。此外如果intent不设置category,则有默认值default
data
data由两部分组成,分别是miniType和URI,miniType指的是媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*。而uri的结构如下所示。
<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
举几个例子
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.comn:80/search/info
scheme:代表url的模式,比如http、file、content等,这是必填项
host:代表uri的主机名,如www.baidu.com,这也是必填项。
port:uri中的端口号,这是选填项。
path、pathPrefix、pathPattern:这三个参数表示路径的信息,其中path表示完整路径,pathPrefix表示带有路径的前缀信息,pathPattern代表有通配符“*”表达式来匹配路径。
匹配符号:
- “*” 用来匹配0次或更多,如:“a*” 可以匹配“a”、“aa”、“aaa”…
- “.” 用来匹配任意字符,如:“.” 可以匹配“a”、“b”,“c”…
- 因此 “.*” 就是用来匹配任意字符0次或更多,如:“.*html” 可以匹配 “abchtml”、“chtml”,“html”,“sdf.html”…
转义:
因为当读取 Xml 的时候,“\” 是被当作转义字符的(当它被用作 pathPattern 转义之前),因此这里需要两次转义,读取 Xml 是一次,在 pathPattern 中使用又是一次。如:“” 这个字符就应该写成 “\”,“\” 这个字符就应该写成 “\\”。
比如:http://example.com/blog/abc.html
path只需要写 path=blog/abc.html,pathPrefix只需要pathPrefix=/blog
比如相匹配以html的文件,可以".*\\.html" 其中.*代表匹配之前任意长度的任意字符,然后\\.代表转义后的.
data的匹配规则
和action一样,只需要匹配manifest中任意一个即可。
比如如下所示的代码,image/*代表匹配所有类型的图片,此时虽没有制定uri,但是有默认值content和file,也就是说intent中的data的schema必须为content或者file才能匹配。
<intent-filter>
……
<data android:mimeType="image/*"/>
</intent-filter>
注意当需要同时设置uri和type的时候,必须使用setDataAndType。如果分开调用setData和setType,都会先清除对方的值,在进行赋值。
intent.setDataAndType(Uri.parse("file://abc"),"image/*");
Q&A
1.uri和url有什么区别?
uri是为了能唯一标识某个资源,url则是通过地址的方式,标识某个资源,因此url是uri的子集,是一种具体的实现方式。