艺术探索—一、Activity的生命周期全面分析

1.1.1典型情况下生命周期

典型情况下生命周期是指有用户参与的情况下,Activity所经过的生命周期的改变。

onCreate()

表示Activity正在创建,生命周期的第一个方法。

onRestart()

表示Activity正在重新启动。一般情况下,当当前Activity,从不可见重新变为可见状态时,
该方法就会被调用。一般是用户行为所导致,比如用户按Home键切换到桌面或用户打开一个新的
Activity,紧接着又回到这个Activity。 onPause() —onStop()—onRestart()

onStart()

表示Activity正在被启动,即将开始。这是Activity已经可见,但还没有出现在前台,还无法与用户交互。(已经显示但开不到)

onResume()

表示Activity已经可见了,并出现在前台并开始活动。注意!onResume和onStart都表示Activity已经可见。不同之处,onStart时Activity还在后台,而在onResume时Activity才显示到前台。

onPause()

表示Activity正在停止,正常情况下onStop()紧接着就会被调用。在极端情况下,如果快速回到当前Activity,那么onResume()会被调用。onPause()必须执行完,新Activity的onResume()才会执行。不能进行耗时操作。

onStop()

表示即将停止,不能进行耗时操作。

onDestory()

表示Activity即将销毁,生命周期最后一个回调。可以进行资源回收和释放。

  1. 针对一个特定的Activity,第一次启动,回调如下:onCreate - onStart - onResume。
  2. 当用户打开新的Activity或者切换到桌面的时候,回调如下:onPause - onStop。特殊情况,当新的Activity采用透明主题,那么当前Activity则不会回调onStop。
  3. 当用户再次回到原Activity时,回调如下:onRestart - onStart - onResume。
  4. 当用户按back键回退时,回调如下:onPause - onStop - onDestroy。
  5. 当Activity被系统回收,再次打开时,生命周期的回调和1相同。
  6. 从生命周期来说,onCreate和onDestroy是配对的,分别标识Activity的重建和销毁。并且只有一次调用。从Activity是否可见来说,onStart和onStop是配对的,可能被调用多次;从Activity是否在前台来说,onResume和onPause是配对的,可能被调用多次。
Activity启动流程

启动Activity的请求会由Instrumentation来处理,然后它通过Binder向AMS发请求,AMS内部维护着一个ActivityStack并负责栈内的Activity的状态同步,AMS通过ActivityThread去同步Activity的状态,从而完成生命周期方法的调用。

在ActivityStack中的resumeTopActivityInnerLocked方法中有如下代码:

1
2
3
4
5
6
7
8
9
//we need to start pausing the current activity so the top one can be resumed
boolean dontWaitForPause = (next.info.flags& ActivityInfo.FLAG_RESUME_WHILE_PAUSING)!=0;
boolean pausing = mStackSupervisor.pauseBackStacks(userLeaving, KeyStore.TrustedCertificateEntry,dontWaitForPause);
if(mResumedActivity != null){
pausing != startPaUSINGlOCAKED(userLeaving,false,true,dontWaitForPause);
if(DEBUG_STATES){
Slog.d(TAG,"resumeTopActivityLocked:pausing" + mResumedActivity);
}
}

1.1.2 异常情况下的生命周期

异常情况下生命周期是指Activity被系统回收或由于当前设备的configuration发生改变从而导致Activity被销毁重建。

情况一:资源相关的系统配置发生改变导致Activity被杀死并重新创建

默认情况下,当系统配置发生改变后,Acitivity就会被销毁并重新创建。

异常情况下Activity的重建过程.png

当Activity在异常情况下终止时,系统会调用onSaveInstanceState来保存当前Activity的状态。这个方法的调用时机是在onStop()之前,而与onPause()则没有既定的时序关系,可能是在onPause之前调用,也可能是在 onPause之后调用。需要强调一点,这个方法只会出现在Acitivity被异常终止的情况下,正常情况下系统不会回调这个方法。当Activity被重新创建后,系统会调用onRestoreInstanceState,并且把Activity销毁时onSaveInstanceState方法所保存的Bundle对象作为参数同时传递给onRestoreInstanceState和onCreate方法。因此,我们可以通过onRestoreInstanceState和onCreate方法来判断Activity是否被重建,从时序上来说,onRestoreInstanceState的调用时机在onStart之后

