1. Activity生命周期分析
这里生命周期分为两部分内容:
- 一是典型情况下的生命周期
指在有用户参与的情况下,Activity所经过的生命周期的改变 - 另一部分是异常情况下的生命周期
Activity被系统回收或由于当前设备的Configuration发生改变从而导致Activity被销毁重建,与典型情况下的关注点略有不同。
1.1 典型情况下的生命周期
正常情况下会经过如下生命周期:
onCreate
:表示Activity正在被创建,做一些初始化工作,如加载布局,初始化一些数据onRestart
:表示Activity正重新启动。当当前Activity从不可见重新变为可见状态,就会被调用onStart
:表示Activity正在启动,此时Activity已经可见,未在前台,无法交互onResume
:表示Activity已经可见在前台,可交互了onPause
:表示Activity正在停止,可做一些不耗时操作,如停止动画,存储数据。注意
:onPause必须先执行完,新Activity的onResume才会执行onStop
:表示Activity即将停止,不能做太耗时操作。onDestroy
:表示Activity即将销毁,可以做一些回收工作与资源释放
附加情况:
- 用户打开新Activity或者切换到桌面,回调:onPause -> onStop 如果新Activity采用透明主题,当前Activity不会回调onStop
- 用户按back键回退,回调为:onPause -> onStop -> onDestroy
- Activity被系统回收再次打开,生命周期回调一样,但是不代表所有过程一样
- 整个生命周期,onCreate与onDestroy只可能有一次调用
问题一:onStart和onResume,onPause和onStop有什么实质性不同?
这两个配对回调的意义不同,从是否可见,是否在前台,其他基本无差别
问题二:当前Activity为A,新开打B,B的onResume和A的onPause哪个先执行?
这个可以从源码获得解释,在新Activity启动之前,栈顶的Activity需要先onPause后,新的Activiy才能启动
1.2 异常情况下的生命周期
1.2.1 资源相关系统配置发生改变导致Activity被杀死并重新创建
典型例子本来手机屏幕是竖屏状态,突然旋转屏幕,系统配置发生了变化,默认情况下,Activity就会被销毁并且重新创建,当然也可以阻止系统重新创建我们的系统
当系统配置发生改变后,Activity会被销毁,其onPause、onStop、onDestroy均会被调用,系统会调用onSaveInstanceState来保存当前的Activity状态,在onStop之前调用,与onPause无既定时序关系。Activity被重新创建后,系统会调用onRestoreInstanceState,并且把onSaveInstanceState保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate方法。onRestoreInstanceState调用时机在onStart之后
系统自动为我们做了一定的恢复工作,系统默认为我们保存当前Activity的视图结构,如文本框中输入数据,ListView滚动位置等,具体针对哪个View系统能为我们恢复哪些数据,可以查看View的源码。
在onSaveInstanceState中存储一个字符串,Activity销毁重建可以在onCreate或onRestoreInstanceState中接收,二者区别是onCreate需要判空而后者不需要,onRestoreInstanceState被调用则一定有值
Activity正常销毁时,系统不会调用onSaveInstanceState。
1.2.2 资源内存不足导致低优先级的Activity被杀死
优先级从高到低分为三种:
- 前台Activity:正在和用户交互,优先级最高
- 可见非前台Activity:
- 后台Activity:优先级最低
内存不足,按照上述优先级去杀死目标Activity所在进程,后续通过onSaveInstanceState和onRestoreInstanceState存储和恢复数据。一个进程如果没有四大组件在执行,很容易被系统杀死。较好方法是将后台工作放入Service中从而保证进程有一定的优先级。
1.2.3 系统异常销毁Activity不想重建
系统配置发生改变,如果不想系统重新创建Activity,可以给Activity指定configChange属性。
示例:如果不想Activity在屏幕旋转时重新创建就可以给configChanges属性添加orientation这个值。
android:configChanges="orientation"
如果想要指定多个值,可以用“|”连接起来。一般常用的只有local,orientation和keyboardHidden这三个选项,其他很少使用
2. Activity启动模式
2.2.1 Activity的LaunchMode
目前有四种启动模式:standard、singleTop、singleTask和SingleInstance
standard
:标准模式,系统默认模式,每次启动一个Activity都会重新创建一个新的实例,不管这个实例是否已经存在。这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。当用ApplicationContext去启动standard模式的Activity,会报错,因为非Activity类型的Context没有所谓的栈,解决方法是将待启动Activity指定为FLAG_ACTIVITY_NEW_TASK标记位。singleTop
:栈顶复用模式。新Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建,同时它的onNewIntent方法会被回调,通过此方法的参数我们可以取出当前请求的信息。singleTask
:栈内复用模式。这是一种单实例模式。只要Activity在一个栈中存在,多次启动此Activity都不会重新创建实例,并把这个活动之上的所有活动统统出栈。如原来栈是ABC,现在启动B,则栈为BC。singleInstance
:单实例模式。具有此模式的Activity只能单独地位于一个任务栈中。
对于一个具有singleTask模式的Activity请求启动后,系统首先会寻找是否存在A想要的任务栈。那么什么是Activity所需要的任务栈?需要从一个参数说起:TaskAffinity。这个参数标识了一个Activity所需要的任务栈的名字。默认所有Activity所需的任务栈名字为应用的包名,也可以为每个Activity都单独指定TaskAffinity属性,这个属性必须不能和包名相同。TaskAffinity主要和singleTask启动模式或allowTaskReparenting属性配对使用。
任务栈分为前台任务栈和后台任务栈,用户可以通过切换将后台任务栈再次调到前台。
当TaskAffinity
和allowTaskReparenting
结合时,这种情况比较复杂,会产生特殊
的效果。当一个应用 A 启动了应用 B 的某个 Activity 后,如果这个 Activity 的
allowTaskReparenting 属性为 true 的话,那么当应用 B 被启动后,此 Activity 会直接从应用
A 的任务栈转移到应用 B 的任务栈中。
给Activity指定启动模式有两种方法:
- 第一种是通过AndroidMenifest为Activity指定启动模式:
<activity
android:name="com.ryg.chapter_1.SecondActivity"
android:configChanges="screenLayout"
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);
二者的区别是:第二种优先级高于第一种;其次,二者限定范围不同:第一种方式无法直接为Activity设定FLAG_ACTIVITY_CLEAR_TOP 标识,而第二种方式无法为 Activity 指定 singleInstance 模式。
2.2.2 Activity的Flags
Activity的标记位很多,这里给出一些常用的标记位,具体使用可以查看官方文档:
- FLAG_ACTIVITY_NEW_TASK
是为 Activity 指定“singleTask”启动模式 - FLAG_ACTIVITY_SINGLE_TOP
为 Activity 指定“singleTop”启动模式 - FLAG_ACTIVITY_CLEAR_TOP
具有此标记位的 Activity,当它启动时,在同一个任务栈中所有位于它上面的 Activity 都要出栈。这个标记位一般会和 singleTask 启动模式一起出现,在这种情况下,被启动 Activity的实例如果已经存在,那么系统就会调用它的 onNewIntent。如果被启动的 Activity 采用 standard模式启动,那么它连同它之上的 Activity 都要出栈,系统会创建新的 Activity 实例并放入栈顶。singleTask 启动模式默认就具有此标记位的效果 - FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有这个标记的 Activity 不会出现在历史 Activity 的列表中,当某些情况下我们不希望
用户通过历史列表回到我们的 Activity 的时候这个标记比较有用。它等同于在 XML 中指定
Activity 的属性 android:excludeFromRecents=“true”。
3. IntentFilter匹配规则
启动 Activity 分为两种,显式调用和隐式调用。隐式调用需要Intent 能够匹配目标组件的 IntentFilter 中所设置的过滤信息,如果不匹配将无法启动目标Activity。IntentFilter 中的过滤信息有 action、category、data。
示例:
<activity
android:name="com.ryg.chapter_1.ThirdActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:launchMode="singleTask"
android:taskAffinity="com.ryg.task1" >
<intent-filter >
<action android:name="com.ryg.charpter_1.c"/>
<action android:name="com.ryg.charpter_1.d"/>
<category android:name="com.ryg.category.c"/>
<category android:name="com.ryg.category.d"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
</activity>
为了匹配过滤列表,需要同时匹配过滤列表中的 action、category、data 信息,否则匹
配失败。一个 Activity 中可以有多个 intent-filter,一个 Intent 只要能匹配任何一组 intent-filter 即可成功启动对应的 Activity。
<activity android:name="ShareActivity">
<!-- This activity handles "SEND" actions with text data -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain"/>
</intent-filter>
<!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media
data -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND_MULTIPLE"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="application/vnd.google.panorama360+jpg"/>
<data android:mimeType="image/*"/>
<data android:mimeType="video/*"/>
</intent-filter>
</activity>
- action的匹配规则
action 是一个字符串,系统预定义了一些 action,同时我们也可以在应用中定义自己的action。action 的匹配规则是 Intent 中的 action 必须能够和过滤规则中的 action 匹配,这里说的匹配是指 action 的字符串值完全一样。一个过滤规则中可以有多个 action,那么只要Intent 中的 action 能够和过滤规则中的任何一个 action 相同即可匹配成功。Intent没有指定action,则匹配是被。action区分大小写 - category匹配规则
category 是一个字符串,系统预定义了一些 category,同时我们也可以在应用中定义自己的 category。category 的匹配规则要求Intent中如果含有category,那么所有的 category 都必须和过滤规则中的其中一个 category 相同。Intent 中如果出现了 category,不管有几个 category,对于每个 category 来说,它必须是过滤规则中已经定义了的 category。Intent 中可以没有 category,这个Intent仍可以匹配成功。 - data匹配规则
如果过滤规则中定义了data,那么Intent中必须也要定义可匹配的data。
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>]
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
Scheme
:URI模式,比如http、file、content等,如果URI没有指定scheme,整个URI参数都是无效的
Host
:URI模式,比如www.baidu.com,同样未指定整个URI无效
Port
:URI中端口号,比如80,仅当URI指定了scheme和host参数是有意义
path、pathPattern,pathPrefix
:表示路径信息,path表示完整路径信息,pathPattern也是,不过可以包含通配符“*”;pathPrefix表示路径前缀信息
data匹配规则与action类似。URI的默认值为content和file,即没有指定URI,但是Intent中URI部分scheme必须为content或file才能匹配
如果要为Intent指定完整的data,必须调用setDataAndType方法,先后调用会彼此清除对方的值
intent.setDataAndType(Uri.parse("file://abc"),"image/png")。
data有两种特殊的写法:
<intent-filter . . . >
<data android:scheme="file" android:host="www.baidu.com" />
. . .
</intent-filter>
<intent-filter . . . >
<data android:scheme="file" />
<data android:host="www.baidu.com" />
. . .
</intent-filter>
使用隐式方式启动一个Activity,可以做一下判断是否有匹配的Activity:可采用PackageManager的resolveActivity
方法或者 Intent 的 resolveActivity 方法。找不到匹配的返回null。PackageManager 还提供了 queryIntentActivities
方法,这个方法和 resolveActivity 方法不同的是:它不是返回最佳匹配的 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 的 Activity。使用这个标记位的意义在于,只要上述两个方法不返回 null,那么 startActivity 一定可以成功。如果不用这个标记位,就可以把 intent-filter 中 category 不含 DEFAULT 的那些 Activity 给匹配出来,从而导致 startActivity 可能失败。因为不含有 DEFAULT 这个 category 的 Activity是无法接收隐式 Intent 的。