Android Activity
Activity
基本概念:Activity(活动)是Android四大组件之一,用于显示用户界面,和用户进行交互。
生命周期:
1 | Activity 的状态: |
正常情况下的生命周期
指在有用户参与的情况下,Activity 所经过的生命周期的改变。
onCreate()->onStart()->onResume()->onPause()->onStop()->onDestory()
方法 | 解释 |
---|---|
onCreate() | 正在被创建。当 activity 启动时,做一些必要的初始化工作。 |
onStart() | 正在被启动,即将开始。状态为可见,但位于后台。 |
onResume() | 进入运行状态,与用户进行交互。并状态为可见,并位于前台。 |
onPause() | 表示 activity 正在停止,此时可以做一些存储数据,停止动画等工作,但不能太耗时,因为这会影响到新 activity 的显示,onPause 必须先执行完,新的 activity 的 onResume 才会执行。状态为可见,但位于后台。 |
onStop() | 即将停止。此时应该释放不再需要的资源。状态为不可见,并位于后台。 |
onRestart() | 表示 activity 正在重新启动 ,一般情况下,当前 activity 从完全不可见重新变成可见状态时,会被调用,这种情形一般是用户行为所导致的,比如用户按 HOME 键切换到桌面然后重新打开 APP 或者按 back 键返回此 activity。 |
onDestory() | 即将被销毁。可做最后的回收和资源释放的工作,避免内存泄漏。 |
流程:
- 启动 activity:onCreate()->onStart()->onResume(),activity进入运行状态。
- activity被其他activity覆盖(DialogActivity,但未完全覆盖),系统会调用onPause()方法,暂停activity的运行。(如果新 Activity 采用透明主题,则不会回调 onStop)
- 当activity由被覆盖状态回到前台,系统会调用onResume()方法,activity再次进入运行状态。
- 当activity跳转到新的activity界面并完全覆盖此activity,或按home键回到主屏,自身退居后台:则执行onPause()->onStop(),进入停滞状态。
- 当activity由后台回到前台,再次回到原 Activity:则执行onRestart()->onStart()->onResume(),重新进入运行状态。(注意只是生命周期方法一样,不代表所有过程都一样)
- 当activity已被系统杀死,而后用户回退到此activity界面:则重新执行onCreate()->onStart()->onResume(),进入运行状态。
- 按 back 键回退时:则执行onPause()->onStop()->onDestory(),结束当前activity运行状态。
注:
- 锁屏:onPause()->onStop(),解锁屏幕:onStart()->onResume()。
- 旧activity先执行onPause()方法,然后新activity再执行onResume()方法。
- 关于activity是否执行onPause()方法,只有启动dialog风格的activity时才会调用。当是dialog弹框时(AlertDialog),并不会执行onPause()方法。
- activity从是否可见来讲,onStart()和onStop()是相对的。从是否在前台来讲,onResume()和onPause()是相对的。也就是说,onStart()可见但位于后台,onResume()可见并位于前台,onPause()可见但位于后台,onStop()不可见并位于后台。
Activity 的启动过程:
- 启动 Activity 的请求会由 Instrumentation 来处理
- 然后它通过 Binder 向 AMS(ActivityManagerService) 发请求
- AMS 内部维护着一个 ActivityStack 并负责栈内的 Activity 的状态同步
- AMS 通过 ActivityThread 去同步 Activity 的状态从而完成生命周期方法的调用
旧 Activity 先执行 onPause 方法,新 Activity 再执行 onResume 方法。
在 ActivityStack 中的 resumeTopActivityInnerLocked 方法中,栈顶的 Activity 需先 onPause 后,新 Activity 才能启动。
最终,在 ActivityStackSupervisor 中的 realStartActivityLocked 方法会调用 app.thread.scheduleLaunchActivity()。
app.thread 的类型是 IApplication(它的具体实现是 ActivityThread 中的 ApplicationThread),所以,实际上调到了 ApplicationThread 的 scheduleLaunchActivity 方法,这个方法会调用 handleLaunchActivity 方法完成新 Activity 的启动三回调。
异常情况下的生命周期
指 Activity 被系统回收或者由于当前设备的 Configuration 发生改变从而导致 Activity 被销毁重建。
Configuration 类专门用来描述 Android 设备的配置情况:
1 | // 获取实例对象 |
情况 1:资源相关的系统配置发生改变导致 Activity 被杀死并重新创建
当系统资源配置发生改变时,比如切换横竖屏,则Activity 会被销毁(默认情况下)并重新创建,同时由于是在异常情况下终止的,系统会调用 onSaveInstanceState(调用时机在 onStop 之前,和 onPause 没有既定的时序关系) 来保存当前 Activity 的状态。
当 Activity 被重新创建后,系统会调用 onRestoreInstanceState(调用时机在 onStart 之后),并把 Activity 销毁时 onSaveInstanceState 方法所保存的 Bundle 对象作为参数同时传递给 onRestoreInstanceState(调用就是有值的) 和 onCreate(注意 Bundle 的非空判断)方法。
并且,在这两个方法中,系统会自动做一定的恢复工作。和 Activity 一样,每个 View 都有这两个方法。
注意,系统只在 Activity 异常终止时才会调用它们,其他情况不会触发这个过程。但是,按 Home 键或者启动新 Activity 仍然会单独触发 onSaveInstanceState 的调用。
关于保存和恢复 View 层次结构,系统的工作流程:
一种典型的委托思想,上层委托下层,父容器委托子元素。
数据的恢复过程也是类似的。
- 当 Activity 被意外终止时,它会调用 onSaveInstanceState 保存数据。
- 然后 Activity 会委托 Window 保存数据。
- Window 再委托它上面的顶级容器保存数据。
- 顶级容器是一个 ViewGroup,一般来说可能是 DecorView。
- 最后顶级容器再去一一通知它的子元素来保存数据。
当系统配置发生改变后,如果不想重新创建 Activity:
可在 AndroidMenifest.xml 中,给 Activity 指定 configChanges 属性。比如 android : configChanges = “orientation|vertical”。常用的值有:
- locale。设备本地位置发生改变,一般指切换了系统语言。
- orientation。屏幕方向发生改变,比如旋转了屏幕。
当 minSdkVersion 和 targetSdkVersion 有一个不低于 13。为了防止旋转屏幕时 Activity 重启,还要加上 screenSize。 - keyboardHidden。键盘的可访问性发生改变,比如用户调出了键盘。
1 | /** |
情况 2:资源内存不足导致低优先级的 Activity 被杀死
其数据存储和恢复过程和情况 1 完全一致。
Activity 的优先级,从高到低:
- 前台 Activity。正在和用户交互的 Activity。
- 可见但非前台 Activity。比如 activity 中弹出了一个对话框,导致 activity 可见但是位于后台无法和用户直接交互。
- 后台 Activity。已经被暂停的 Activity,比如执行了 onStop。
注意:一些后台工作不适合脱离四大组件而独自运行在后台中,这样进程很容易被杀死。比较好的方法是放入 Service 中从而保证进程有一定的优先级。
Activity与Fragment生命周期关系
创建过程 | 销毁过程 |
---|---|
Fragment–onAttach() | Fragment–onPause() |
Fragment–onCreate() | Activity–onPause() |
Fragment–onCreateView() | Fragment–onStop() |
Activity–onCreate() | Activity–onStop() |
Fragment–onActivityCreated | Fragment–onDestoryView() |
Activity–onStart() | Fragment–onDestory |
Fragment–onStart() | Fragment–onDetach() |
Activity–onResume() | Activity–onDestory() |
Fragment–onResume() |
Activity与menu创建先后顺序
在activity创建完回调onResume后创建menu,回调onCreateOptionsMenu。
Activity的启动模式
LaunchMode:
用于设定 Activity 的启动方式,无论是哪种启动方式,所启动的 Activity 都会位于 Activity 栈的栈顶。
首先了解,任务栈是一种”后进先出”的栈结构,当栈中无任何 Activity 时,系统会回收这个任务栈。
任务栈分为前台任务栈和后台任务栈(此栈中的 Activity 位于暂停状态),用户可通过切换将后台任务栈再次调到前台。
Low Memory Killer:App 的启动和销毁
- App 的启动销毁管理机制:Activity 栈,先进先出,保护机制(当内存不足时,先入栈的先杀掉。) -> LRU Cache(缓存淘汰机制)
启动模式要解决的问题:
- 默认情况下,当我们每次启动一个activity时,系统都会创建一个新的实例,并放入任务栈中,位于栈顶。
- 当按back键返回时,基于后进先出的原则,位于栈顶的activity会被移除,重复下去,当任务栈为空时,会被系统回收。
- 这种情况下,在使用时,系统会大量的并重复的创建实例,而当要退出应用时,需多次按返回,只有任务栈被销毁时,程序才会退出。所以需要不同的启动模式,来面对不同的情况。
四种启动模式:standard,singleTop,singleTask,singleInstance,
standard模式:标准模式。默认的启动模式,每次启动一个activity,都会创建一个新的实例,activity默认会进入启动它的activity所属的任务栈中(taskId值相同,hashcode值不同)。当我们不指定启动模式时,系统也会默认使用该模式。
注:在非activity类型的context(如ApplicationContext)并没有所谓的任务栈,所以不能通过ApplicationContext去启动standard模式的activity,用它启动 standard 模式的 Activity 时,可为待启动的 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记。
singleTop模式:栈顶复用模式。如果activity位于任务栈的栈顶,则不会被重新创建,而是复用栈顶的实例,并将Intent对象传入,同时回调它的onNewIntent方法。如果不在栈顶或不存在,则行为同standard模式。
注:执行onNewIntent()—->onResart()——>onStart()—–>onResume()。
singleTask模式:栈内复用模式。一种单实例模式,如果栈内存在此activity(根据taskAffinity匹配),则会清除之上的activity,复用此activity,并调用onNewIntent()方法。(onPause–>onNewIntent–>onResume)
当启动此模式 Activity 时,系统会先寻找是否存在它想要的任务栈:如不存在,就重新创建一个任务栈,然后创建它的实例后把它放到栈中。
如存在它需要的任务栈,并且栈中有它的实例存在,那么系统会把它调到栈顶(同时,默认具有 clearTop 效果)并调用它的 onNewIntent 方法,如果实例不存在,就创建它的实例并把它压入栈中。
singleInstance模式:单实例模式。除了具有 singleTask 模式的所有特性外,此模式的 Activity 只能单独位于一个任务栈中,当它启动时,系统会创建一个新的任务栈给它。由于站内复用特性,后续的请求均不会创建新的activity实例。
为 Activity 指定启动模式
- 第一种方式:在 AndroidMenifest 中为 Activity 指定 launchMode 属性。
<activity android:name=".standard.StandardActivity" android:launchMode="standard|singleTop|singleTask|singleInstance" >
- 第二种方式:动态代码,通过 Intent 设置标志位。(intent.addFlags())
- 两种方式的区别:
- 第二种方式优先于第一种方式。
- 限定范围不同。
任务相关性:TaskAffinity 参数
通过设置不同的启动模式可以实现调配不同的 Task。但是 taskAffinity 在一定程度上也会影响任务栈的调配流程。此参数标识了一个 Activity 所需要的任务栈的名字,这个属性值不能和包名相同(每一个 Activity 都有一个 Affinity 属性,如果不在清单文件中指定,默认情况下,所需任务栈的名字为包名,比如初始的MainActivity,它的Task的名字就是包名)。在默认情况下,同一应用程序的所有的 Activity 都有着相同的 taskAffinity。Android 使用任务(task)来管理 Activity,一个任务就是一组存放在栈里的 Activity 的集合,这个栈也被称作返回栈(back task)。栈是一种后进先出的数据结构。
通过命令:adb shell dumpsys activity activities
可将系统中所有存活中的 Activity 信息打印到控制台(其中 TaskRecord 代表一个任务栈)。
TaskAffinity 的值为字符串,且中间必须含有包名分隔符”.”。可以在 AndroidManifest.xml 中通过给 activity 指定 TaskAffinity 属性来指定任务栈。指定方式:<activity android:name=".ActivitySingleTop" android:launchMode="singleTop" android:taskAffinity="com.castiel.demo.singletop"/>
TaskAffinity 属性主要和 singleTask、singleInstance 模式或者 allowTaskReparenting 属性配对使用。不对 standard 和 singleTop 模式有任何影响,即使指定了该属性为其他不同的值,这两种启动模式下不会创建新的 task,它们都被创建在一个任务栈中。taskAffinity 在下面两种情况时会产生效果:
1、taskAffinity 与 FLAG_ACTIVITY_NEW_TASK 或者 singleTask 模式配合使用时:
它具有该模式 Activity 目前任务栈的名字,待启动 Activity 会运行在名字和 TaskAffinity 相同的任务栈中。(如果新启动 Activity 的 taskAffinity 和 栈 的 taskAffinity 相同则加入到该栈中,如果不同,就会创建新栈。)
2、taskAffinity 与 allowTaskReparenting 配合时:
例子一:如果 allowTaskReparenting 属性为 true,A 应用启动 B 应用的一个 Activity C,C 会从 A 的任务栈转移到 B 的任务栈中,也就是说明 Activity 具有转移的能力。
比如使用一款社交应用,这个应用的联系人详情界面提供了联系人的邮箱,当点击邮箱时会跳到发送邮件的界面。会把发送邮件界面放到社交应用中详情界面所在栈的栈顶。
当社交应用启动了发送邮件的 Activity,此时发送邮件的 Activity 是和社交应用处于同一个栈中的,并且这个栈位于前台。如果发送邮件的 Activity 的 allowTaskReparenting 设置为 true,此后 E-mail 应用所在的栈位于前台时,发送邮件的 Activity 会由社交应用的栈中转移到与它更亲近的邮件应用(taskAffinity 相同)所在的栈中。
例子二:allowTaskReparenting 赋予 Activity 在各个 Task 中间转移的特性。一个在后台任务栈中的 Activity A,当有其他任务进入前台,并且 taskAffinity 与 A 相同,则会自动将 A 添加到当前启动的任务栈中。举一个生活中的场景:
在某外卖 App 中下好订单后,跳转到支付宝进行支付。当在支付宝中支付成功之后,页面停留在支付宝支付成功页面。
按 Home 键,在主页面重新打开支付宝,页面上显示的并不是支付宝主页面,而是之前的支付成功页面。
再次进入外卖 App,可以发现支付宝成功页面已经消失。
例子三:假设有两个项目
- 项目一:其中有 3 个 Activity:FirstA、FirstB、FirstC。打开顺序依次是 FirstA -> FirstB -> FirstC。其中 FirstC 的 taskAffinity 为”lagou.affinity“,且 allowTaskReparenting 属性设置为true。FirstA 和 FirstB 为默认值;
- 项目二:其只有一个 Activity–ReparentActivity,并且其 TaskAffinity 也等于”lagou.affinity“。
- 将这两个项目分别安装到手机上之后,打开项目一,并从 FirstA 开始跳转到 FirstB,再进入 FirstC 页面。然后按 Home 键,使其进入后台任务。此时系统中的 Activity 信息:
- TaskRecord -> FirstC、FirstB、FirstA。
- 接下来,打开项目二,屏幕上本应显示 ReparentActivity 的页面内容,但是实际上显示的却是 FirstC 中的页面内容,并且系统中 Activity 信息如下:
- TaskRecord -> FirstC、ReparentActivity。
- TaskRecord -> FirstB、FirstA。
- 可以看出,FirstC 被移动到与 ReparentActivity 处在一个任务栈中。此时 FirstC 位于栈顶位置,再次点击返回键,才会显示 ReparentActivity 页面。
接下来通过系统源码来查看 taskAffinity 的应用(基于 Android 8.0 版本)。ActivityStackSupervisor 的 findTaskLocked 方法用于找到 Activity 最匹配的栈,最终会调用 ActivityStack 的 findTaskLocked 方法,这里只截取了和 taskAffinity 相关的部分:ActivityStack.java
1 | void findTaskLocked(ActivityRecord target, FindTaskResult result) { |
Intent 的 Flags
在 Intent 中定义了很多 FLAG,其中有几个 FLAG 也可以设定 Activity 的启动方式,如果 Launch Mode 和 FLAG 设定的 Activity 的启动方式有冲突,则以 FLAG 设定的为准。
所以,Flags 并不能简单地等同于启动模式,它的作用也很多,比如可以影响 Activity 的运行状态。一般不需要指定标记位,理解即可。几个常见的标记位如下:
- FLAG_ACTIVITY_NEW_TASK:和 Launch Mode 中的 singleTask 效果是一样的。
- FLAG_ACTIVITY_SINGLE_TOP:和 Launch Mode 中的 singleTop 效果是一样的。
- FLAG_ACTIVITY_CLEAR_TOP。在 Launch Mode 中没有与此对应的模式,如果要启动的 Activity 已经存在与栈中,则将所有位于它上面的 Activity 出栈。singleTask 默认具有此标记位的效果。
还有一些 FLAG 对分析栈管理有些帮助:
- FLAG_ACTIVITY_NO_HISTORY:Activity 一旦退出,就不会存在于栈中。同样地,也可以在 AndroidManifest 文件中设置 android:noHistory。
- FLAG_ACTIVITY_MULTIPLE_TASK:需要和 FLAG_ACTIVITY_NEW_TASK 一同使用才有效果,系统会启动一个新的栈来容纳新启动的 Activity。
- FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:Activity 不会被放入到 “最近启动的 Activity” 列表中。也就是说此 Activity 不会出现在历史 Activity 列表中。
- FLAG_ACTIVITY_BROUGHT_TO_FROM:这个标志位通常不是由应用程序中的代码设置的,而是 Launch Mode 为 singleTask 时,由系统自动加上的。
- FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY:这个标志位通常不是由应用程序中的代码设置的,而是从历史列表中启动的(长按 Home 键调出)。
- FLAG_ACTIVITY_CLEAR_TASK:需要和 FLAG_ACTIVITY_NEW_TASK 一同使用才有效果,用于清除与启动的 Activity 相关栈的所有其他 Activity。
接下来通过系统源码来查看 FLAG 的应用(基于 Android 8.0 版本)。首先,根 Activity 启动时会调用 AMS 的 startActivity 方法,经过层层调用会调用 ActivityStarter 的 startActivityUnchecked 方法:ActivityStarter.java
1 | private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord, |
接下来查看 computeLaunchingTaskFlags 方法,,计算启动的 FLAG 的逻辑比较复杂,这里只截取了一小部分:
1 | private void computeLaunchingTaskFlags() { |
IntentFiler 的匹配规则
启动 Activity 分为两种:
- 显式调用
需明确指定被启动对象的组件信息,包括包名和类名。优先级更高。 - 隐式调用
不需要明确指定组件信息。需要 Intent 能匹配目标组件的 IntentFilter(可以有多个,匹配其一即可) 中所设置的过滤信息。
过滤信息有 action、category、data,它们都可以有多个,同时匹配这三个类别才能启动目标 Activity。
注意,不含有 DEFAULT 这个 category 的 Activity 是无法接收隐式 Intent 的。
可通过 PackageManager 的 resolveActivity,queryIntentActivities(这两方法的标记位使用 MATCH_DEFAULT_ONLY,仅匹配声明了 DEFAULT 这个 category 的 Activity),或者是 Intent 的 resolveActivity 。来判断是否 null,避免 startActivity 失败。
各属性匹配规则
- action
要求 Intent 中的 action 必须有一个且必须和过滤规则中的其中一个 action 相同,并且区分大小写。 - category
可以不存在(会默认加上标记位),但如果有,每一个都要能够和过滤规则中一个 actegory 相同。 - data
如果过滤规则中定义了 data,那么 Intent 中也必须定义可匹配的 data。如果要指定完整的data,必须调用 setDataAndType(Uri.prase(file://abc),”text/plain”) 方法。
它由两部分组成: - mimeTyp。指媒体类型
- URI。默认值为 content 和 file。
后台启动 Activity
从 Android10(API 29)开始,Android 系统对后台进程启动 Activity 做了一定的限制。主要目的就是尽可能的避免当前前台用户的交互被打断,保证当前屏幕上展示的内容不受影响。解决办法:Android 官方建议我们使用通知来替代直接启动 Activity 操作:也就是当后台执行的任务执行完毕之后,并不会直接调用 startActivity 来启动新的界面,而是通过 NotificationManager 来发送 Notification 到状态栏。这样既不会影响当前使用的交互操作,用户也能及时获取后台任务的进展情况,后续的操作由用户自己决定。
示例(Kotlin)
1 | /** |
1 | class Main2Activity : BaseActivity(), View.OnClickListener { |
1 | class Main3Activity : BaseActivity() { |
System.exit(0) 、onDestory()、Activity.finish()三者的区别
- System.exit(0) 是正常结束程序,kill 掉当前进程,针对的是整个Application。
- onDestory() 方法是 Activity 生命周期的最后一步,资源空间等就被回收了。当重新进入此 Activity 的时候,必须重新创建,执行 onCreate() 方法。
- Activity.finish() 当调用此方法的时候,系统只是将最上面的Activity移出了栈,并没有及时的调用onDestory() 方法,也就是占用的资源没有被及时释放。
1 | /** |
1 | <!-- theme 对话框形式主题 --> |
备注
参考资料:
Android 开发艺术探索