同时,我们要知道,在onSaveInstanceState和onRestoreInstanceState方法中,系统自动为我们做了一定的恢复工作。当Activity在异常情况下需要重新创建时,系统会默认为我们保存当前Activity的视图结构,并且在Activity重启后为我们恢复这些数据,比如文本框中用户输入的数据,ListView滚动的位置等,这些View相关的状态系统都能够默认为我们恢复。具体针对某一个特定的View系统能够为我们恢复哪些数据,我们可以查看View的源码,和Acitivity一样,每个View都有onSaveInstanceState和onRestoreInstanceState这两个方法,看一下它们的具体实现,就能知道系统能够自动为每个View恢复哪些数据。关于保存和恢复View层次结构,系统的工作流程是这样的:首先Acitivtiy被意外终止时,Activity会调用onSaveInstanceState去保存数据,然后Activity会委托Window去保存数据,接着Window在委托它上面的顶级容器去保存数据。顶层数据是一个ViewGroup,一般来说它很可能是DecorView。最后顶层容器再去一一通知它的子元素来保存数据。这样整个数据保存过程就完成了。可以发现,这是一种很典型的委托思想,上层委托下层,父容器委托子元素去处理一件事情,这种思想在Android中有很多应用,比如View的绘制过程,事件分发都是采用类似的思想。数据恢复过程也是一样。

举例TextView的恢复过程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
@Override
public Parcelable onSaveInstanceState() {
Parcelable superState = super.onSaveInstanceState();

// Save state if we are forced to
final boolean freezesText = getFreezesText();
boolean hasSelection = false;
int start = -1;
int end = -1;

if (mText != null) {
start = getSelectionStart();
end = getSelectionEnd();
if (start >= 0 || end >= 0) {
// Or save state if there is a selection
hasSelection = true;
}
}

if (freezesText || hasSelection) {
SavedState ss = new SavedState(superState);

if (freezesText) {
if (mText instanceof Spanned) {
final Spannable sp = new SpannableStringBuilder(mText);

if (mEditor != null) {
removeMisspelledSpans(sp);
sp.removeSpan(mEditor.mSuggestionRangeSpan);
}

ss.text = sp;
} else {
ss.text = mText.toString();
}
}

if (hasSelection) {
// XXX Should also save the current scroll position!
ss.selStart = start;
ss.selEnd = end;
}

if (isFocused() && start >= 0 && end >= 0) {
ss.frozenWithFocus = true;
}

ss.error = getError();

if (mEditor != null) {
ss.editorState = mEditor.saveInstanceState();
}
return ss;
}

return superState;
}

Activity被销毁了以后调用onSaveInstanceState来保存数据,重新创建以后在onCreate和onRestoreInstanceState中都能够正确的回复我们之前存储的字符串。

针对onSaveInstanceState方法还有一点需要说明,那就是系统只会在Activity即将被销毁并且有机会重新显示的情况下才会去调用它。当Activity正常被销毁的时候,系统不会调用onSaveInstanceState,因为被销毁的Activity不可能再次被显示。

系统只在Activity异常终止的时候才会调用onSaveInstanceState 和 onRestoreInstanceState来存储和恢复数据,其他情况不会触发这个过程。

情况二:资源内存不足导致低优先级的Activity被杀死

Acitivity按照优先级从高到低可以分为如下三种:

  1. 前台Activity—正在和用户交互的Activity,优先级最高。
  2. 可见但非前台Activity—比如从Activity中弹出了一个对话框,导致Activity可见,但位于后台无法和用户直接交互。
  3. 后台Activity—已经被暂停的Activity,比如执行了onStop,优先级最低。

当系统内存不足时,系统就会按照上述优先级去杀死目标Acitivity所在的进程,并在后续通过onSaveInstanceState和onRestoreInstanceState来存储和恢复数据。如果一个进程中没有四大组件在执行,那么这个进程将很快被系统杀死。因此,一些后台工作不适合脱离四大组件而肚子运行在后台中,这样进程很容易被杀死。比较好的方法是将后台工作放在Service中从而保证进程有一定的优先级,这样就不会轻易地被系统杀死。

上面分析了系统的数据存储和恢复机制,我们知道,当系统配置发生改变后,Activity会被重新创建,那么有没有办法不重新创建呢。答案是有的。

系统配置中有很多内容,如果当某项内容发生改变后,我们不想系统重新创建Actiivty,可以给Activity指定configChanges属性。

