《Android开发艺术探索》这本书在Android开发界内可谓口碑极佳,他的作者任玉刚在百度担任Android资深开发工程师。我的一个初中朋友现在深圳百度做Android研发,上周去深圳找他玩的时候碰巧在他电脑桌上看到了这本书,他谈到这本书内容多、有深度且知识点非常系统值得反复阅读。的确,《Android开发艺术探索》这本书去年看过一遍就感觉非常不错,下面将一些书中的重要知识点归纳并分享出来,这是本系列的第一篇,主要介绍的是 Activity的生命周期和启动模式
评语:本节也从原理解释了为什么每次继承Activity后,需要重写onCreate(Bundle savedInstanceState),方法里面的savedInstanceState虽然用的少,但是很重要!谷歌Android团队的这种设计策略可以从侧面反映其思虑之远、逻辑健壮性以及降低特殊情况的损耗。正文有详细解释
下面是我们熟悉的Activity的生命周期流程图:
下面就生命周期逐个分析:
(1)onCreate:create 表示Activity正在被创建,这是Activity生命周期的第一个方法,也是我们在android开发中接触的最多的生命周期方法。它本身的作用是进行Activity的一些初始化工作,比如使用setContentView加载布局,对一些控件和变量进行初始化等。但也有很多人将很多与初始化无关的代码放在这,其实这是不规范的。此时Activity还在后台,不可见。所以动画不应该在这里初始化......
(2)onStart:start 表示启动,这是Activity生命周期的第二个方法。注意:此时Activity已经可见了,但是还没出现在前台,还无法与Activity交互,其实将Activity的初始化工作放在这也没有什么问题,放在onCreate中是由于官方推荐的以及我们开发的习惯。
(3)onResume:resume 表示继续、重新开始,这名字和它的职责也相同。此时Activity经过前两个阶段的初始化已经蓄势待发。Activity在这个阶段就表示 已经出现在前台并开始活动
(4)onPause:pause 表示暂停,当Activity要跳到另一个Activity或应用正常退出时都会执行这个方法。此时Activity在前台并可见,我们可以进行一些轻量级的存储数据和去初始化的工作,不能在这个方法里面做耗时以及重量级的操作,因为在跳转Activity时只有当一个Activity执行完了onPause方法后另一个Activity才会启动,而且android中指定如果onPause在500ms即0.5秒内没有执行完毕的话就会强制关闭Activity。从生命周期图中发现可以在这快速重启,但这种情况其实很罕见,比如用户切到下一个Activity的途中按back键快速得切回来。注意:onPause()必须先执行完毕,新的Activity的onResume()才会执行
(5)onStop:stop 表示停止,此时Activity已经不可见了,但是Activity对象还在内存中,没有被销毁。这个阶段的主要工作也是做一些资源的回收工作。
(6)onDestroy:destroy 表示毁灭,这个阶段就表示Activity被销毁,不可见。常见的操作是:将没释放的资源释放,进行一些回收工作。例如EventBus的注销、MVP架构下,对P层进行注销等等
(7)onRestart:restart 表示重新开始,Activity在这时可见,当用户按Home键切换到桌面后又切回来或者从后一个Activity切回前一个Activity就会触发这个方法。当前Activity从不可见重新变为可见状态,就会调用这个方法
切换过程:
针对一个特定的Activity,第一次启动过程的结果回调:onCreate - onStart - onResume
打开新Activity或者切换桌面,回调:onPause - onStop (如果目标Activity采用透明主题,当前Activity不会回调onStop)
回到原来的Activity,回调如下:onRestart - onStart - onResume
back键回退,回调如下:onPause - onStop - onDestroy
容易混淆的两个概念:
onStart 和 onResume,onPause 和 onStop 描述感觉一样,但是区别在于onStart跟onStop是从Activity 是否可见 来回调的;onResume和onStop是从Activity 是否位于前台 这个角度来回调的
假设当前Activity打开新的Activity,那么新的 Activity的 onResume()先执行还是旧的Activity先执行onPause ()?答案是:旧的Activity先执行onPause(),执行完毕以后,在执行新Activity的 onResume()进行启动
上面的内容都是针对正常情况下Activity生命周期的流程处理,在一些情况出现的时候,可能会导致Activity的生命周期异常,当然,谷歌Android团队在设计之初就考虑了这个问题。
异常情况下的生命周期:
当资源相关的系统配置发生改变以及系统内存不足等一些特殊的情况就会导致Activity生命周期异常而销毁,这种特殊情况,系统会调用 onSaveInstanceState来保存当前Activity状态,理论上来说,这个方法一般不会被调用,如果出现这种情况,当Activity重新创建的时候,系统会调用onRestoreInstanceState,将之前onSaveInstanceState方法保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate方法。
-
还有一种情况,我们知道Activity优先级有如下区分:
(1)前台可见正在和用户交互的activity优先级最高;
(2)可见但是非前台activity,如activity弹出个窗口。
(3)后台已经暂停的activity优先级最低,当系统内存不足的时候就会优先回收这些Activity所在的进程,后通过onSaveInstanceState和onRestoreInstanceState来存储和恢复数据。较好的方法是将后台程序放入service中保证优先级的提高,这样就不会轻易被杀死。
Activity启动模式:
关于启动模式这个话题的确老生常谈,但是还是需要温故知新,为了更好的理解Activity的启动模式,必须要先了解掌握以下几个基础概念:
- 基础概念一:栈结构,是一种先进后出(也称为:后进先出)的顺序
- 基础概念二:默认启动,多次启动一个Activity,系统会创建对应的实例对象,并将其放入任务栈中,按下Back键,就会一 一回退
- 基础概念三:基于此(其实是节约性能以及内存和适应多种开发场景的考虑),谷歌提供了四种Activity启动模式
standard模式(标准模式)
系统的默认模式,每次启动一个新的Activity就会创建一个实例对象(谁启动了这个Activity,那么这个Activity 就 运行在启动它的那个Activity所在的栈中)
singleTop模式(栈顶复用模式)
如果新的Activity位于任务栈的栈顶,那么此Activity就不会被重复创建,同时,这个Activity的onNewIntent方法会被回调。
- 注意一:此模式下的Activity的onCreate、onStart不会被系统调用,因为没有发生改变
- 注意二:如果新的Activity实例已存在但不是在栈顶,那么新的Activity还是会被重建
singleTask模式(栈内复用模式)
只有Activity在一个栈中存在,那么多次启动此Activity都不会创建实例,当然它会回调onNewIntent方法。这种模式,系统首先会判断新的Activity是否存在任务栈,如果不存在任务栈,则创建一个新的任务栈,接着将Activity的实例对象放入刚才创建的任务栈中;如果存在该Activity的任务栈,首先要判断该栈是否存在这个activity的实例对象,如果实例对象存在,系统就会把这个Activity调到栈顶并回调它的onNewIntent方法;如果该实例对象不存在,就创建这个Activity的实例并压入对应的任务栈中
singleInstance模式(单实例模式)
这种模式算是加强的singleTask模式,这种模式的Activity只能单独的运行在一个任务栈中。
那么,声明指定Activity的方式大家也应该都知道,可以通过清单配置文件里,Activity标签内的launchMode去声明具体的启动模式(一个Activity只能有一种):
<activity
android:name="com.sample.MainActivity"
android:label="@string/app_name"
android:launchMode="standard"
android:launchMode="singleTop"
android:launchMode="singleTask"
android:launchMode="singleInstance"
>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
intent-filter :
intent-filter我们一般称为意图过滤器。试想这样一种场景,如果一个 Intent 请求在一片数据上执行一个动作, Android 如何知道哪个应用程序(和组件)能用来响应这个请求呢?Intent Filter的出现就是为了解决这个问题(本质是根据设置的条件去过滤),它是用来注册 Activity 、 Service 和 Broadcast Receiver,让其 具有能在某种数据上执行一个动作的能力。使用 Intent Filter ,应用程序组件就会告知 Android ,它们能为其它程序的组件的动作请求提供服务,包括同一个程序的组件、本地的或第三方的应用程序。
另外,Activity的跳转启动,一般分为两种:
- 第一种:显示调用
如果是显示调用来实现Activity的跳转启动,需要明确指定被启动对象的组件信息,包括包名和类名。一般是在相同的应用程序内部实现的。以两个Activity之间的跳转为例
Intent intent = new Intent(MainActivity.this, NewActivity.class);
startActivity(intent);
- 第二种:隐式调用
隐式调用则是通过Intent Filter来实现的,它一般用在没有明确指出目标组件名称的前提下。Android系统会根据隐式意图中设置的动作(action)、类别(category)、数据(URI和数据类型)找到最合适的组件来处理这个意图。一般是用于在不同应用程序之间。
注意:原则上一个Intent不应该即是显式调用有是隐式调用,如果二者共存以显式调用为主。
上面提到了action、category、data三个标签,下面就逐个分析:
action的匹配规则
一个intent filter中可以有多个action,那么只要intent中的action能和intent filter中的任何一个action相同集合匹配成功。需要注意的是,intent中没有指定action,那么匹配失败。注意,action严格区分大小写、action严格区分大小写!
category的匹配规则
category的匹配规则和action不同,intent中如果出现了category,不管有几个,都必须是intent filter中已经定义的category,也就是说intent可以没有category,一旦有,每个都必须和intent filter中定义的任意一个category相同。
那为什么我们没在intent filter中加android.intent.category.DEFAULT这个category会报错呢?原因是系统在调用startActivity或者startActivityForResult的时候会默认在intent中加上android.intent.category.DEFAULT这个category。
还是以两个Activity之间的跳转为例:
Intent intent = new Intent();
intent.setAction("com.android.test");
intent.addCategory("com.android.testcat");
startActivity(intent);
AndroidManifest.xml配置文件中:
<activity android:name=".NewActivity">
<intent-filter>
<action android:name="com.android.test"/>
<category android:name="com.android.testcat"/>
//必须加上,否者报错
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
data的匹配规则
data的匹配规则和action类似,如果intent filter中定义了data,那么intent中也必须要定义可匹配的data,否者匹配失败。
首先来了解一下data的结构,data由两部分组成,分别是:mimeType和URI。
- mimeType指媒体类型,比如image/jpeg、text/plaind、video/*等,可以表示图片、文本、视频等不同的媒体格式。
- URI的结构看上去复杂,但耐心分析也就那回事,下面是URI的结构: <schema>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
假设现在有这样一串URI:https://www.baidu.com:80/android/info
根据上面的URI结构,这串URI代表的含义如下:
Schema:这个代表URI的模式,常见的schema有http、file、content等等。注意:如果URI没有指定URI,这就意味整个URI无效!
host:主机名,这里的就是www.baidu.com,如果host没有声明任何属性值,同样意味该URL无效
port:端口号,这里的端口号就是指80,当前仅当URI声明schema和host的时候,port才有意义
path、pathPrefix、pathPattern:代表的就是完整的路径信息
上面说完了URI,接下来开始介绍data的过滤规则,下面分情况说明
情况一:data规则不完整。如下所示
<intent-filter>
<data android:mimeType="image/*"/>
</intent-filter>
这种规则指定了媒体类型为所有类型的图片,那么intent中的mimeType属性必须为“image/*”才能匹配成功,这种情况虽然没有指定URI,但intent中的URL部分的schema默认值为content和file。也就是说虽然没有指定URI,但是intent中的URI部分的schema必须为content或者file才能匹配成功,这点尤其需要注意。
情况二:定义了多组data规则,并且每个data都定义了完整属性,既有URI又有mimeType。如下所示:
<intent-filter>
<data android:mimeType="video/mpeg" android:scheme="http" ... />
<data android:mimeType="audio/mpeg" android:scheme="http" ... />
...
</intent-filter>
为了匹配这种intent filter,我们也需要在intent中完整定义其中一组data规则才能匹配成功。另外,如果要为intent指定完整的data,必须调用setDataAndType方法,不能先调用setData再调用setType,因为这两个方法彼此会清除对方的值。
最后,但我们通过隐式方式启动一个Activity时,可以做一下判断,看是否有Activity能够匹配我们的intent,以免不必要的奔溃。判断方法有两种:一是采用PackageManager的resolveActivity方法,二是采用Intent的resolveActivity方法,如果他们找不到匹配的Activity就会返回null,根据其返回值我们就规避上述问题了。
《Android开发艺术探索》第一章的内容大抵就是这样,因为书中的内容都是干货,所以值得反复阅读和品位,我个人的思考是:提高代码的稳健性和健壮性是我们必须要考虑的问题,以及出现异常情况下,我们该如何处理减少损失。
如果这篇文章对你有帮助,希望各位看官留下宝贵的star,谢谢。
Ps:著作权归作者所有,转载请注明作者, 商业转载请联系作者获得授权,非商业转载请注明出处(开头或结尾请添加转载出处,添加原文url地址),文章请勿滥用,也希望大家尊重笔者的劳动成果。