《Android开发艺术》Activity的生命周期和启动模式
Activity的生命周期和启动模式
本博客是对 任玉刚先生的 Android开发艺术探索 的总结。仅供个人学习使用
如需系统学习,请购买正版书籍。
第一章 Activity的生命周期和启动模式
前言
除了Window, Dialog和Toast,我们能见到的界面还有 Avtivity。
1.1 Activity的生命周期全面分析
两部分:
- 典型情况下的生命周期
- 有用户参与的情况下,Activity所经历的生命周期的改变
- 异常情况下的生命周期
- Activity被系统回收或者由于当前设备的Configuration发生改变从而导致Activity被销毁重建
1.1.1 典型情况下的生命周期分析
Activity的正常生命周期:
![img](https://img-blog.csdnimg.cn/20190605121015428.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl8zODI0NDE3NA==,size_16,color_FFFFFF,t_70)
-
onCreate方法:Activity正在被创建,用于初始化,譬如加载布局、初始化活动所需数据,在活动第一次创建时候被调用;
-
onResart方法:Activity正在重新启动,当前Activity从不可见重新变为可见时,onRestart就会被调用。
-
onStart方法:Activity正在被启动,尚未显示在前台们无法进行交互,由不可见变为可见时调用;
-
onResume方法:Activity已经可见了,准备好与用户进行交互时;
-
onPause方法:Activity正在停止,另一个活动来到前台,部分可见;若快速回到当前Activity,那么onResume就会被调用,可以做一些数据存储,停止动画等工作;
-
onstop方法:Activity即将停止,活动完全不可见,轻量级的资源回收;
-
onDestroy方法:Activity即将被销毁,活动被销毁之前调用,回收工作和资源释放;
-
onResart方法:Activity正在重新启动,当前Activity从不可见重新变为可见时,onRestart就会被调用。
附加具体说明:
1.针对一个特定的Activity,第一次启动,回调如下:onCreate —> onStart —> onResume
2.当用户打开新的Activity或者切换到桌面的时候,回调如下:onPause —> onStop
3.当用户再次回到原来的Activity,回调如下:onRestart —> onStart —> onResume
4.当用户按back键的时候回调如下:onPause —> onStop —> onDestroy
5.从整个生命周期来说,onCreate和onDestroy是配套的,标示着Activity的创建和销毁,只可能有一次调用,从Activity是否可见来说,onStart和onStop是配套的,随着用户的操作和设备屏幕的点亮和熄灭,这两个方法可能被调用多次,从Activity是否在前台来说,onResume和onPause是配套的,随着用户操作或者设备的点亮和熄灭,这两个方法可能被多次调用。
6.问:onStart和onResume,onPause和onStop看起来的确差不多,有什么实质的不同?
答:这两个配对的回调分别代表不同的意义,onStart和onStop是从Activity是否可见这个角度来回调的,onPause 和 onResume 是从 Activity是否位于前台的角度来回调的。除了这种区别,在实际的使用中,没有其他明显的区别。
7.问:假设当前Activity为A,如果用户打开了一个新的Activity为B,那么B的onResume和A的onPause谁先执行?
答:肯定是栈顶的Activity需要先onPause后,新的Activity才能启动。这是因为Instrumentation 通binder向ActivityManagerService(AMS)发请求,AMS内部维护着一个ActivityStack,并负责栈内的Activity的状态同步,AMS通过ActivityThread去同步Activity的状态从而完成生命周期方法的调用。在ActivityStack中的resumeTopActivityLnnerLocked方法中告诉我们需要先Pause,在ApplicationThread的scheduleLaunchActivity方法中,告诉了我们新的Activity的创建过程。
onPause(A) onCreate(B) onstart(B) onResume(B) onStop(A)
8.模拟调用顺序。
我们知道onPause和onStop都不能做耗时的操作,尤其是onPause,这也意味着,我们应当尽量的在onStop中做操作,从而使新的Activity尽快显示出来并且换到前后台。(配合相关代码)
1.1.2 异常情况下的生命周期分析
当资源相关的系统配置发生改变以及系统内存不足的时候,Activity就有可能被杀死,下面分析这两种情况。
(1)情况1:资源相关的系统配置发生改变导致Activity被杀死并重新创建
情形:当应用程序启动时,系统会根据当前设备的情况去加载合适的Resources资源,比如说横屏手机和竖屏手机会拿着两张不同的图片(设定了landscape或者portrait状态下的图片),比如之前Activity处于竖屏,我们突然旋转屏幕,由于系统配置发生了改变,在默认情况下,Activity会被销毁并且重新创建,当然我们也可以阻止系统重新创建我们的Activity。
回答:在onStop之前系统会调用onSaveInstanceState来保存当前Activity的状态(正常情况下是调用这个方法),和onPause没有既定的时序关系,Activity会被销毁,其onPause,onStop,onDestroy均会被调用,可以通过onRestoreInstanceState和onCreate方法来判断Activity是否被重建。如果被重建了,我们就取出之前的数据恢复,从时序上来说,onRestoreInstanceState的调用时机应该在onStart之后。
恢复的内容:系统会默认我们保存当前的Activity视图架构,并且为我们恢复这些数据,比如文本框中用户输入的数据,ListView滚动的位置,这些View相关的状态系统都会默认恢复。保存的流程:Activity委托Window,Window委托顶级容器、顶级容器是一个ViewGroup器,再去一一通知他的子元素来保存数据、上层委托下层,父容器委托子容器,去处理一件事件。
TextView源码:TextView源码的onSaveInstanceState和onRestoreInstanceState中保存了保存了TextView自己的文本选中和文本内容等。
举例:验证数据存储和恢复的情况。onRestoreInstanceState一旦被调用,其参数Bundl的savedInstanceState一定有值,我们不用额外的判断是否为空但是onCreate不行,onCreate如果正常启动的话,其参数Bundler onSaveInstanceState为null,所以需要一些额外的判断,这两个方法我们选择任意一个都是可以进行数据恢复的,但是建议我们使用onRestoreInstanceState去恢复数据。系统只在Activity异常终止的情况下才会调用onSaveInstanceState和onRestoreInstanceState来存储和恢复数据,其他情况不会触发。
public class MainActivity extends AppCompatActivity {
private static String TAG = "MainActivity";
private EditText editText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
editText = (EditText) findViewById(R.id.edt_test);
if(savedInstanceState!=null){
String test = savedInstanceState.getString("extra_test");
Log.d(TAG, "[OnCreate]Restore onCreate: "+test);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Log.d(TAG, "onSaveInstanceState: ");
outState.putString("extra_test","testing");
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String test = savedInstanceState.getString("extra_test");
Log.d(TAG, "[onRestoreInstanceState]Restore extra_test "+test);
}
}
(2)情况2:资源内存不足导致低优先级的Activity被杀死
存储和恢复与情形一一致,Activity按照优先级的从高往低,可以分为三种:
1. 前台Activity : 正在和用户交互的Activity,优先级最高
2. 可见但非前台Activity : 比如对话框,导致Activity可见但是位于后台无法和用户直接交互
3. 后台Activity : 已经被暂停的Activity,比如执行了onStop,优先级最低
当系统内存不足时,系统会按照优先级去杀死Activity所在的进程。如果一个进程中没有在四大组件在执行,那么这个进程将很快被系统杀死。
如何保活?
将后台工作放在Service中从而保证了进程有一定的优先级,这样就不会轻易的被杀死。
系统配置发生改变后,Activity会被重新创建,那我们有没有什么办法不重新创建呢?
系统配置中有很多内容,如果当某项内容发生改变后,我们不想屏幕旋转时候系统重新创建,就可以给activity的configChangs属性加上orientation这个值。其他属性还有:
android:configChanges="orientation|screenSize"
如果我们没有在 Activity的configChanges属性中指定该选项的话,当配置发生改变后就会导致Activity重新创建。只需要在 AndroidMenifest.xml中加入Activity声明即可。
常用的只有
- local 切换系统语言
- orientation 方向
- keyboardHidden 切除键盘和隐藏键盘
1.2 Activity的启动模式
形形色色的启动模式和标志位实在是太容易被混淆了。
1.2.1 Activity的LaunchMode
通过在AndroidMAnifest.xml中
四种启动模式:
-
standard:
标准模式,每次启动一个Activity都会重新创建一个实例。采用栈的方式进行存储。 -
singleTop:
栈顶复用模式,在这个模式下,如果新的Activity已经位于任务栈的栈顶,那么此Activity不会被重新创建。onNewIntent方法会被回调,通过此方法的参数,我们可以取出当前请求的信息。这个Activity的onCreate,onStart不会被系统调用。 -
singleTask:
栈内复用模式,这是一种单实例模式,在这种模式下,只要Activity在一个栈内存在,那么多次启动此Activity都不会创建实例。
三个例子:
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切换到栈顶并且调用其oNnNewIntent方法,同时由于singleTask默认具有clearTop的效果,会导致栈内所有在D上面的Activity全部出栈,于是最终S1中的情况为AD。 -
singleInstance
单实例模式,处理其他程序与我们程序共享这个活动的实例。这是一种加强的singleTask的模式,他除了具有singleTask的所有属性之外,还加强了一点,那就是具有此模式下的Activity只能单独的处于一个任务栈中,有些像Java中的static。eg:有两个任务栈,前台任务栈的情况为AB,而后台任务栈的情况是CD,这里假设CD的启动模式都是singleTask,现在请求启动D,那么整个后台任务站都会被切换到前台,这个时候整个后退列表变成了ABCD,当用户按back键的时候,列表中的Activity会一一出栈,如左图所示。
什么是Activity所需的任务栈?TaskAffinity翻译成任务相关性,TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配合使用,第一种情况:TaskAffinity是具有该模式Activity目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。第二种情况:现在有2个应用A和B,A启动了B的一个Activity C ,然后按Home键回到桌面,然后再单击B的桌面图标,这个时候并不是启动; B的主Activity,而是重新显示了已经被应用A启动的Activity C,或者说,C从A的任务栈转移到了B的任务栈中。
如何指定Activity的启动模式?
1)AndroidManifest为Activity指定启动模式:
<activity android:name=".MainActivity"
android:launchMode="singleTask"
android:label="@string/app_name">
2)Intent中设置标志位来为Activity指定启动模式。
Intent intent = new Intent(MainActivity.this,Main2Activity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
总结:第二种优先级高于第一级;
上述的方式在能力限定范围有所不同。
-
第一种无法直接设定Intent.FLAG_ACTIVITY_CLEAR_TOP标识;
-
第二种无法指定singleInstance模式。
-
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-
1.2.2 Activity的Flags
标志位包括:
FLAG_ ACTIVITY_ NEW _ TASK singleTask启动模式
FLAG_ ACTIVITY_ SINGLE _ TOP singleTop启动模式
FLAG_ ACTIVITY_ CLEAR _ TOP 如果被启动的Activity采用standard模式启动,它和它之上的Activity都要出栈,singleTask具有此属性。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 这个标记的Activity不会出现在历史的Activity的列表中,当某些情况我们不希望用户通过历史列表回到我们的Activity的时候这个标记比较有用。
1.3 IntentFilter的匹配规则
Activity的启动分为两种
- 显示调用
- 明确对象的组件信息,包名类名
- 隐式调用
- 不需要明确信息,隐式调用需要intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果不匹配将无法启动目标Activity,IntentFilter中的过滤信息action,category,data。
- 为了匹配过滤列表,需要同时匹配过滤列表中的action,category,data信息,否则匹配失败,一个过滤列表中的action,category,data可以有多个,必须完全匹配action,category,data;
- Activity可以有多个intent-filter,一个intent只要能匹配一组intent-filter即可成功启动Activity。
- 不需要明确信息,隐式调用需要intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果不匹配将无法启动目标Activity,IntentFilter中的过滤信息action,category,data。
<intent-filter>
<action android:name="com.liuguilin.activitysample.c" />
<action android:name="com.liuguilin.activitysample.d" />
<category android:name="com.liuguilin.category.c" />
<category android:name="com.liuguilin.category.d" />
<data android:mimeType="text/plain" />
</intent-filter>
完全与之相匹配的是:
Intent intent = new Intent(“com.liuguilin.activitysample.c”);
intent.addCategory("com.liuguilin.category.c");
intent.setDataAndType(Uri.parse("file//abc"),"text/plain");
startActivity(intent);
1. action的匹配规则
Intent中必须有一个action且必须能够和过滤规则中的某个action相同,即 "com.liuguilin.activitysample.c"或"com.liuguilin.activitysample.d"中的一个相同即可成功匹配。区分大小写。有系统预定义了一些action,同时我们也可以在应用中定义自己的action。
2. category的匹配规则
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。
3. data的匹配规则
数据格式:
data由两部分组成
-
mimeType
- 媒体类型,比如image/jpeg,audio/mpeg4-generic等,可以表示图片,文本,视频等不同的媒体格式。
-
URI而URI包含的数据可就多了,下面的URI的结构:
<scheme>://<host>"<port>/[<path>|<pathPrefix>|<pathPattern>]
Scheme:URI的模式,比如http、file、content等;
Host:URI的主机,比如www.google.com;
Port:URI中的端口号;
Path、pathPattem 和 pathPrefix:这三个参数表述路径信息。
content://com.liuguilin.project:200/folder/subfolder/etc http://www.baidu.com:80/search/info
1. 过滤规则及匹配:
过滤规则1:<intent-filter> <data android:mimeType="image/*"/> </intent-filter>
指定了媒体类型为所有类型的图片,Intent中的mimeType属性必须为“image/*”才能匹配,虽然没有指定URL,但是有默认值,URL的默认值为content和file。
匹配规则:
intent.setDataAndType(Uri.parse("file://abc"),"image/*");
原因:
URI的默认值为content和file,虽然没有指定URI,但是Intent中的URI部分的scheme必须为content或者file才能匹配。
2. 过滤规则及匹配:
过滤规则2:<intent-filter> <data android:mimeType="video/mpeg" android:scheme="http" .../> <data android:mimeType="audio/mpeg" android:scheme="http" .../></intent-filter>
匹配规则:
intent.setDataAndType(Uri.parse("http://abc"),"video/png")intent.setDataAndType(Uri.parse("http://abc"),"audio/png");
这两种特殊写法,作用是一样的
<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>
Intent-filter 的匹配规则对于Serviece和BroadcastReceiver也是同样道理,不过系统对于Service的建议Service尽量显式调用。
判断Activity能否匹配我们的隐式Intent
PackageManager提供的resolveActivity方法,或者Intent的resolveActivity方法,如果它们找不到匹配的Activity就会返回Null。否则会报错。PackageManager还提供了queryIntentActivities方法,这个方法和resolveActivity不同的是,它返回所有匹配成功的Activity信息。
Activity入口的Intent:<action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" />