启动流程
使用 Context 启动活动的方法如下
1 | void startActivity(Intent intent); |
其实际流程如图俄罗斯瓷娃娃一样,分别经过ContextImpl,Instrumentation和AMS的处理。
1。首先是 ContextImpl 类,它委托给了Instrumentation 类,自己仅仅检查线程和 flag 参数。
1 |
|
这里应该注意如果不是从 Activity 中启动活动,则需要添加 FLAG_ACTIVITY_NEW_TASK 标记,否则将抛出异常。
活动启动的结果用 Instrumentation.ActivityResult 类表示,其定义如下
1 | private final int mResultCode; |
而后将对该结果进行检测处理,采用主线程 Handler 机制进行,发生消息为 H.SEND_RESULT ,响应方法为
1 | void handleSendResult(ResultData res) |
该方法中将分发处理结果( dispatchActivityResult ),并调用 onActivityResult(requestCode, resultCode, data) 方法。
2。其次是 Instrumentation 类,它委托给AMS来完成,自己只检查下结果,验证活动是否能够正确启动。
1 | ActivityResult execStartActivity(...){ |
如结果是 ActivityManager.START_CLASS_NOT_FOUND 会爆出常见异常
1 | Unable to find explicit activity class ; have you declared this activity in your AndroidManifest.xml?" |
3。最后是AMS ,启动活动会经过一系列复杂的流转,涉及到活动栈的处理等,但最终会回到 ActivityThread 类中来。
在 ActivityThread的 main 方法中,启动了主线程的 Looper 循环。启动活动将发送一个 LAUNCH_ACTIVITY 消息,而后使用内部类 H 来处理该消息。
1 | ActivityClientRecord r = (ActivityClientRecord) msg.obj; |
handleLaunchActivity 方法将完成 WindowManager 初始化,并利用反射创建活动对象
1 | void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) { |
performLaunchActivity 方法创建活动,
1 | private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent){ |
其中 attach 方法创建了 Window 的实例。
1 | void attach(Context context){ |
如果活动创建失败,则结束;如果成功,handleResumeActivity 方法会将 DecorView 对象渲染到窗口 PhoneWindow 上去,但注意此时该控件是不可见的。如果启动成功,在处理结果的时候后将可见性改为可见。
1 | void handleResumeActivity(IBinder token, ...){ |
因为 AMS 是单例实现,Hook 掉 AMS 非常容易,详情见weishu的文章
1 | Class amsClass = Class.forName("android.app.ActivityManagerNative"); |
生命周期
在理解了启动流程后,更容易理解生命周期,正常的启动和退出流程是
启动(post 方法等为碎片而存在):
onCreate –> onStart –> onPostCreate –> onResume –> onPostResume –> onAttachedToWindow
退出:
onPause –> onStop –> onDestroy –> onDetachedFromWindow
如果中途切换其它 App
onPause –> onSaveInstanceState –> onStop
恢复页面
onRestart –> onStart –> onResume –> onPostResume
使用adb命令回收Activity(adb shell am force-stop [包名]),与正常退出一致。
如果使用下列方法旋转屏幕(可以靠重力感应完成,会销毁和重建活动)
1 | setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); |
则生命周期如下,这里调用保存和恢复方法,也可以在onCreate方法中恢复。
onPause –> onSaveInstanceState –> onStop –> onDestroy –> onDetachedFromWindow–>
onCreate –> onStart –> onRestoreInstanceState –> onResume –> onAttachedToWindow
这和使用 recreate()方法效果一样。
做如下配置可以避免重建,只执行 onConfig 方法
1 | android:configChanges="keyboard|screenSize|orientation" |
此外启动栈内已存在的活动将会调用 onNewIntent 方法。
保存和恢复
活动的保存和恢复是向下分发的,碎片和View都受到影响。如 View会构建一个BaseSavedState对象
1 | protected Parcelable onSaveInstanceState() { |
使用时获取并扩展该对象,写入数据即可。
1 | public static class SavedState extends BaseSavedState { |
任务栈
Activity 的启动跳转关系与Task(任务)和 Back Stack(回退栈)紧密相连,Task
是一个Framework层概念,专指在程序运行时一组相互关联的activity的集合,负责控制界面的跳转和返回。Back Stack则是实现 Task 所用的数据结构,拥有栈的特点.
下面三个属性会影响Task和Back Stack的状态。
- 活动的启动模式
- taskAffinity属性
- intent的flag属性
1 | android:launchMode="standard" |
简单说来
- 启动模式为standard或singleTop时,Activity 一般在同一个栈中。
- 启动模式为singleTask或singleInstance时,一般会产生新的任务栈。
Task和Back Stack
默认情况下,某个Application中所有activity都处在同一个Task中。但二者并没有严格的约束条件,这里有两种情况:
- 两个Application中的activity可以处在同一个Task中。即 task 是可以跨应用的,这正是task存在的一个重要原因。
- 一个Application中的activity也可以处于多个Task中。
例如在应用中的一个Activity A中使用系统邮件,会启动系统邮件程序的一个Activity B。这两个activity是存在于不同app中的,但是被系统放在一个task中,这样能保证程序回退到原有程序中。
设备Home界面是大多数tasks的起始点。当用户点击程序在Home上的启动图标时,这个程序的task就转入前台。如果程序没有打开,那么之前不存在task,一个新的task将被创建并且程序的”main” 活动将被打开并被推入栈中;如果程序已经打开,则已经存在Task,就恢复显示栈顶的活动。
当目前的activity启动另外一个activity时,新的activity被压入栈中作为栈顶并且获取到了focus。前面的那个activity则进入stopped状态,此时系统会保留它的UI状态以便恢复。当用户点击back按钮时,栈顶的activity从栈顶退出并被destroyed,之前处于stopped状态的activity则进入resume状态(其保存的UI状态得到恢复).
如果用户持续点击back按钮,那么在栈中的每一个activity都会做退栈并显示之前activity的动作, 直到用户退回到Home界面(或者是用户开始task的地方)。当所有的activities都从栈中被移除之后,这个task也就消失了。
一个task是一个紧密结合的单元,处于后台的task中的所有活动都处于stopped状态,只有task转移到前台,栈顶的活动显示。众多tasks都可以一并在后台被Hold住,然而系统为了恢复内存而有可能销毁这些栈中的activities。
此时activity的状态信息则会丢失. 但系统仍然为那个activity在back stack中保留了位置, 但是当这个activity成为栈顶activity时, 系统必须recreate它(而不是resume它),这时需要使用者主动实现onSaveInstanceState()回调方法来保存恢复所需的信息。
在activity中调用 moveTaskToBack (boolean nonRoot)方法即可将activity 退到后台,注意不是finish()退出。
启动模式
每个活动的创建有两步:以下对四种启动模式的分析都从这两步进行
1.判断是否创建新任务栈,即新Task。
2.判断是否创建新活动,即新 Activity。
活动如何创建主要受两个因素影响:taskAffinity属性与启动模式。
a.taskAffinity 属性意味着 activity 更倾向归属于哪一个task,可以认为它指定了activity所在的task名称,这一属性是活动处在不同的Task
中的必要不充分条件。使用taskAffinity属性的一些原则是:
1.如果不设置某活动的taskAffinity属性,则该属性值与启动它的活动相同。第一个活动的该属性为应用的包名。
2.taskAffinity属性并不能唯一决定活动所在的栈,还要受到启动模式影响。
举例:假设某个APP内的活动启动顺序为:a-b-c-d。如果采用默认配置,则abcd四个活动都将处在名称为包名的 Task 中。
b.启动模式
设置活动和 Task 的关系。一共有四种启动模式:为standard,singleTop,singleTask,singleInstance。
Standard(默认模式)
1.taskAffinity 属性无效。即使设置也并不会创建新 Task ,活动所处的 task 与启动它的活动永远保持一致。
2.活动的创建百无禁忌,不需要任何检查就创建新的活动实例,因为能够重复创建活动实例。
SingleTop
- taskAffinity 属性同样无效。
2.与标准模式的区别在于第二步, SingleTop 模式对活动的创建做了一个较弱的约束,即不允许创建与栈顶重复的活动实例。会检查活动返回栈的栈顶活动是否是待启动的活动类,如果是则不会创建活动实例。
如当前活动栈是a-b-c-d
。如果再启动d
,仍是a-b-c-d
;启动a
,将是a-b-c-d-a
。
SingleTask
前两种模式中, taskAffinity 属性都是无效的,不会对新活动所处的 task 产生影响。 singleTask 模式下, taskAffinity 属性终于获得用武的机会,将对task产生影响。
1.在新活动c创建时,先查看是否存在与其taskAffinity属性相同的task。
- 如果存在,不会创建新task。
- 如果不存在,则创建以taskAffinity属性为名的新task。
如果不设置taskAffinity属性,则该属性值与启动它的活动一致,此时不会产生新的task。
2.再在选中的task中查找有无活动c实例。
- 有则将该task中c实例之上的活动全部出栈,使得该实例处在栈顶;
- 没有则在栈顶上新建活动
c
.
SingleInstance
SingleInstance 模式中,taskAffinity 属性再次失效,活动总会在新 Task 中启动。
1.不管怎么设置taskAffinity属性,活动总是会在新的任务task中运行。
2.以 SingleInstance 模式启动的活动在整个系统中是单例的。如果单例 task 中存在了一个实例,那么会把已存在的任务调度到前台,且会调用该Activity的onNewIntent方法。
以 SingleInstance 模式启动的 Activity 具有独占性,即它会独自占用一个task,被他开启的任何 Activity 都会运行在其他task中, 这一点与 singletask 模式有所区别。
使用技巧:如何退出APP?
性能分析工具 dumpsys可以查看感兴任务栈信息
1 | //1. 列出dumpsys所有支持命令 |
命令得到的信息很多,可以获取
1 | ACTIVITY MANAGER RECENT TASKS (dumpsys activity recents) |
singleTask 这个模式有一个重要应用,即所谓的“优雅的退出APP”。将第一个活动设置为singleTask 模式,则在其他活动中向首活动跳转将会将该栈内部的所有活动出栈。
如果某APP中启动活动顺序是A–>B—>C—>D,要从D中退出整个APP。原理是将活动A设置为singleTask 模式,并在该活动中跳转到启动活动A,则A之上的活动将全部出栈,最终结束A即可。
这么做的思路和原理是正确的,但是必须保证ABCD都处在同一个任务栈下才有效。A只能将和它相同的栈内的活动出栈,而不能将其他任务栈中的活动出栈。
更简单的操作是直接使用finishAffinity()方法,注意该方法会销毁栈。
Intent
Intent 译作“意图”,表示一个对某类数据待执行的操作,可以用来启动安卓的三大组件活动,服务和广播。因为 Intent 将用在跨进程通信中,所以它是一个 Parcelable 类。
Intent 的基本信息包括 action 和 data。 Action 表示动作类型,Data 指定动作操作的数据 Uri。 例如 ACTION_VIEW 是默认动作,表示展示数据给用户,而展示何种数据由类型决定,展示的内容由 Uri 指定,下例表示展示文本数据。
1 | Intent intent = new Intent(ACTION_VIEW); |
发出此 Intent 后,设备内部能够匹配此意图的应用会出现在列表中,即数据展示方式由设备已有的程序决定。隐式 Intent 的解析机制依赖 IntentFilter ,安卓组件通过设置 IntentFilter 确定自己所能匹配的 Intent 。
Intent 的补充信息包括Categories(对动作信息进行补充),Type(指定数据的MIME类型),Extras(提供额外信息),
当发出 Intent 后会对 PackageManager 做查询,在 AndroidManifest.xml 配置文件上寻找能够完成匹配的安卓组件。组件中 IntentFilter 所定义的信息要全部一致才算匹配成功,如果定义了多个 category ,至少要匹配其中一个。
安卓组件也可以定义多个 intent-filter ,此时只要有一个匹配成功就能启动组件。
如果设备内没有能够响应 Intent 的程序,则会抛出异常,因此为安全起见可用如下方法预先判断下
1 | List<ResolveInfo> resolveInfos = getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); |
PackageManager.MATCH_DEFAULT_ONLY表示只匹配设置了CATEGORY_DEFAULT的活动。
标记位 Flags 控制动作的执行方式,它与具体的组件有关。最常见的标记位多是与活动组件由关的,以FLAG_ACTIVITY
开头。
1.FLAG_ACTIVITY_SINGLE_TOP表示SINGLE_TOP启动方式
2.FLAG_ACTIVITY_NO_ANIMATION防止活动启动时使用转场动画。
3.FLAG_ACTIVITY_TASK_ON_HOME 活动将在主屏幕所在栈上新栈,此时点击后退,将回退到主屏幕上。
4.FLAG_ACTIVITY_CLEAR_TASK 此前的活动栈将销毁,所启动的活动将成为新栈的根,需要配合FLAG_ACTIVITY_NEW_TASK
使用。
1 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); |
PendingIntent
PendingIntent
表示一个延时意图,亦是Parcelable
对象,因为是跨进程实现,即便启动进程销毁,IIntentSender对象依然存在在另一个进程中。这造成一个问题,使用该对象时如果进程已经有一个PendingIntent对象存在,应该如何处理?
1.不管怎样首先要判断两个PendingIntent是否相等?
一个错误是创建多个仅仅”extra”不同的PendingIntent
对象,这些实际上相同的。使用下列方法可以进行相等性判断
1 | Intent#filterEquals(Intent) |
2.发送时如果要创建不同的PendingIntent对象,可以更改getActivity方法中的请求码。
3.发送时如果要创建相同的PendingIntent对象,可以使用FLAG_UPDATE_CURRENT /FLAG_CANCEL_CURRENT标记位。
FLAG_ONE_SHOT
PendingIntent
只使用一次FLAG_NO_CREATE
PendingIntent
如果不存在,不再创建FLAG_CANCEL_CURRENT
如果PendingIntent
存在,取消创建新的 ,如果发送的intent仅仅在extra 数据不同,可以使用该标记位创建新的FLAG_UPDATE_CURRENT
如果PendingIntent
存在,替代它的extra data
几个常用的API
关于栈
1 | public int getTaskId() |
创建和关闭
1 | public void recreate(); |