您当前的位置: 首页 >  android

命运之手

暂无认证

  • 3浏览

    0关注

    747博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

【Android】【TaskStack】任务栈与Activity启动模式全解析

命运之手 发布时间:2020-09-06 15:24:34 ,浏览量:3

说到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();
	    }
	}

关注
打赏
1654938663
查看更多评论
立即登录/注册

微信扫码登录

0.0602s