Activity的configChanges属性
configChanges 功能
mmc: SIM卡唯一标识IMSI(国际移动用户识别码)中的国家代码,由三位数字组成,中国为460。此项标识mcc 代码改变
mnc: SIM卡唯一标识IMSI(国际移动用户识别码)中的运营商代码,由两位数字组成,中国移动TD系统为00,中国联通为01,中国电信为03.此项标识mnc发生改变。
locale: 设备本地位置发生改变,一般指切换系统语言。
touchscreen: 触摸屏发生了改变,正常情况下无法发生,可忽略。
keyboard: 键盘类型发生改变,比如用户外接键盘。
keyboardHidden: 键盘的可访问性发生了改变,比如用户调出了键盘。
navigation: 系统导航方式发生了改变,比如采用了轨迹球导航,很难发生,可忽略。
screenLayout: 屏幕布局发生了改变,很可能是用户激活了另外一个显示设备。
fontScale: 系统字体缩放比例发生了改变,比如用户选择了一个新的字号。
uiMode: 用户界面模式发生了改变,如开启夜间模式。
orientation: 屏幕方向发生改变,如旋转屏幕。
screenSize: 当屏幕尺寸信息发生了改变,旋转设备屏幕时,屏幕尺寸发生改变。当minSDK > 13时,会导致Activity重启。
smalllestScreenSize: 在实际的物理尺寸发生变化时,如用户切换到了外部的显示设备,当minSDK > 13时,会导致Activity重启。
layoutDirection: 当布局方向发生变化时,如切换阿拉伯语,默认方向为rtl。

如果我们没有在Activity的configChanges属性中指定该选项的话,当配置发生改变后就会导致Acitivity重新创建。
我们通常使用只有locale,orientation和keyboardHidden三个选项。

1
android:configChanges = "locale|orientation|keyboardHidden"

1.2 Activity的启动模式

1.2.1 Activity的LaunchMode

为什么Activtiy需要启动模式?

在默认情况下,当我们多次启动同一个Activity的时候,系统会创建多个实例并把他们一一放入任务栈中。当我们单击back键,会发现这些Activity会一一回退。任务栈是一种“后进先出”的栈结构。每按一下back键就会有一个Activity出栈,直到栈空为止。当栈中无任何Acitivity的时候,系统就会回收后这个任务栈。为了避免多次启动同一个Activity,系统创建多次同一实例的问题。Android系统在设计中提供了standard、singleTop、singleTask、singleInstance四种启动模式。

standard

系统默认模式,每次启动一个Activity都会重新创建一个新的实例,不管这个实例是否已经存在。被创建的实例的生命周期符合典型情况下Activity的生命周期。这是一种典型的多实例的实现,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。比如Acitivity A 启动了Activtity B(Standard),那么B就会进入到A所在的栈中。

值得注意的是,当我们用ApplicationContext 去启动standard模式的Acitivity时会报错,错误如下:

这是因为standard模式的Activity默认会进入启动它的Activity所属的任务栈中。但是由于非Activity类型的Context(如ApplicationContext)并没有所谓的任务栈,所以就会出现异常。
解决方法是为待启动Activity指定FLAG_ACTIVTIY_NEW_TASK标记位,这样启动的时候就会为被启动的Activity创建一个新的任务栈。这个时候待启动的Activity实际是以singleTask模式启动的。

singleTop

栈顶复用模式,如果新的Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建。同时它的onNewIntent方法会被回调,通过此方法的参数我们可以取出当前请求的信息。需要注意的是,这个Activity的onCreate、onStart不会被系统调用,因为它并没有发生改变。如果新的Activity的实例已存在但不是位于栈顶,那么新的Activity仍然会重新创建。举个例子,假设目前栈内的情况为ABCD,其中ABCD为四个Activity,A位于栈底,D位于栈顶,这个时候假设要再次启动D,如果D的启动模式为singleTop,那么栈内情况仍为ABCD。如果D的启动模式为standard,那么栈内情况为ABCDD。

singleTask

栈内复用模式,这是一种单实例模式,在这种模式下,只要Activity在一个栈中存在,那么多次启动此Activity都不会重新创建实例。和singleTop一样,系统也会回调其onNewIntent。具体一点,当一个具有singleTask模式的Activity A 请求启动后。系统首先会寻找是否存在A 想要的任务栈,如果不存在,就会重新创建一个任务栈。然后创建A的实例,把A放入栈中。如果存在A所需的任务栈,这时就要看A是否在栈中有实例存在,如果有实例存在,那么系统就会把A调到栈顶并调用它的onNewIntent()方法。如果实例不存在,就创建A的实例并把A压入栈中。

举例说明:

