Activity的启动与生命周期
这是我参与「第四届青训营 」笔记创作活动的第4天
Activity的生命周期全面分析
典型情况下的生命周期分析
Activity在不同情况调用生命周期的函数过程
1.对于一个Activity,第一次启动时调用:onCreate->onStart->onResume
2.当用户打开新的Activity或者切换到桌面时调用:onPause->onStop
如果新的Activity采用了透明主题,则当前Activity不会调用onStop
3.当用户再次回到原Activity时调用:onRestart->onStart->onResume
4.当用户按back键回退时调用:onPause->onStop->onDestory
5.当Activity被系统回收后再次打开,生命周期方法回调过程和1一样,但并不代表所有过程都一样
关于Activity的启动与生命周期的问题
-
onStart和onResume,onPause和onStop从描述上看起来差不多,对我们有什么本质的区别?
从实际使用过程来说, onstart和 onresume、 onpause和 onstop看起来的确差不多,甚至
我们可以只保留其中一对,比如只保留 onstart和 on Stop。既然如此,那为什么 Android系统
还要提供看起来重复的接口呢?其实这两个配对的回调分别表示不同的意义, on Start和 onstop
是从 Activity是否可见这个角度来回调的,而 onresume和 onpause是从 Activity是否位于前台
这个角度来回调的,除了这种区别,在实际使用中没有其他明显区别
-
假设当前 Activity为A,如果这时用户打开一个新 Activity B,那么B的 on Resume和A的 on Pause哪个先执行呢?
第二个问题可以从 Android源码里得到解释。从 Activity的启动过程来看,我们来看一下系统源码。 Activity的
启动过程的源码相当复杂,涉及 Instrumentation、 Activity Thread和Activity Managerservice(下面简称AMS)。
简单理解,启动 Activity的请求会由 Instrumentation来处理,然后它通过 Binder向AMS发请求,AMS内部维护着
一个 Activity Stack并负责栈内的 Activity的状态同步,AMs通过 Activity Thread去同步Activity的状态从而完成
生命周期方法的调用。在 Activity Stack中的 resumetopactivitynonelocked方法中,有这么一段代码:
// We need to start pausing the current activity so the top one
//can be resumed
boolean dontWaitForPause = (next.info.flags&Activityinfo.FLAG.RESUMEWHILE PAUSING) != 0;
boolean pausing = mStackSupervisor.pauseBackStacks(userleaving,true,dontWaitForPause);
if (mResumedActivity != null) {
pausing != startPausingLocked(userLeaving,false,true,dontwaitorpause);
if (DEBUG STATES) Slog. d(TAG,"resume Topactivitylocked: Pausing"+mResumedActivity);
}
从上述代码可以看出,在新 Activity启动之前,栈顶的 Activity需要先 onPause后,新Activity才能启动。最终,在 Activity Stacksupervisor中的 realstartactivity Locked方法会调用如下代码。
app.thread.scheduleLaunchActivity(new Intent(r.intent), r. appToken,
System.identityHashcode(r),r.info,newConfiguration(mservice,mconfiguration),
r,compat,r,task.voiceinteractor,app.repprocstate,r.icicle,r.persistentstate,
results,newIntents,!andResume,mService.isNextTransitionForward(),profilerInfo);
这个 app. thread的类型是 IApplicationThread,而 IApplicationThread的具体实现是 Activity Thread中的 Application Thread所以,这段代码实际上调到了 Activity Thread的中,即 ApplicationThread的 scheduleLaunchactivity方法,而 scheduleLaunchactivity方法最终会完成新 Activity的 on Create、 on Start、 on Resume的调用过程。因此,可以得出结论是旧 Activity先 on Pause,然后新 Activity再启动。
异常情况下的生命周期分析
-
资源相关的系统配置发生改变导致Activity被杀死并重建
系统配置发生变化是指:比如当前Activity从竖屏状态转换到横屏状态,由于系统配置发生了变化,在默认情况下,Activity就会被销毁重新创建,当然也可以阻止系统重新创建我们的Activity
在默认情况下,如果Activity不做特殊处理,那么当系统配置发生变化时,Activity就会被销毁重新创建,生命周期如图所示
当系统配置发生改变后, Activity会被销毁,其 onPause、onStop、onDestroy均会被调用,同时由于 Activity是在异常情况下终止的,系统会调用 on Savelnstancestate来保存当前Activity的状态。这个方法的调用时机是在 onStop之前,它和 onPause没有既定的时序关系,它既可能在 onPause之前调用,也可能在 onPause之后调用。需要强调的一点是,这个方法只会出现在 Activity被异常终止的情况下,正常情况下系统不会回调这个方法。
当Activity被重新创建后,系统会调用 onRestorelnstanceState,并且把 Activity销毁时onSavelnstancestate方法所保存的 Bundle对象作为参数同时传递给 onRestorelnstanceState和 on Create方法。因此,我们可以通过 onRestorelnstanceState和 onCreate方法来判断 Activity是否被重建了,如果被重建了,那么我们就可以取出之前保存的数据并恢复,从时序上来说, onRestorelnstanceState的调用时机在 on Start之后。
同时,在onSavelnstancestate和onRestorelnstanceState方法中,系统为自动做了一定的恢复工作,会默认保存当前的视图结构,并在重启Activity后为我们恢复这些数据,比如文本框中用户输入的数据和选中的状态,ListView的滚动位置等等,具体针对某个特定的view系统能恢复什么数据需要查询onSavelnstancestate和onRestorelnstanceState方法的源码才能得知
关于保存和恢复vew层次结构,系统的工作流程是这样的:首先 Activity被意外终止时, Activity会调用 onSavelnstance State去保存数据,然后 Activity会委托 Window去保存数据,接着 Window再委托它上面的顶级容器去保存数据。顶层容器是一个 ViewGroup一般来说它很可能是 DecorView。最后顶层容器再去一一通知它的子元素来保存数据,这样整个数据保存过程就完成了。可以发现,这是一种典型的委托思想,上层委托下层、父容器委托子元素去处理一件事情,这种思想在 Android中有很多应用,比如vew的绘制过程、事件分发等都是采用类似的思想。至于数据恢复过程也是类似的
-
资源内存不足导致低优先级的Activity被杀死
Activity的优先情况,按照从高到低,一共有以下三种情况
(1)前台 Activity———正在和用户交互的 Activity,优先级最高。
(2)可见但非前台 Activity———比如 Activity中弹出了一个对话框,导致 Activity可见但是位于后台无法和用户直接交互。
(3)后台 Activity———已经被暂停的 Activity,比如执行了 onStop,优先级最低。
当系统资源不足时,系统就会按照由低到高的优先级来杀死目标Activity所在的进程,并在onSavelnstancestate和onRestorelnstanceState方法中来储存和恢复数据,如果一个进程没有四大组件在执行,那么它很快就会被系统杀死,如果后台工作不想被系统杀死就把后台工作放到Service中从而保证进程有一定的优先级。
阻止异常情况下的Activity重建
系统配置中有很多内容,如果当某项内容发生改变后,我们不想系统重新创建 Activity,可以在AndroidMenifest.xml中给对应的Activity指定 configChanges属性,比如不想让 Activity在屏幕旋转的时候重新创建,就可以给configChanges属性添加 orientation这个值,如果想指定多个值,可以用“ | ”号来连接如下所示。
当Activity不被系统重新创建时,会调用Activity的onConfigurationChanged方法,可以在这里进行自己的特殊操作
android:configChanges="orientation|keyboardHidden"
系统配置列表
相关面试题
Q1:Activity 上有 Dialog 时按 Home 键时的生命周期 A:onPause -> onSaveInstanceState -> onStop。需要注意的是,在 Activity 中弹出 Dialog 时,并不会回调 onPause 方法;按 Home 键会单独触发 onSaveInstanceState 方法。
Q2:两个 Activity 跳转时必然会执行的是哪几个方法? A:分情况分析一下: 当 Activity A 中打开新的 Activity B 时,这时 A 会回调 onPause、onSaveInstanceState、onStop,B 会回调 onCreate、onStart、onResume; 当 Activity A 中打开透明主题的 Activity B 时,这时 A 会回调 onPause,B 会回调 onCreate、onStart、onResume; 当 Activity A 打开 Activity B 时,B 已经存在于栈中并且 B 的启动模式是 singleTask,这时 A 会回调 onPause、onStop、onDestroy,B 会回调onRestart 、 onStart、onNewIntent、onResume方法。 综上分析,A 必然执行的方法是 onPause,B 是onStart 、`onResume``。
Q3:adb shell am kill packagename 这个命令什么时候起作用? 这个命令用来告诉设备去终止指定包名的进程,但仅当那个应用是在后台的时候。
Activity的启动模式
Activity的LaunchMode
- standard (标准模式),这是系统的默认模式。每次启动一个 Activity 都会创建一个新的实例,不管这个实例是否已经存在。被创建的实例的生命周期符合典型情况下的 Activity 的生命周期。一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。在这种模式下,谁启动了这个Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。
- singleTop(栈顶复用模式)。在这种模式下,若新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的 onNewIntent 方法会被回调,但是 onCreate、onStart 方法不会被系统调用(这是因为它并没有发生改变)。若新 Activity 的实例已存在但不是位于栈顶,那么新 Activity 仍然会重新创建。
- singleTask(栈内复用模式),这是一种单实例模式。一个singleTask模式下的Activity启动时,系统先寻找有无该Activity所需要的任务栈,如果无,则先创建该任务栈再创建该Activity,如果存在则直接创建该Activity,如果该栈存在且栈内存在该Activity,则将该Activity之上的活动出栈。在这种模式下,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,同时它的 onNewIntent 方法会被回调。
- singleInstance(单实例模式),这是一种加强的 singleTask 模式。加强的是具有此种模式的 Activity 只能单独地位于一个任务栈中。
默认情况下,所有Activity所需的任务栈的名字为应用的包名,可以为Activity单独指定TaskAffinity属性,这个属性不能和包名相同,TaskAffinity属性主要和singleTask模式或者allowTaskReparenting配对使用,在其他情况下没有意义。
任务栈还分为前台任务栈和后台任务栈,用户可以通过切换来将后台任务栈切换至前台
如果TaskAffinity和allowTaskReparenting结合使用的时候,会产生特殊的效果,例如:现在有两个应用A,B,应用A启动了应用B的一个Activity C,然后回到桌面后,再点击应用B,这个时候并不是启动了应用B原先默认启动的Activity而是Activity C,Activity C由于是由应用A所启动的,所以Activity C一开始只能应用A的任务栈中运行,但是C属于应用B的活动,正常情况下C的TaskAffinity和应用A的任务栈不可能相同,当应用B被启动后,B的任务栈也被创建,系统发现Activity C所需的任务栈已经被创建了,则将Activity C放到B的任务栈中
指定 Activity 启动模式的方法及其区别是什么?
启动模式允许你去定义如何将一个Activity 的实例和当前的任务进行关联,有两种方法,第一种是在 AndroidManifest 中通过 android:launchMode 属性来给 Activity 指定启动模式,第二种是通过在 Intent 中设置标志位来为 Activity 指定启动模式。也就是说,当 Activity A 启动了 Activity B,第一种方式是 Activity B 定义自己如何与当前的任务进行关联,第二种方式是 Activity A 在 Intent 中要求 Activity B 怎样与当前任务进行关联。 区别:首先,优先级上,第二种方式的优先级要高于第一种,当两种都存在时,以第二种为准;其次,上述两种方式在限定范围上不同,比如,第一种方式无法直接为 Activity 设置 FLAG_ACTIVITY_CLEAR_TOP 标志,而第二种方式无法为 Activity 指定 singleInstance 模式。
Activity的Flags
下面是几个常用的标记位:
FLAG_ACTIVITY_NEW_TASK
设置了这个
flag,新启动 Activity 就会被放置到一个新的任务当中(与singleTask有点类似,但不完全一样)。和singleTask的区别是:当使用ApplicationContext去启动一个standard的 Activity 时会报错,android.util.AndroidRuntimeException: Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?这时只能通过给
Intent添加FLAG_ACTIVITY_NEW_TASK的方式解决报错,通过给新 Activity添加android:launchMode="singleTask"的方式是不能解决报错的。
FLAG_ACTIVITY_SINGLE_TOP
等同于为 Activity 指定 “singleTop”启动模式。
FLAG_ACTIVITY_CLEAR_TOP
设置了这个flag,如果要启动的 Activity 在当前任务中已经存在了,就不会再次创建这个Activity的实例,而是会把这个Activity之上的所有Activity全部关闭掉。比如说,一个任务当中有A、B、C、D四个Activity,然后D调用了startActivity() 方法来启动B,并将 flag 指定成FLAG_ACTIVITY_CLEAR_TOP,那么此时C和D就会被关闭掉,现在返回栈中就只剩下 A 和 B了。
此时Activity B会接收到这个启动它的Intent,你可以决定是让Activity B调用 onNewIntent() 方法(不会创建新的实例),还是将 Activity B 销毁掉并重新创建实例。如果 Activity B 没有在AndroidManifest中指定任何启动模式(也就是standard模式),并且 Intent 中也没有加入一个FLAG_ACTIVITY_SINGLE_TOP 的 flag,那么此时 Activity B 就会销毁掉,然后重新创建实例。而如果Activity B在 AndroidManifest中指定了singleTop或者 singleTask,或者是在 Intent 中加入了一个FLAG_ACTIVITY_SINGLE_TOP flag,那么就会调用 Activity B 的onNewIntent()方法。也就是singleTask模式。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
具有这个标记的Activity不会出现在历史Activity的列表中,当某些情况下我们不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用。它等同于在XML中指定Activity的属性 android:excludeFromRecents="true"
IntentFilter的匹配规则
启动 Activity 的两种方式
显式调用和隐式调用。当两种方式都存在时,显式调用优先于隐式调用。显式调用需要明确地指定被启动对象的组件信息,包括包名和类名;隐式调用不需要明确指定组件信息,需要 Intent 能够匹配目标组件的 IntentFilter 中所设置的过滤信息,IntentFilter的过滤信息有action,category,data
action 的匹配规则
action 的匹配要求 Intent 中的 action 存在且必须和过滤规则中的其中一个 action 相同。注意,Intent 中如果没有指定 action,那么匹配失败。
指定在 Intent 中的 action 必须和过滤器中的 action 列表中的一个相匹配; 如果过滤器中不包含任何 action,所有的隐式 Intent 都会失败; action区分大小写,大小写有任何的不同都会匹配失败
category 的匹配规则
category 要求 Intent 可以没有 category,但是如果你一旦有 category,不管有几个,每个都要能够和过滤规则中的任何一个 category 相同。
指定在 Intent 中的 category 集合必须是过滤器中定义的 category 集合的子集。
在隐式调用时,必须在 intent-filter 中指定 “android.intent.category.DEFAULT” 这个 catogery,系统在调用 startActivity 或者 startActivityForResult 的时候会默认为 intent加上 “android.intent.category.DEFAULT” 这个 catogery。如果不指定,就会匹配失败。
data 的匹配规则
如果Activity的过滤规则中定义了data那么Intent中必须也要定义可匹配的data,data 数据要能够完全匹配过滤规则中的某一个 data。data 有两部分组成,mimeType 和 URI。
mimeType指媒体类型,比如image/jpeg,audio/mpeg4-generic和video/*等,可以表示图片,文本,视频等不同的媒体格式
URI的结构<scheme>://<host>:<port>/[<path> | <pathPrefix> | <pathPattern>]scheme:URI的模式,比如http、file、 content等,如果URI中没有指定 scheme,那么整个URI的其他参数无效,这也意味着URI是无效的。
host:URI的主机名,比如www.baidu.com,如果host未指定,那么整个URI中的其他参数无效,这也意味着URI是无效的。
port:UR中的端口号,比如80,仅当UR中指定了 scheme和host参数的时候port参数才是有意义的。
path,pathPrefix和pathPattern:这三个参数表述路径信息,其中path表示完整的路径信息;pathPrefix表示路径的前缀信息; pathPattern也表示完整的路径信息,但是它里面可以包含通配符“ * ”,“ * ”表示0个或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表示真实的字符串,那么'' * "要写成
\*,``要写成\\例如:
content://com.example.project:200/folder/subfolder/etc
http://www.baiducom:80/search/info
隐式启动的注意事项
当用隐式启动一个Activity时,可以做一下判断,看看是否有Activity能够匹配我们的隐式Intent,如果不做判断,并且没有能够匹配的Activity的话,将会出现报错,判断方法有两种:采用PackageManager的resolveActivity方法或者Intent的resolveActivity方法,如果找不到匹配的Activity将会返回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 android: name="android.intent.category.DEFAULT">这个 category的 Activity。使用这个标记位的意义在于,只要上述两个方法不返回null,那么 startactivity一定可以成功。如果不用这个标记位,就可以把 intent-filter中 category不含 DEFAULT的那些 Activity给匹配出来,从而导致 startactivity可能失败,因为不含有 DEFAULT这个category的 Activity是无法接收隐式Intent的。在action和 category中,有一类 action和 category比较重要,它们是
<action android:name"android.intent.action.MAIN"> <category android:name="andorid.intent.category.LAUNCHER"> 这两者的共同作用是用来标明这是一个入口Activity并且会出现在系统的应用列表中,少了任何一个都没有实际意义,也无法出现在系统的应用列表中
实现指定分享功能
例如,将特定内容分享至微信
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.setType("text/plain");
//分享内容标题
intent.putExtra(Intent.EXTRA_SUBJECT, "分享标题");
//分享内容
intent.putExtra(Intent.EXTRA_TEXT, "分享内容");
// 可以选择默认分享
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
startActivity(intent);
}
// 不可以选择默认分享
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
Intent chooser = Intent.createChooser(intent, "分享");
startActivity(chooser);
}
// 指定分享到微信
List<ResolveInfo> list = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
boolean matched = false;
for (int i = 0; i < list.size(); i++) {
ResolveInfo resolveInfo = list.get(i);
if ("com.tencent.mm.ui.tools.ShareImgUI".equals(resolveInfo.activityInfo.name)) {
matched = true;
intent.setComponent(new ComponentName("com.tencent.mm", "com.tencent.mm.ui.tools.ShareImgUI"));
startActivity(intent);
break;
}
}
if (!matched) {
startActivity(intent);
}
获取应用注册的所有 Activity 和 Receiver
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_ACTIVITIES);
ActivityInfo[] activities = packageInfo.activities;
if (activities != null) {
for (ActivityInfo activity : activities) {
Log.d(TAG, "onClick: " + activity.name);
}
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}