第一章主要讲了以下几个知识点:
- Activity生命周期
- Activity的启动模式
- IntentFilter匹配规则
主要参考Android开发艺术探索外,另外还有下面这些博客:
- onSaveInstanceState和onRestoreInstanceState详解
- Activity启动模式中SingleTop,不仅仅会调用onNewIntent()
- Android启动模式之singleinstance的坑
- Android任务和返回栈完全解析,细数那些你所不知道的细节
- android:allowTaskReparenting实例
- Android Flags:FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS学习
- TaskRecord分析
一. Activity的生命周期全面分析
从典型情况和异常情况来分别分析生命周期,典型情况是指有用户参与的情况下,异常情况是指Activity被系统回收或由于当前设备Configuration发生改变导致Activity被销毁重建。
(1) 典型情况下的生命周期
1. 生命周期的回调方法
正常情况,会经历如下回调方法对应的生命周期:
- onCreate:表示Activity正在被创建,会做一些初始化工作,比如setContentView、初始化数据。
- onRestart:表示Activity重新启动,一般当前Activity从不可见变为可见状态时会被调用。
- onStart:表示Activity正在被启动,这是Activity是可见的,但是没有出现在前台,无法交互。
- onResume:表示Activity可见,显示在前台并开始活动,可交互。
- onPause:表示Activity正在停止,紧接着onStop会被调用,如果立刻返回当前Activity,那么onResume会被调用(情景几乎不可能)。可以做数据存储、停止动画。不能做耗时操作,因为onPause执行完,新的Activity才能onResume,会影响新的Activity的显示。
- onStop:表示Activity即将停止,可做轻微重量级的回收工作,但同样不能太耗时。
- onDestroy:表示Activity即将被销毁,做回收工作和最终的资源释放。
2. 对应点
- onCreate-onDestroy对应,Activity的创建与销毁,一个生命周期只会调用一次。
- onStart-onStop对应,Activity的可见与不可见,一个生命周期可能会调用多次。
- onResume-onPause对应,Activity是否在前台,一个生命周期可能会调用多次。
3. 特定情境
- Activity第一次启动:onCreate->onStart->onResume
- 打开新的Activity或切换到桌面:onPause->onStop
- 如果打开的是透明主题的Activity:onPause
- 用户返回原来Activity:onRestart->onStart->onResume
- 用户按BACK键返回:onPause->onStop->onDestroy
- Activity被系统回收后再次打开和第一次启动的生命周期一样
4. 细节点
- onStart和onStop是从是否可见的角度来回调的,onResume和onPause是从是否在前台的角度来回调的,除此无别的区别。
- 从一个Activity打开新的Activity是旧Activity先回调onPause再新Activiy回调onResume。为了使新Activity尽快切换到前台,我们尽量在onStop中做工作而不是onPause。
(2) 异常情况下的生命周期
当资源相关系统配置发生改变以及系统内存不足时,Activity可能被杀死,在分析这两种情况之前,先来看期间的两个很重要的方法:onSaveInstanceState()和onRestoreInstanceState()。
1. 数据存储恢复机制:onSaveInstanceState()、onRestoreInstanceState()
简单流程
Activity在异常情况下终止时会调用onSaveInstanceState()来保存当前Activity的状态。当Activity被重建后,Activity的onRestoreInstanceState()会被调用,将销毁时onSaveInstanceState()所保存的Bundle对象作为参数同时传递给onRestoreInstanceState()和onCreate()方法。
onSaveInstanceState()的调用
在当前Activity即将被销毁并有机会重新显示的时候,onSaveInstanceState()会被调用。
- 比如以下情景会调用:
- 按下Home键时,此时系统有可能会因为内存不足回收Activity,但是很有可能用户会重新打开此Activity,所以此时会调用。
- 切换到别的程序时,理由同上。
- 按下电源键,理由同上。
- 启动新的Activity,理由同上。
- 设配配置发生改变,比如竖屏切换到横屏,Activity会销毁重建。
- 以下场景是人为确认想销毁Activity的情况是不会调用的:
- 按Back键。
- 调用了finish()。
- onSaveInstanceState()与Activity生命周期回调的时序关系
- 在onStop()之前。
- 和onPause()顺序不定。
onSaveInstanceState()虽然说是保持数据的方法,但是如果想保存额外数据、重写其方法,要慎重选择是否在此方法中保存。onSaveInstanceState()不一定会被调用,因此不适合在该方法中保存持久化数据,保存持久化数据的操作应该放在onPause()中,同时应该注意数据量大的话应该另开线程。
onRestoreInstanceState()的调用
Activity真正被销毁后重建时才会被调用。如果按Home键后在没有销毁前立刻返回Activity,此时onRestoreInstanceState()是不会被调用的。
- onSaveInstanceState()和onRestoreInstanceState()并不是成对出现。
- onCreate()和onRestoreInstanceState()虽然都有Bundle参数,但是前者有可能为空,而后者一定不为空,因为后者只有销毁重建时才会调用,一定保存过Bundle。
- onRestoreInstanceState()不一定会被调用,
- onRestoreInstanceState()在onStart()之后调用。
系统自动保存恢复的是什么内容?
系统会默认为我们保存Activity的视图结构,并在重建后恢复这些数据,比如EditText中输入的文本、ListView滚动到的位置,具体保存的内容需要看具体View的onSaveInstanceState()和onRestoreInstanceState()实现。
保存恢复View的具体流程
Activity被以外终止时,Activity会调用onSaveInstanceState(),然后Activity委托Window去保存数据,Window委托顶级容器(通常是DecorView),之后顶级容器再去分发子元素保存数据。恢复数据的过程也是类似,典型的委托思想,上层委托下层。
2. 资源相关的系统配置发生改变导致Activity被杀死并重新建立
当应用程序启动时,系统会根据当前设备的情况去加载适合的资源,如果当前Acticity处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,在默认情况下,Acticity会被销毁并重建,会调onSaveInstanceState()和onRestoreInstanceState()。
3. 资源内存不足导致低优先级的Activity被杀死
当系统内存不足时就会按照优先级杀死Activity所在的进程,并调onSaveInstanceState()和onRestoreInstanceState()保存并恢复数据。
Activity优先级分为三种:
- 前台Activity:正在和用户交互的Activity,优先级最高。
- 可见非前台Activity:可见但无法交互。
- 后台Activity:不可见,已被暂停,比如执行了onStop(),优先级最低。
如果一个进程中没有四大组件在执行,那么这个进程很快会被系统杀死,因此后台工作不应该脱离四大组件,好方法是将后台工作放在Service中从而保证一定优先级。
4. 设置系统配置发生改变时Activity不重建
给Activity指定configChanges属性可以让Activiy不重新创建,在Menifest.xml中加入Activity声明即可,如下,configChanges指定了"orientation|screenSize",即旋转屏幕时Activity不会重启。
minSdkVersion和targetSdkVersion有大于13时旋转屏幕时screenSize也会改变,所以还加上了screenSize。设置之后会发现Activity没有重启,并且onSaveInstanceState()和onRestoreInstanceState()也没有调用,调用了onConfigurationChanged()。
二. Activity启动模式
(1) Activity的LaunchMode
1. 四种LaunchMode
standard
standard是系统的默认模式,有以下特点:
- 每次启动一个Activity都会重新创建一个Activity实例,不管这个实例是否已经存在。
- 生命周期符合典型情况下Activity的生命周期。
- 一个任务栈可以有多个实例。
- 每个实例可以属于不同的任务栈。
- 运行在启动它的Activity所属的任务栈中。
如果我们用ApplicationContext去启动一个standard模式的Activity会报错。因为非Activity类型的context是没有任务栈的。可以通过给Activity增加FLAG_ACTIVITY_NEW_TASK标记位来解决这个问题,这样在Activity启动时会为它创建一个新的任务栈,实际上相当于以singleTask模式来启动的。
想到一个问题,给standard模式一定运行在启动它的Activity所属的任务栈中吗?给standard的Activity指定taskAffinity会有效果吗?
adb shell dumpsys activity
查看栈的情况如下:
Running activities (most recent first):
TaskRecord{289ac57 #14185 A=com.example.test U=0 StackId=1 sz=3}
Run #16: ActivityRecord{61ba6d u0 com.example.test/com.utte.test.ThirdActivity t14185}
Run #15: ActivityRecord{b5cf6f9 u0 com.example.test/com.utte.test.SecondActivity t14185}
Run #14: ActivityRecord{191f0de u0 com.example.test/com.utte.test.MainActivity t14185}
所有的Activity都在com.example.test栈里面,taskAffinity没有生效。给standard的Activity指定taskAffinity没有效果。
但是还是不能证明给standard模式一定运行在启动它的Activity所属的任务栈中。因为到后面如果就会知道,如果启动它的Activity是singleInstance的,就不是了。
singleTop
栈顶复用模式,有以下特点:
- 如果此Activity实例位于栈顶时,不会再创建一个新的实例。
- 此Activity的实例存在但是不在栈顶,会重新创建新实例。
- 第一种情况时,栈顶实例的onCreate()、onStart()不会重新调用,onNewIntent方法会被调用。
onCreate -> onStart -> onResume -> intent此Activity -> onPause -> onNewIntent -> onResume
D/SingleTopActivity: onCreate: com.utte.test.SingleTopActivity@ea58a12
D/SingleTopActivity: onStart: com.utte.test.SingleTopActivity@ea58a12
D/SingleTopActivity: onResume: com.utte.test.SingleTopActivity@ea58a12
D/AppTracker: App Event: start
D/OpenGLRenderer: endAllActiveAnimators on 0x7f853a3400 (RippleDrawable) with handle 0x7f849c8140
D/SingleTopActivity: onPause: com.utte.test.SingleTopActivity@ea58a12
D/SingleTopActivity: onNewIntent: com.utte.test.SingleTopActivity@ea58a12
D/SingleTopActivity: onResume: com.utte.test.SingleTopActivity@ea58a12
看完之后我又想到几个问题,书上只说了如果了Activity位于栈顶的情况下不会重新创建实例,那么这里说的栈顶是仅限于前台任务栈的栈顶还是所有任务栈的栈顶都考虑呢,如果是考虑所有任务栈的栈顶,那么前台后台任务栈是否会切换?具体是如何表现的呢?
做这样一个实验:
三个Activity,MainActivity启动SecondActivity,SecondActivity启动ThirdActivity,最后ThirdActivity再启动SecondActivity。
TaskRecord{cf38 #14208 A=jtt.task U=0 StackId=1 sz=2}
Run #19: ActivityRecord{795dbe2 u0 com.example.test/com.utte.test.SecondActivity t14208}
Run #18: ActivityRecord{6e3fc27 u0 com.example.test/com.utte.test.ThirdActivity t14208}
TaskRecord{ccb7b11 #14207 A=com.example.test U=0 StackId=1 sz=2}
Run #17: ActivityRecord{b37b5c2 u0 com.example.test/com.utte.test.SecondActivity t14207}
Run #16: ActivityRecord{dc11075 u0 com.example.test/com.utte.test.MainActivity t14207}
很显然SecondActivity重新创建了,所以判断不重建的栈顶存在是指的当前前台任务栈的栈顶。
再做一个实验看看singleTop和taskAffinity结合会有什么效果:
同样的启动顺序。
先来分析一下,MainActivity启动SecondActvity之后,包名栈中有两个Activty,SecondActivity处于栈顶。此时启动ThirdActivity,ThirdActivity生成并入了一个新栈utte.task,再启动SecondActivity。但是这只是推测...
一步一步看任务栈。
- 启动
TaskRecord{ea35dba #14195 A=com.example.test U=0 StackId=1 sz=1}
Run #17: ActivityRecord{877ef9f u0 com.example.test/com.utte.test.MainActivity t14195}
- 启动SecondActivity
TaskRecord{2ec663b #14196 A=jtt1.task U=0 StackId=1 sz=1}
Run #17: ActivityRecord{4c3859d u0 com.example.test/com.utte.test.SecondActivity t14196}
TaskRecord{ea35dba #14195 A=com.example.test U=0 StackId=1 sz=1}
Run #16: ActivityRecord{877ef9f u0 com.example.test/com.utte.test.MainActivity t14195}
- 启动ThirdActivity
这就很神奇了,我的jtt2.task呢...ThirdActivity居然是在jtt1.task里面的。
TaskRecord{2ec663b #14196 A=jtt1.task U=0 StackId=1 sz=2}
Run #18: ActivityRecord{9c01f5 u0 com.example.test/com.utte.test.ThirdActivity t14196}
Run #17: ActivityRecord{4c3859d u0 com.example.test/com.utte.test.SecondActivity t14196}
TaskRecord{ea35dba #14195 A=com.example.test U=0 StackId=1 sz=1}
Run #16: ActivityRecord{877ef9f u0 com.example.test/com.utte.test.MainActivity t14195}
- 再次启动SecondActivity
结果就能猜到了。
TaskRecord{2ec663b #14196 A=jtt1.task U=0 StackId=1 sz=3}
Run #19: ActivityRecord{a14f24a u0 com.example.test/com.utte.test.SecondActivity t14196}
Run #18: ActivityRecord{9c01f5 u0 com.example.test/com.utte.test.ThirdActivity t14196}
Run #17: ActivityRecord{4c3859d u0 com.example.test/com.utte.test.SecondActivity t14196}
TaskRecord{ea35dba #14195 A=com.example.test U=0 StackId=1 sz=1}
Run #16: ActivityRecord{877ef9f u0 com.example.test/com.utte.test.MainActivity t14195}
难道singleTop启动singleTop时taskAffinity会失效???试了几个还真是这样,taskAffinity和singleTop结合使用时不一定有效。
singleTask
栈内复用模式,有以下特点:
- 栈内存在此Activity时,再次启动,不会重新创建实例。
- 生命周期和singleTop一样启动存在的Activity时会调用onNewIntent()。
- 如果Activity的实例不在栈顶,再次启动此Acivity时会使上面的所有Activity出栈。
- 如果所需任务栈不存在,则会创建任务栈,并把自己压栈。
具体流程如下:
singleInstance
单实例模式,是singleTask模式的增强,具有singleTask模式的所有特性。除此之外还有一个特点就是此模式的Activity只能单独的位于一个任务栈中。
- Activity实例不存在则创建一个新任务栈和实例,将实例压栈。
- 后续再创建此Activity时则不会重新创建,效果同singleTask。
- 一个小例子:
如果MainActivity启动SecondActivity,SecondActivity启动ThirdActivity。一直按back的显示的顺序应该是ThirdActivity-MainActivty-SecondActivity。
- 另一个小例子:
还是上面的Activity,MainActivty启动SecondActivity,这时home键回到桌面,再点击App图标,发现显示的是MainActivity。这是因为当重新启动的时候,系统会先去找主栈里的activity,也就是APP中LAUNCHER的activity所处在的栈,查看是否有存在的activity,没有的话则会重新启动LAUNCHER。
2. 任务和任务栈
任务是一个Activity的集合,它使用栈的方式来管理其中的Activity,这个栈就是返回栈,也叫任务栈。栈中Activity的顺序就是按照它们被打开的顺序依次存放的。
- 用户在home上点击了一个应用的图标时,此应用的任务会被转移到前台。如果这个应用并没有任何一个任务(应用最近没有被启动过),系统会去创建一个新的任务,并且将该应用的主Activity放入到任务栈中。
- 当一个Activity启动另外一个Activity时,新的Activity会被放置到任务栈栈顶并将获得焦点。前一个Activity仍然保留在任务栈当中,但会处于停止状态。
- 当用户按下Back键时,栈顶Activity会被移除掉,前一个Activity则会得重新回到最顶端的位置。
- 如果用户一直地按Back键,这样任务栈中的Activity会一个个地被移除,直到最终返回到主屏幕。当返回栈中所有的Activity都被移除时,对应的任务也会被系统回收。
- 当用户开启了一个新的任务,或者点击Home键回到主屏幕的时候,之前任务会被转移到后台。当任务处于后台状态的时候,返回栈中所有的Activity进入停止状态,栈中的顺序会原封不动地保留着。
- 如果任务切换到后台之后过了很长一段时间,系统会清除这个任务中除了最底层的那个Activity之外的其它所有Activity。当用户重新回到这个任务时,最底层的那个Activity将得到恢复。这个是系统默认的行为。
activity标签的几个属性可以改变上述系统默认行为
- alwaysRetainTaskState:
如果将最底层的那个Activity的这个属性设置为true,那么上面所描述的默认行为就将不会发生,任务中所有的Activity即使过了很长一段时间之后仍然会被继续保留。 - clearTaskOnLaunch:
如果将最底层的那个Activity的这个属性设置为true,那么只要用户离开了当前任务,再次返回的时候就会将最底层Activity之上的所有其它Activity全部清除掉。简单来讲,就是一种和alwaysRetainTaskState完全相反的工作模式,它保证每次返回任务的时候都会是一种初始化状态,即使用户仅仅离开了很短的一段时间。 - finishOnTaskLaunch:
这个属性和clearTaskOnLaunch是比较类似的,不过它不是作用于整个任务上的,而是作用于单个Activity上。如果某个Activity将这个属性设置成true,那么用户一旦离开了当前任务,再次返回时这个Activity就会被清除掉。
3. TaskAffinity
这个参数标识了一个Activity所需要的任务栈的名字,可以翻译成任务相关性。为Activity单独指定TaskAffinity可以指定此Activity的任务栈,在不指定TaskAffinity的默认情况下,Activity所需的任务栈的名字为App的包名。
TaskAffinity属性主要和singleTask启动模式或者allowTaskReparenting属性配对使用。自己觉得最好不要和其它启动模式配对使用,因为上面的测试证明了会发生奇怪的情况。
TaskAffinity和singleTask
TaskAffinity和singleTask启动模式配对使用是,用来指定此模式Activity运行的任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中。
TaskAffinity和allowTaskReparenting
TaskAffinity和allowTaskReparenting结合时,当应用A启动了应用B的一个ActivityC,如果C的allowTaskReparenting为true,那么此时按home键返回桌面,再打开应用程序B,此时显示的并不是应用程序B,而是重新显示了刚刚被A启动的ActivityC,C从A的任务栈转到了B的任务栈。
allowTaskReparenting主要作用是Activity的迁移,从与taskAffinity属性不同的任务栈迁移到与其相同名称的任务栈,且只有在reset task时才会生效,从桌面启动App会带
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
,在reset task时会根据allowTaskReparenting的值迁移Activity。
4. 如何给Activity指定启动模式
- 在AndroidMenifest中指定
- 通过Intent中设置标志位来指定
Intent intent = new Intent(MainActivity.this, SingleTaskActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
区别:
- intent设置比Menifest设置的优先级要高。
- 功能范围稍有差异。比如Menifest中无法设置FLAG_ACTIVITY_CLEAR_TOP标识,intent无法指定singleInstance模式。
(2) Activity的Flags
Activity的标记位很多,有的可以设定Activity的启动模式,有的可以影响Activity的运行状态,有的标记位是系统内部使用的,应用程序不应该去手动设置,以防出现问题。
下面是一些常用的标记位:
FLAG_ACTIVITY_SINGLE_TOP
相当于指定singleTop模式,效果与在menifest中指定是相同的。
FLAG_ACTIVITY_NEW_TASK
相当于指定singleTask模式,效果与在menifest中指定是相同的。
FLAG_ACTIVITY_CLEAR_TOP
会使同一任务栈中它上面的Activity全部出栈。
- singleTask默认具有此标记的效果,如果实例已经存在,那么它之上的Activity全部出栈后系统会调用它的onNewIntent()。
- 和standard搭配时,它连同它之上的所有Activity都要出栈,之后系统重新创建新实例并放入栈中。
FLAG_ACTIVITY_EXCLUDE_FROM_RECENT
等同于在xml中指定android:excludeFromRecents="true"
,具有此标记的Activity不会出现在历史Activity的列表中。看到一篇博客说的是具有此标记的Activity所在的任务栈不会出现在任务管理器的列表中。
那么问题又来了,书上说的历史Activity列表就是管理器列表吗?管理器列表显示的真的是任务栈吗?
三. IntentFilter的匹配规则
启动Activity的方式分为两种,显示调用和隐式调用。显示调用需要指定启动对象的组件信息,包括包名和类名。隐式调用需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果匹配失败则无法启动。
intent-filter匹配规则
- intent-filter中有action、category、data,需要同时匹配列表中的这个三个类别的信息才能够匹配成功。
- 一个Activity中可以有多个intent-filter,一个intent只需要匹配成功一个intent-filter就可以成功启动对应Activity。
action匹配规则
action是一个字符串,系统预定义了一些action,同时我们自己的应用里也可以定义自己的action。
- intent中必须有action。
- 匹配是指字符串完全相同,区分大小写。
- 一个过滤中可以有多个action,intent能够与任意一个action相同则匹配成功。
- 如果过滤中有acrion,intent中action不存在的时,过滤失败。
匹配action,总结来说action必须存在且必须和过滤中的其中一个action相同。
category匹配规则
category是一个字符串,系统预定义了一些category,同时我们自己的应用里也可以定义自己的category。
- 系统在调用startActivity()或startActivityForResult()时会自动为Intent加上
android.intent.category.DEFAULT
。 - 只有加了
android.intent.category.DEFAULT
的Activity才能接受隐式调用。 - 如果Intent中有category,那么不管有几个,每个category都必须是intent-filter中定义的。
- 如果Intent中没有category,那么只要intent-filter中有default category,就匹配成功。
data匹配规则
data格式
data由两部分组成,mimeType和URI。mimeType是媒体类型,比如image/jpeg、audio/mpeg4-generic和video/*,表示不同的媒体格式。URI的结构如下:
://:/[||]
比如:
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
- scheme:http、file、content等,必须指定,否则整个URI无效。
- host:主机名,必须指定,否则无效。
- port:端口号,非必要。
- path:完整路径。
- pathPrefix:路径前缀信息。
- pathPattern:也是完整路径,可以包含通配符“*”,表示0个或多个任意字符。
匹配规则:
- filter中有data时,intent中必须有data。
- intent中的data必须至少与intent-filter中的某一个data完全相同才匹配成功。
- intent-filter没有指定URI时,scheme默认值为file和content。
为intent指定data时不能先用setData()再用setType,两者会清除对方的值。应该使用setDataAndType()。
启动前判断
如果找不到能够匹配成功的Activity就会报错,所以可以在隐式调用之前做一个判断。
PackageManager的resolveActivity()
如果找不到匹配成功的Activity就会返回null,否则返回最佳匹配的Activity信息。Intent的resolveActivity()
如果找不到匹配成功的Activity就会返回null,否则返回最佳匹配的Activity信息。PackageManager的queryIntentActivites()
与上面两个方法不同的是,如果存在能成功匹配的Activity就返回所有匹配成功的Activity信息。
public abstract ResolveInfo resolveActivity(Intent intent, @ResolveInfoFlags int flags)
public ComponentName resolveActivity(@NonNull PackageManager pm)
public abstract List queryIntentActivities(Intent intent, int flags)
flags一般填MATCH_DEFAULT_ONLY,表示只匹配intent-filter中声明default category的Activity。