一本android开发程序员必读的一本书,感谢任玉刚大佬的分享
一、Activity的生命周期全面分析
- 典型情况下的生命周期
所谓典型情况下的生命周期,是指在有用户参与的情况下,Activity所经过的生命周期的改变 - 异常情况下的生命周期
异常情况下的生命周期是指Activity被系统回收或者由于当前设备的Configutation发生改变从而导致Activity被销毁重建
1.1.1 典型情况下的生命周期分析
在正常情况下,Activity会经历如下生命周期。
- onCreate: 表示Activity正在被创建,这是生命周期的第一个方法。在这个方法中,我们可以做一些初始化工作,比如调用setContentView去加载界面布局资源、初始化Activity所需数据等
- onRestart: 表示Activity正在重新启动。一般情况下,当当前Activity从不可见重新变为可见状态时,onRestart就会被调用。这种情形一般都是用户行为所导致的,比如用户按Home键切换到桌面或者用户打开了一个新的Activity,这时当前的Activity就会暂停,也就是onPause和onStop被执行了,接着用户又回到了这个Activity,就会出现这种情况。
- onStart: 表示Activity正在被启动。即将开始,这时Activity已经可见了,但是还没有出现在前台,还无法和用户交互。这个时候可以理解为Activity已经显示出来了,但是我们还看不到。
- onResume: 表示Activity已经可见了,并且出现在前台并开始活动。要注意这个和onstart的对比,onStart和onResume都表示Activity已经可见,但是onStart的时候Activity还在后台,onResume的时候Activity才显示到前台。
- onPause:表示Activity正在停止,此时可以做一些存储数据、停止动画等工作,但是注意不能太耗时,因为这回影响到新Activity的显示,onPause必须先执行玩,新Activity的onResume才会执行。
- onStop: 表示Activity即将停止,可以做一些稍微重量级的回收工作,同样不能太耗时。
- onDestroy: 表示Activity即将被销毁,这是Activity生命周期中的最后一个回调,在这里,我们可以做一些回收工作和最终的资源释放。
(1)针对一个特定的Activity,第一次启动,回调onCreate > onStart > onResume。
(2)当用户打开新Activity或者切换桌面的时候,回调如下: onPause> onStop。
(3)当用户再次回到原Activity时,回调如下: onRestart > onStart > onResume 。
(4)当用户按back键回退时,回调如下:onPuse > onStop > onDestroy。
(5)当Activity被系统回收后再次打开,生命周期方法回调过程和(1)一样,注意只是生命周期方法一样,不代表所有过程都一样。
(6)对整个生命周期来说,onCreate和onDestroy是配对的,分别标识着Activity的创建和销毁,并且只可能有一次调用。从Activity是否可见来说,onStart和onStop是配对的,onResume和onPause是配对的。这两个方法可被多次调用。
- 当打开一个新的Activity时,旧的Activity会先调用onPause后,新的Activity才能调用onCreate,Android官方文档对onPause是这样说的:
Called when the system is about to start resuming a previous activity. This is typically used to commit unsaved changes to persistent data, stop animations and other things that may be consuming CPU, etc. Implementations of this method must be very quick because the next activity will not be resumed until this method returns.
Followed by either onResume() if the activity returns back to the front, or onStop() if it becomes invisible to the user.
.....此方法的实现必须非常快,因为在该方法返回之前,将不会恢复下一个活动。....
1.1.2 异常情况下的生命周期分析
- 情况1:资源相关的系统配置发生改变导致Activity被杀死并重新创建(例:手机横竖屏切换)
当系统配置发生改变时,Activity 会被销毁,其onPause、onStop、onDestory均会被调用,同时由于Activity 是异常情况下种植的,系统会调用onSaveInstanceStace来保存当前Activity状态,这个方法调用在onStop之前,它和onPause调用没有既定的先后顺序。当Activity被重新创建,系统会调用onRestoreInstanceState,并且把Activity销毁时onSaveInstanceStace保存的Bundle对象作为参数传递给onRestoreInstanceState和onCreate方法。
- 情况 2:资源内存不足导致低优先级的Activity被杀死
Activity的优先级从高到低可以分为三种:
(1)前台Activity——正在和用户交互的Activity,优先级最高。
(2)可见但非前台Activity——比如Activity中弹出一个对话框,导致Activity可见但是位于后台无法和用户直接交互。
(3)后台Activity已经被暂停的Activity,比如执行了onStop,优先级最低。
当系统内存不足时,系统会按照上述优先级去杀死目标Activity所在的进程。并通过onSaveInstanceStace和onRestoreInstanceState储存和回复数据。如果一个进程中没有四大组件在执行,那么这个进程将很快被系统杀死,因此一些后台工作放入Service中从而保证进程有一定的优先级比较好,这样就不会被轻易的杀死。
1.1.3 configChanges
针对横竖屏发生Activity会发生重建的情况,我们可以给configChanges设置orientation这个值来避免。
android:configChanges="orientation"
configChanges的项目和含义:
项目 | 含义 |
---|---|
mcc | SIM卡唯一标识IMSI(国际移动用户识别码)中的国家代码,由三位数字组成,中国为460.此项标识mcc代码发生了改变。 |
mnc | SIM卡唯一标识IMSI(国际移动用户识别码)中运营上代码,由两位数字组成,中国移动TD系统为00,中国联通为01,中国电信为03.此项标识mnc发生改变。 |
locale | 设备的本地位置发生了改变,一般指切换了语言。 |
touchscreen | 触摸屏发生了改变,这个很费解,正常情况下无法发生,可以忽略他。 |
keyboard | 键盘类型发生了改变,比如用户使用了外插键盘。 |
keyboardHidden | 键盘的可访问性发生了改变,比如用户调出了键盘。 |
navigaion | 系统导航方式发生了改变,比如采用了轨迹球导航,这个有点费解,正常情况下无法发生,可以忽略他。 |
screenLayout | 屏幕布局发生了改变,很可能是用户激活了另外一个显示设备。 |
fontScale | 系统字体缩放比例发生了改变,比如用户选择了一个新字号。 |
uiMode | 用户界面模式发生了改变,比如是否开启了夜间模式(API 8 新添加) |
orientation | 当屏幕的尺寸信息发生了改变,当旋转设备屏幕时,屏幕尺寸会发生变化,这个选项比较特殊,它和编译选项有关,当编译选项中的minSdkVersion和targetSdkVersion均低于13时,此选项不会导致Activity重启,否则会导致Activity重启(API 13 新添加) |
smallestScreenSize | 设备的屋里屏幕尺寸发生改变,这个项目和屏幕方向没有关系,仅仅表示在实际的物理屏幕的尺寸改变的时候发生,比如用户切换到了外部的显示设备,这个选项和screenSize一样,当编译选项中的minSdkVersion和targetSdkVersion均低于13时,此选项不会导致Activity重启,否则会导致Activity重启(API 13 新添加) |
layoutDirection | 当布局方向发生变化,这个属性用的比较少,正常情况下无须修改布局的layoutDirection属性(API 17 新添加) |
在代码中:
...
...
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
log("onConfigurationChanged: "+newConfig.orientation);
}
旋转几次得出结果为:
E/main: onCreate
E/main: onStart
E/main: onResume
E/main: onConfigurationChanged:2
E/main: onConfigurationChanged:1
E/main: onConfigurationChanged:2
由上面的日志可见,配置过后Activity旋转屏幕将不会重新创建而是调用onConfigurationChanged方法。
1.2 Activity的启动模式
1.2.1 Activity 的 LaunchMode
- standard:标准模式,系统默认模式,每次启动一个 Activity 都会重新创建一个新的实例,不管是否已经存在,调用正常的Activity生命周期。
- singleTop:栈顶复用模式。顾名思义只有在栈顶的 Activity 会被复用,在这种情况下,如果新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的 onNewIntent 方法会被调用,
1.当任务栈中存在 ABCD(Activity) ,此时启动C,依旧会开启一个新的CActivity,然后任务栈中 ABCDC 。
2.此时我们在启动C,C就不会被创建,这时候会调用一次 onPause > onNewIntent > onResume。任务栈中任然会是 ABCDC。
- singleTask:栈内复用模式。这是一种单实例模式,在这种模式下,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,重复创建同一个 Activity 会调用 onNewIntent 。
1.当任务栈中存在 ABCD (Activity),此时打开 D,会调用一次 onPause > onNewIntent > onResume。
任务栈中会是 ABCD,复用 D,不会创建新的 Activity 。
2.当任务栈中存在 ABCD (Activity),此时打开 A,A依旧是复用流程,但是B、C、D都会调用 onPause > onStop > onDestory 被销毁掉。因为singleTask默认具有clearTop的效果,会导致 A 上面所有的 Activity 全部出栈。
- singleInstance:单实例模式,这是一种加强的 singleTask 模式,它除了具有 singleTask模式的所有特性外,那就是具有此种模式的 Activity 智能单独的位于一个任务栈中,
启动7下 Activity ,退出的时候只要 4 下,说明所有 Activity 都被复用了,但是这是栈复用,总共四个栈,按先进后出顺序。
GIF画面切换的可能有点快,为了缩短时间。
如图启动模式,共11个步骤,singleInstance 模式下的 Activity 会单独创建一个任务栈并且如果重复调用已经创建过的 singleInstance 模式的 Activity ,系统将会复用这个栈调用 onNewIntent,而不会重新创建,而其他模式则不会。
1.2.2 Activity 的 Flags
- FLAG_ACTIVITY_NEW_VIEW
这个标记位的作用是为 Activity 指定 “singleTask” 启动模式,其效果和在 XML 中指定该启动模式相同。
- FLAG_ACTIVITY_SINGLE_TOP
这个标记位的作用是为 Activity 指定 “singleTop” 启动模式,其效果和在 XML 中指定该启动模式相同。
- FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的Activity,当他启动时,在同一个任务栈中所有位于他上面的Activity都要出栈,这个模式一般需要和FLAG_ ACTIVITY_ NEW _ TASK配合使用,在这种情况下,被启动的Activity的实例如果已经存在,那么系统就会调用它的onNewIntent,如果被启动的Activity采用标准模式,那么他连同他之上的Activity都要出栈,系统会创建新的Activity实例并放入栈顶
- FLAG_ ACTIVITY_ EXCLUDE_ FROM _ RECENTS
具有此标记位的Activity,不会出现在历史Activity的列表当中,当某种情况下我们不希望用户通过历史列表回到我们的Activity的时候就使用这个标记位了,他等同于在XML中指定Activity的属性: android:excludeFromRecents="true"
1.3IntentFilter 的匹配规则
Activity 的启动分显示和隐式调用,显示就是明确的指定被启动对象的组件信息,包括包名和类名,隐式调用需要 Intent 能够匹配目标组件设置的 IntentFilter 中的过滤信息,其中有 action 、category 、data。一个过滤列表中 action 、category、data 可以有多个,只有一个 Intent 能同时匹配一组 IntentFilter 中的 action 类别 、category 类别 、data 类别,才能启动对应的 Activity 。另外一点,一个 Activity 可以有多个 intent-filter ,一个 Intent 只要能够匹配任何一组 intent-filter 即可成功启动对应的 Activity 。如下所示:
AndroidManifest:
执行代码:
Intent intent = new Intent();
intent.setAction("android.intent.action.SEND");
intent.addCategory("android.intent.category.DEFAULT");
intent.setType("text/plain");
startActivity(intent);
- action 的匹配规则
action是一个字符串,系统预定了一些action,同时我们也可以在应用中定义自己的action,action的匹配规则是intent中的action必须能够和过滤规则中的action匹配,这里说的匹配是指action的字符串值完全一样,一个过滤规则中的可以有多个action,那么只要intent中的action能够和过滤规则匹配成功,针对上面的过滤规则,需要注意的是,intent如果没有指定action,那么匹配失败,总结一下,action的匹配需求就是intent中的action存在且必和过滤规则一样的action,这里需要注意的是他和category匹配规则的不同,另外,action区分大小写,大小写不同的字符串匹配也会失败
- category
category是一个字符串,系统预定义了一些category,同时我们也可以在应用中定义自己的category。category的匹配规则和action不同,它要求Intent中如果含有category,那么所有的category都必须和过滤规则中的其中一个category相同。换句话说,Intent如果出现了category,不管有几个category,对于每个category来说,它必须是过滤规则中已经定义的category。当然,Intent中可以没有category,如果没有category的话,按照上面的描述,这个Intent仍然可以匹配成功。这里要注意下它和action匹配过程的不同,action
是要求Intent中必须有一个action且必须能够和过滤规则中的某个action相同,而category要求Intent可以没有category,但是如果你一旦有category,不管有几个,每个都要能和过滤规则中的任何一个category相同。为了匹配前面的过滤规则中的category,我们可出下面的Intent,intent.addcategory (“com.ryg.category.c”)或者Intent.addcategory (“com rcategory.d)亦或者不设category。为什么不设置category也可以匹配呢?原因是系统在调用startActivity或者startActivityForResult的时候会默认为Intent加上“android.intent.category.DEFAULT”这个category,所以这个category就可以匹配前面的过滤规则中的第三个category。同时,为了我们的activity能够接收隐式调用,就必须在intent-filter中指定“android intent categor.DEFAULT”这个category,原因刚才已经说明了。
- data匹配规则
data的匹配规则和action有点类似,如果过滤规则中定义了data,那么intent中必须也要定义可匹配的data,在介绍data的匹配规则之前,我们需要来了解一下data的结构,因为data稍微有点复杂
data的语法如下所示:
android:host="string"
android:mimeType="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:port="string"
android:scheme="sstring" />
> data 由两部分组成,mimeType 和 URI。mimeType 指定媒体类型,比如 image/jpeg 、audio/mpeg4-generic 和 video/* 等,可以表示图片、文本、视频等不同的媒体格式,而 URI 中包含的数据就比较多了,下面是 URI 的结构:
` ://"/[||]`
这里再给几个实际的例子就好理解了,如下所示。
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
看了上面的两个示例应该就瞬间明白了,没错,就是这么简单。不过下面还是要介绍一下每个数据的含义。
- **Scheme**:URI 的模式,比如 http、file、content 等,如果 URI 中没有指定 scheme,那么整个 URI 的其他参数无效,这页意味着 URI 是无效的。
- **Host**:URI 的主机名,比如 www.baidu.com,如果 host 未指定,那么整个 URI 中的其他参数无效,这页意味着 URI 是无效的。
- **Port**: URI 中的端口号,比如 80,仅当 URI 中指定了 scheme 和 host 参数的时候 port 参数才是有意义的。
- **Path、pathPattern 和 pathPrefix**:这三个参数表述路径信息,其中 path 表示完整的路径信息;pathPattern 也表示完整的路径信息,但是它里面可以包含通配符 “ \* ” , “ \* ” 表示 0 个或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表达真实的字符串,那么 “ * ” 要写成 “ \\\\\* ”,“ \ ”要写成“ \\\\\\\ ”,pathPrefix表示路径的前缀信息。
> 介绍完data的数据格式后,我们要说一下data的匹配规则了。前面说到,data的匹配规则和action类似,它也要求Intent中必须含有data数据,并且data数据能够完全匹配过滤规则中的某一个datn.这里的完全匹配是指过滤规则中出现的data部分也出现在了 Intent
中的data中。下面分情况说明。
(1)如下过滤规则:
1. ```
...
这种规则指定了媒体类型微所有类型的图片,嘛呢 Intent 中的 mimeType 属性必须为 “ image/* ” 才能匹配,这种情况下虽然过滤规则没有指定 URI,但是 Intent 中的 URI 部分的 scheme 必须为 content 或者 file 才能匹配,这点是需要尤其注意的。为了匹配(1)中的规则,我们可以写出如下示例。
intent.setDataAndType(Uri.parse("file://abc"), "image/png");
另外,如果要为 Intent 指定完整的 data,必须要用调用 setDataAndType 方法,不能调用 setDate 在调用 setType,因为这两个方法会彼此清空对方的值,源码:
3. ```
public Intent setData(Uri data) {
mData = data;
mType = null;
return this;
}
...
public Intent setType(String type) {
mData = null;
mType = type;
return this;
}
(2)如下过滤规则:
这种规则指定了两组data规则,且每个data都指出了完整的属性值,既有URI又有类型,为了匹配类型(2),我们可以写出如下示例:
intent.setDataAndType(Uri.parse("http://abc"),"video/png");
//或者
intent.setDataAndType(Uri.parse("http://abc"),"audio/png");
通过上面的实例,我们应该知道了data的匹配规则,关于data还有一些特殊的情况需要说明一下,这也是他和action不同的地方,如下两种特殊的写法,他们的作用是一样的:
...
...
到这里我们已经把IntentFilter的过滤规则都讲了一遍了,还记得本书前面给出的一个实例吗?现在我们给出完整匹配它的intent:
Intent intent = new Intent();
intent.setAction("android.intent.action.SEND");
intent.addCategory("android.intent.category.DEFAULT");
intent.setDataAndType(Uri.parse("file//abc"),"text/plain");
startActivity(intent);
还记得URI中的scheme中的默认值吗?如果把上面的intent.setDataAndType(Uri.parse(“file//abc”),”text/plain”);这句改成intent.setDataAndType(Uri.parse(“http//abc”),”text/plain”);打开的actiivty就会报错,提示无法找到Activity,另外一点,intent-filter的匹配规则对于服务和广播也是同样的道理,不过系统对于 Service 的建议是尽量使用显式意图来启动服务。
最后,当我们通过隐式方式启动一个Activity的时候,可以做一下判断,看是否Activity能够匹配我们的隐式 Intent,如果不做判断就有可能出现上述的错误了。判断方法有两种:采用 PackageManager 的 resolveActivity 方法或者 Intent 的 resolveActivity 方法, 如果它们找不到匹配的Activity就会返回null,我们通过判断返回值就可以规避上述错误了,另外,PackageManager 还提供了 queryIntentActivities 方法,这个方法和resolveActivity方法法不同的是:它不是返回最佳匹配的Activity信息而是返回所有成功匹配的Activity信息,我们看一下 queryIntentActivities 和 resolveActivity 的用法:
public abstract ListqueryIntentActivities(Intent intent,int fladgs);
public abstract ResolveInfo resolveActivity(Intent intent,int flags);
PackageManager packageManager = getPackageManager();
List resolveInfos = packageManager.queryIntentActivities(intent, 0);
int size = resolveInfos.size();
if (size > 0) {
startActivity(intent);
}
//或者
if (intent.resolveActivity(getPackageManager()) != null) {
startActivity(intent);
}
上述两个方法的第一个参数比较好理解,第二个参数需要注意,我们要使用MATCH_ DEFAULT _ ONLY这个标记位,这个标记位的含义是仅仅匹配那些在intentfilter中声明了 < category android-name=”android.intent.category DEFAULT”>这个category的 Activity。使用这个标记位的意义在于,只要上述两个方法不返回null,那么startActivity一定可以成功,如果不用这个标记位,就可以把intent-filter 中 category不含DEFAULT的那些Activity给匹配出来,从而导致startActivity可能失败。因为不含有DEFAULT这个category的Activity是无法接收隐式Intent的。在action和 category中,有一类action和category比较重要,他们是:
这二者共同作用是用来标明这是一个入口Activity并且会出现在系统的应用列表中,少了任何一个都没有实际意义,也无法出现在系统的应用列表中,也就是二者缺一不可,另外,针对 Service和BroadcastReceiver,PackageManager同样提供了类似的方法去获取成功匹配的组件信息。