说到Activity启动模式,相信大家肯定都不模式,多少会有所接触 大多人应该都知道SingleInstance模式可以保证Activity单例,对其它模式的功能和具体使用场景应该都不是很清楚
LaunchMode的背后,其实本质上是TaskStack管理,和Activity切换管理 这里就给大家完整地讲解下关于任务栈的全部知识和原理,以及应用场景
什么是任务栈
顾名思义,任务栈就是通过一个栈的数据结构来管理一系列任务,实际上,这里的任务指的就是Activity,任务栈就是管理Activity切换的 任务栈记录了Activity之间的调用和启动顺序,当我们按下返回键时,就可以根据任务栈里的信息来决定返回到哪个窗口
看到这里,有人可能会觉得,任务栈不就是一个List就能解决的事情,第N个Activity销毁,返回到第N-1个Activity就是了 其实这样想也算对了那么一点点,因为栈说白了就是弹出N,露出N-1,但是这仅仅是任务栈最简单的形式
实际上,应用并不一定就是当前窗口返回上个窗口,还可能在返回前销毁一些已经无效的窗口 而且,应用还可能是多进程的,或者被其它进程调用启动,这些都不是一个List能够解决的
任务栈特性
- 打开一个新的Activity后,新的Activity会被存放到栈顶位置,只有栈顶的Activity才可以与用户进行交互
- 栈顶的Activity结束后,界面会回退到任务栈中的上一个Activity
- 当一个任务栈中的Activity全部结束,无Activity可以继续回退时,会销毁此任务栈,并回退到最近一次访问的任务栈的栈顶
- 系统桌面,即Launcher程序,也是一个独立的任务栈,当我们从桌面进入到自己的任务栈,任务栈结束后就会回到桌面
- 当我们按下Home键(圆形按钮)或任务键(方形按钮)时,实际上是打开了Launcher程序,等于进入了Launcher任务栈一次
- 任务栈是由操作系统管理的,它并不属于某个应用或进程
- Launcher程序通过桌面图标启动了一个新Activity,Launcher程序通过通知栏启动了一个新Activity,旧的Activity启动一个SingleInstance模式的Activity,旧的Activity启动了一个指定了taskAffinity属性的Activity,旧的Activity启动了一个documentLaunchMode="always"的Activity,旧的Activity启动Activity时指定了FLAG_ACTIVITY_NEW_TASK选项,都会创建一个新的任务栈
- 除了这些情况之外,一个Activity启动另一个Activity,这两个Activity都是处于同一个任务栈中的,即便是多进程应用,或者调用其它应用中的Activity组件,也是处于同一任务栈的
- 综上所述,我们大概知道,一个应用可以包含多个任务栈,一个任务栈也可以包含来自不同应用的Activity,主要看这些Activity是不是通过相互调用启动的,只有通过Launcher程序启动,或者指定了特殊模式的Activity,才会创建一个新的任务栈
获取应用任务栈信息
操作系统处于安全考虑,只允许我们获取和当前应用相关的任务栈 这里的相关,指的任务栈由当前应用创建,如果其它应用启动了我们的共享组件,只能在其它应用中获取该组件相关的任务栈信息
方法一:这个API只能获取到最近任务列表中的任务栈 后台任务栈必须开启了android:documentLaunchMode=“always”,出现在最近任务列表中,才能获取到
//获取任务栈列表
ActivityManager activityManager = getSystemService(ActivityManager.class);
List tasks = activityManager.getAppTasks();
//遍历任务栈
for (ActivityManager.AppTask task : tasks) {
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
int taskId = info.id; //任务栈ID
Intent launchIntent = info.baseIntent; //创建该任务栈的Intent
String action = launchIntent.getAction(); //Intent的action
Set categories = launchIntent.getCategories(); //Intent的category
ComponentName component = launchIntent.getComponent(); //Intent要启动的组件名,即Activity的包名和类名
Bundle extras = launchIntent.getExtras(); //Intent携带的额外参数
int activityCount = info.numActivities; //栈内Activity数量
String topActivity = info.topActivity.getClassName(); //栈顶Activity
String baseActivity = info.baseActivity.getClassName(); //栈底Activity,如果所有Activity都不销毁,baseActivity就是originActivity
boolean isRunning = info.isRunning; //任务栈是否在运行,Activity有可能处于挂起状态,等待restore,该API安卓10开始才可用
task.moveToFront(); //调度任务栈至前台显示
task.setExcludeFromRecents(true); //不显示在最近任务列表中
task.finishAndRemoveTask(); //销毁任务栈内全部Activity
task.startActivity(context, intent, options); //在指定任务栈中启动一个新的Activity,并将当前任务栈调至前台
}
方法二:能获取应用全部的任务栈,但是接口没有上面的灵活
//获取任务栈列表
ActivityManager manager = getSystemService(ActivityManager.class);
List infos = manager.getRunningTasks(100);
//遍历任务栈
for (ActivityManager.RunningTaskInfo info : infos) {
int taskId = info.id;
int activityCount = info.numActivities;
ComponentName baseActivity = info.baseActivity;
ComponentName topActivity = info.topActivity;
manager.moveTaskToFront(taskId, 0);
break;
}
Activity启动模式
任务栈最简单的管理方式就是,每次启动Activity都在当前栈新建一个Activity实例,放到栈顶 但这个方式并不能适合所有情景,因此安卓提供了几种常用的任务栈管理方式,即Activity的启动模式,这个属性可以在Manifest清单中通过launchMode属性配置
- Standard模式,默认的模式,总是在当前任务栈中创建一个新的Activity实例
- SingleTop模式,如果栈顶已存在目标Activity的实例,则复用,否则新建一个新的实例
- SingleTask模式,如果栈中已存在目标Activity的实例,则复用,同时清除此Activity顶部的其它Activity
- SingleInstance模式,单例模式,开启一个独立的任务栈,专门存放目标类型的Activity,并且只允许一个对象实例,已存在则复用。如果SingleInstance模式的Activity启动了其它类型的Activity,如果没有特别设定,被启动的Activity将会被存放到Standard任务栈中
- SingleTask模式适合作为程序的启动入口,回到入口时,其它Activity全部销毁
- SingleInstance模式适合与程序独立,或者供多个应用共享的界面
几种特殊启动模式的应用场景
- SingleTop模式
比如我们使用一款视频APP观看短视频,视频播放页面同时还推荐了类似视频 当我们点击了推荐视频,事件顺序大概是:打开视频播放页面 - 点击推荐视频 - 复用当前页面播放推荐视频,同时刷新推荐列表 即栈顶对象可复用,没必要新建一个实例
- SingleTask模式
比如我们使用一款购物APP购买商品,Activity打开顺序大概是:商品页面 - 下单页面 - 支付页面 - 交易完成界面 显然,我们完成时,需要返回商品页面,没必要返回支付页面,因为订单已经结束,因此我们需要在回到商品页面时销毁下单和支付页面 即Activity顶部的任务已过期,没必要再保留
- SingleInstance模式
比如我们使用系统自带的相机应用,由于性能和硬件占用的原因,肯定是不允许同时打开两份的 当我们进去相机开始录像,再去其它应用里面逐个逛一遍,然后回到桌面再点击相机图标,肯定是会回到之前的实例里继续录制,而不是新建一个新的实例,也不能销毁改Activity顶部的其它Activity 即客观条件只允许单例,但栈结构并不允许复用栈中间的对象,只能复用栈顶对象( SingleTop),或者先弹出复用对象顶部的其它对象,让目标对象浮到栈顶再复用(SingleTask),所以SingleInstance模式只能自己独占一个任务栈
SingleInstance模式弊端
SingleInstance模式将应用变成了多任务栈应用,如果两个任务栈之间启动过其它应用,将会导致应用回退混乱
由于安卓在任务栈栈销毁时,会回退到最近一次启动过的其它任务栈,这样如果同一个应用的栈A和栈B中间启动过其它应用的任务栈的话,当栈B按返回结束后,就不会跳到栈A,而是跳到其它应用
由于安卓桌面本身也是一个应用,也占用了一个任务栈,通过通知栏或后台工作,其它应用也可能中途启动,这种情况是经常发生的
一个常见的情景就是:栈A启动 - 栈B启动 - 按下Home键回到桌面 - Launcher栈启动 - 回到栈B - 栈B销毁 - 回到Launcher栈(即回到桌面)
解决SingleInstance模式下,按下返回键不返回上个Activity,而是返回桌面的问题
通过以上的原理阐述,我们已经能够很容易的解释这种现象
假如我们的应用是个多任务栈应用,包含了任务栈A,任务栈B,桌面程序使用的是Launcher任务栈 我们在任务栈A中启动了任务栈B,然后按下了Home键或任务键,又回到了任务栈B,然后按下了返回键 由于按下Home键或任务键,就等于启动了Launcher任务栈,那么任务栈B之前最近一次启动的任务栈就是Launcher,任务栈B销毁后就得返回Launcher
解决方案很简单,由于我们可以获取到当前应用的任务栈列表,我们将任务栈列表中的其它任务栈调度至前台即可
但是这个方法仅适合双任务栈的情景,N个任务栈的情景则需要大家根据业务去变通处理 好在任务栈信息是可获取的,大家完全可以在Activity创建和销毁时,记录任务栈调度过程,写一个自己的任务栈管理规则,这样再复杂的情景都不是问题
上面提到了两种获取任务栈的方式,这里我们使用第二种方式来获取任务栈信息
@Override
public void onBackPressed() {
//获取任务栈列表
ActivityManager manager = getSystemService(ActivityManager.class);
List infos = manager.getRunningTasks(100);
//遍历任务栈
for (ActivityManager.RunningTaskInfo info : infos) {
int taskId = info.id;
ComponentName baseActivity = info.baseActivity;
ComponentName topActivity = info.topActivity;
int activityNum = info.numActivities;
//跳过当前任务栈
if (taskId == getTaskId()) continue;
//跳过外部任务栈,一般为Launcher程序的任务栈
if (!Texts.equal(topActivity.getPackageName(), getPackageName())) continue;
//将任务栈调度至前台
//使用ActivityManager.MOVE_TASK_WITH_HOME作为flag,可以将任务调至栈顶时,同时将Home程序的任务栈调至该任务之下
//这样当用户结束栈顶任务时,就会回退到Launcher任务栈,即回到桌面,这个功能可用于特殊场景
manager.moveTaskToFront(taskId, 0);
break;
}
//将当前任务栈移至后台
moveTaskToBack(true);
//销毁当前任务栈
finishAndRemoveTask();
}
任务栈高级选项
除了以上提到的基本功能之外,安卓还提供一些高级选项,可以在Manifest中进行配置
- taskAffinity:任务栈关联,新开一个任务栈,相同taskAffinity的Activity会放到同一个任务栈,该属性必须以冒号和英文开头。启动Activity时,只有携带了Intent.FLAG_ACTIVITY_NEW_TASK标志,taskAffinity属性才会生效。设置了taskAffinity的任务栈会在最近任务列表中通过一个单独的缩略图来显示
- taskAffinity属性的一个用途是,限制SingleTask销毁顶部的所有Activity,比如Activity的打开顺序是:X - A - B - C - D - E - X,如果我们想X第二次启动时,销毁DE,而保留ABC,我们可以将XDE放在同一个栈中,设置相同的taskAffinity,并将X设为SingleTask模式
- documentLaunchMode:文档模式,在编程中,习惯把顶级应用下的独立的字窗口叫做文档。文档模式会给Activity新开一个任务栈,并且在最近任务列表中通过一个单独的缩略图来显示。同一个Activity如果有多个实例,每个实例都是一个独立的任务栈。设置了documentLaunchMode后,taskAffinity属性无效
- allowTaskReparenting:任务重新绑定任务栈,一般用于共享Activity。比如应用A打开了应用B的共享组件ShareActivity,由于是从应用A打开的,ShareActivity会保存到应用A的任务栈中。但如果我们此时启动应用B,应用B将不会从LoginActivity启动,而是直接从ShareActivity启动,并将ShareActivity从应用A的任务栈迁移到应用B的任务栈中
- alwaysRetainTaskState:在Activity被后台清理时,是否保留任务栈状态。如果设置为了false,任务栈的状态有可能不被保存,下次打开应用时,任务栈将从栈底的RootActivity重新启动。如果设置为true,应用则会重现创建所有的Activity,恢复之前的任务栈状态,并回到之前栈顶的TopActivity
- clearTaskOnLaunch:点击桌面图标,重现启动应用时,是否清理之前的任务栈状态。如果设置为true,则会清理之前的任务栈状态,等于是完全重新启动。如果设置为false,则会回到上次离开应用时所在的Activity,一般情况下都为false
- autoRemoveFromRecents:当任务栈为空时,是否自动从最近任务列表中清除缩略图
解决SingleInstance模式下,点击桌面图标没有回到上个Activity,而是自动重启的问题
这个问题网上很多都是通过判断isTaskRoot的方法来解决的,但是没有讲解过原理 实际上,这并不是一个通用的解决方法,仅适合某些APP的任务栈管理情景,想要根本解决这个问题,还是要弄懂原因
假如我们应用的启动顺序是SplashActivity - LoginActivity - MainActivity 其中MainActivity是SingleInstance模式,独占一个任务栈,其它两个Activity是普通模式 SplashActivity是Application启动后的首个Activity,我们把它叫做入口Activity,把它所在的任务栈叫做Standard任务栈
点击桌面图标后,Launcher做的工作是:检查Standard任务栈是否存在,存在就打开Standard任务栈的栈顶Activity,不存在就新建一个Standard任务栈,并在该任务栈中启动入口Activity
假如我们从MainActivity回到桌面,再点击应用图标,这时应用怎么做,实际上取决于SplashActivity和LoginActivity有没有被销毁。但不管怎么样,肯定不会回到MainActivity,因此Launcher要启动的是Standard任务栈
如果LoginActivity没有被销毁,那么LoginActivity就处于Standard任务栈的栈顶,返回到LoginActivity 如果LoginActivity和SplashActivity在跳转到其它Activity时都销毁了自己,Standard任务栈就是空的,也会被销毁,从Launcher再点击图标时,就会重建Standard任务栈和LoginActivity实例
值得一提的是,Standard任务栈和LoginActivity重建,并不代表整个应用重建了。进程还是之前的那个进程,MainActivity实例仍然是存活的。Launcher的目的并不是为了重启应用,仅仅是因为工作机制如此
原理弄清楚了,解决问题其实就简单了。我们只需解决两个问题,一个是怎么判断应用是首次启动还是第二次启动,另一个是怎么回到MainActivity
判断应用是否首次启动其实很简单,我们记录下Application的启动时间,对比下时间间隔就知道是否首次启动了,不是首次启动就销毁SplashActivity和LoginActivity
如果只有一个SingleInstance模式的Activity的话,直接启动MainActivity就行了。如果有多个SingleInstance模式的Activity的话,我们可以在onCreate方法中记录下每个Activity的启动顺序,找到最后一次启动的Activity的类名,启动它就行了
对于一般应用而言,SingleInstance模式和多进程的Activity并不会特别多。弄清了原理,我们根据情况变通就行了
long interval = Times.millisOfNow() - CommonApplication.applicationStartTime;
if (interval > 1000) {
finish();
start(MainActivity.class);
return;
}
应用退出时从最近任务列表中清除缩略图
通过上面提到的autoRemoveFromRecents属性,就可以很简单的达到这个效果 但是我们需要为每个任务栈都设置一次,我们也可以通过代码,在基类Activity中统一清除
@Override
protected void onDestroy() {
super.onDestroy();
//获取任务栈列表
ActivityManager manager = getSystemService(ActivityManager.class);
List tasks = manager.getAppTasks();
//遍历任务栈,找到当前任务栈
//如果任务栈没有其它Activity,则从最近任务列表中移除
for (ActivityManager.AppTask task : tasks) {
ActivityManager.RecentTaskInfo info = task.getTaskInfo();
if (info.numActivities == 0)
task.finishAndRemoveTask();
}
}