1
2
3
1.当前任务栈S1中的情况为ABC,这个时候Activity D以singleTask模式请求启动,其所需要的任务栈为S2,由于S2和D实例均不存在,所以系统会先创建任务栈S2,然后再创建D的实例并将其入栈到S2。
2.假设D所需的任务栈为S1,其他情况与例一相同,那么由于S1已经存在,所以系统会直接创建D的实例并将其入栈到S1。
3.如果D所需的任务栈为S1,并且当前任务栈S1的情况为ADBC,根据栈内复用原则,此时D不会重新创建,系统会把D切换到栈顶并调用其onNewIntent方法,同时由于singleTask默认具有clearTop效果,会导致栈内所有在D上面的Activity全部出栈。最终S1中的情况为AD。
singleInstance

单实例模式。这是一种加强的singleTask模式,它除了具有除了singleTask模式的所有特性之外,还加强一点,那就是具有此种模式的Activity只能单独地位于一个任务栈中。比如Activity A 是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的Activity,除非这个独特的任务栈被系统销毁了。

上面介绍了几种启动模式,这里需要指出一种情况,我们假设目前有2个任务栈,前台任务栈的情况为AB,后台任务栈为CD。这里假设CD的启动模式均为singleTask。现在请求D,则整个后台任务栈都会被切换到前台,这个时候整个后退列表变成了ABCD,当用户按back键的时候,列表中的Activity会一一出栈。

image.png

如果不是请求启动D,而是启动C,那么情况就不一样了。

image.png

另一个问题是,在singleTask启动模式中,多次提到某个Activity所需的任务栈。什么是Activtiy所需的任务栈呢?这要从一个参数说起:TaskAffinity,可以翻译为任务相关性。这个参数标识了一个Activity所需要的任务栈的名字,默认情况下,所有Activity所需的任务栈的名字为应用的包名。当然,我们可以为每个Activity都单独指定TaskAffnity属性,这个属性必须不能和包名相同,否则就相当于没有指定。TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用,在其他情况下没有意义。另外,任务栈可分为前台任务栈和后台任务栈,后台任务栈中的Activtiy位于暂停状态,用户可以通过切换将后台任务栈再次调到前台。

当TaskAffinity和allowTaskReparenting结合的时候,这种情况比较复杂,会产生特殊的效果。当一个应用A启动了应用B的某个Activtiy后,如果这个Activity的allowTaskReparenting属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈移到应用B的任务栈中,这还是很抽象。再具体点,比如现在有2个应用A和B,A启动了B的一个Activity C,然后按Home键回到桌面,然后再单击B的桌面图标。这个时候并不是启动了B的主Activity,而是重新显示了已经被应用A启动的Activity C,或者说C从A的任务栈转移到了B的任务栈中。可以这么理解,由于A启动了C,这个时候C只能运行在A的任务栈中,但C属于B应用,正常情况下,它的TaskAffinity值肯定不可能和A任务栈相同(因为包名不同)。所以,当B被启动后,B会创建自己的任务栈,这个时候系统发现C原本所想要的任务栈已经被创建了,所以就把C从A的任务栈中转移过来了。

如何指定Activity启动模式?
  • 第一种是通过AndroidMenifest为Activity指定启动模式。
1
2
3
4
<activity
android:name=".app.launcher.Launcher"
android:launchMode="singleTask"
android:screenOrientation="landscape">
  • 另外一种是通过在Intent中设置标志位来位Activity指定启动模式。
1
2
3
Intent intent = new Intent(getActivity().getBaseContext(), livePlayerActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

这两种方式都可以为Activity指定启动模式,但是两者还是有区别的。第二种的优先级要高于第一种,两者同时存在时,以第二种方式为准。其次上述方式在限定范围上有所不同,比如,第一种方式无法直接为Activity设定FLAG_ACTIVITY_CLEAR_TOP标识,而第二种方式无法为Activity指定singleIntance模式。

1.2.2 Activity的Flags
FLAG_ACTIVITY_NEW_TASK

这个标记位作用是为Activity指定“singleTask”启动模式,其效果和XML中指定该启动模式相同。

FLAG_ACTIVITY_SINGLE_TOP

这个标记位作用是为Activity指定“singleTop”启动模式,其效果和XML中指定该启动模式相同。

FLAG_ACTIVITY_CLEAR_TOP

具有此标记位的Activity,当它启动时,在同一个任务栈中所有位于它上面的Activity都要出栈,这个模式一般需要和FLAG_ACTIVITY_NEW_TASK配合使用,在这种情况下,被启动的Activity的实例如果已经存在,那么系统就会调用它的onNewIntent。如果被启动的Activity实例已经存在,那么系统就会调用它的onNewIntent。如果被启动的Activity采用standard模式启动,那么它连同它之上的Activity都要出栈,系统会创建新的Activity实例并放入栈顶。

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

具有此标记的Activity不会出现在历史Activity的列表中,当某些情况下,当我们不希望用户通过历史列表回到我们的Activity的时候可以使用这个标记位。它等同于在xml中指定Activity的属性android:excludeFromRecents = “true”。

1.3 IntentFilter的匹配规则

Activity的启动方式分为两种,显式调用和隐式调用。显式调用需要明确地指定被启动对象的组件信息,包括包名和类名,而隐式调用则不需要明确指定组件信息。原则上一个intent不应该即是显式调用又是隐式调用,如果二者共存的话以显式调用为主。显式调用很简单,这里主要介绍一下隐式调用。隐式调用需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果不匹配则无法启动目标Activity。

1
2
3
4
<activity

</intent-filter>
</activity>

为了匹配过滤列表,需要同时匹配过滤列表中的action,category,data信息,否则匹配失败。一个过滤列表中的action,category和data可以有多个,所有的action,category,data分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程。只有一个Intent同时匹配action类别,category类别,data类别才算完全匹配,只有完全匹配才能启动目标Activity。

action的匹配规则

action是一个字符串,系统预定义了一些action,同时我们也可以在应用中定义自己的action。action的匹配规则是Intent中的action必须能够和过滤规则中的aciton匹配(即action的字符串值完全一样)。一个过滤规则中可以有多个aciton,那么只要Intent中的action能够和过滤规则中的任何一个action相同即可匹配成功。Intent中如果没有指定action,那么匹配失败。总结一下,action的匹配要求Intent中的action存在且必须和过滤规则中的其中一个action相同,另外action区分大小写。

category的匹配规则

与action相同,category也是一段字符串,系统预定义了一些category,我们也可以在应用中定义自己的category。category的匹配规则与action不同,Intent中可以不指定category,(系统在调用startActivity或者startActivityForResult的时候会默认为Intent加上“android.intent.category.DEFAULT”。因此 如果我们想要让activity能够接收隐式调用,就必须在intent-filter中指定“android.intent.category.DEFAULT”这个category)

如果在Intent中指定了category,则要求每一个category都要与过滤规则中的任何一个category相同。

data匹配规则

data匹配规则与action类似,如果过滤规则中定义了data,那么Intent中必须也要能够定义可匹配的data。

data的结构
1
2
3
4
5
6
7
8
<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的结构
1
<scheme>://<host>"<port>/[<path>|<pathPrefix>|<pathPattern>]

这里再给几个实际的例子就好理解了

1
2
content://com.tracyliu.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/in
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:

其中path表示完整的路径信息

pathPattern:

pathPattern也表示完整的路径信息,但是它里面可以包含通配符“”,“”表示0或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表示真实的字符串,那么“”要写成“\”,“\”要写成“\\”;

pathPrefix:

pathPrefix表示路径的前缀信息。

data的匹配规则,它要求Intent中必须含有data数据,并且data数据能够完全匹配过滤规则中的某一个data,这里的完全匹配指过滤规则中出现的data部分,也出现在Intent中的data中。

下面分情况说明

(1) 如下过来规则

1
2
3
<intent-filter>
<data android:mimeType="image/*"/>
</intent-filter>

这种规则指定了媒体类型为所有类型的图片,那么Intent中的mimeType属性必须为“image/*”才能匹配,这种情况下虽然过滤规则没有指定URI,但是Intent中的URI部分的schema必须为content或者file才能匹配,这点是需要尤其注意的。

1
intent.setDataAndType(Uri.parse("file://abc"),"image/png");

另外,如果要为Intent指定完整的data,必须调用setDataAndType方法,不能先调用setData再调用setType,因为这两个方法会清除彼此的值。由下面setData源码可知

1
2
3
4
5
public Intent setData(Uri data) {
mData = data;
mType = null;
return this;
}

(2) 如下规律规则

1
2
3
4
<intent-filter>
<data android:mimeType="video/mpeg" android:scheme="http" .../>
<data android:mimeType="audio/mpeg" android:scheme="http" .../>
</intent-filter>

这种规则指定了两组data规则,且每个打他,都指定了完整的属性值,既有URI又有mimeType,为了匹配(2),我们写出如下示例:

1
intent.setDataAndType(Uri.parse("http://abc"),"video/png");

或者

1
intent.setDataAndType(Uri.parse("http://abc"),"audio/png");

关于data还有一个特殊情况需要说明下,这也是它和action不同的地方,如下两种特殊写法,他们的作用是一样的。

>
1
2
3
4
5
6
7
8
9
10
<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能够匹配我们的隐式Intent。判断方法有两种采用 PackageManger的resolveActivity方法或Intent的resolveActivity方法,如果它们找不到匹配的Activity就会返回null,我们通过判断