先给出结论:Activity的启动模式的设计的主要目的是为了解决应用页面交互需求的不同场景。
具体不同的启动模式,适合解决什么样的场景问题,我们先来看看Activity的启动模式都有哪些。
1、standard标准模式
在介绍标准模式之前,先介绍一下多个Activity实例在内存当中的维护数据结构是什么,所有的Activity实例被创建后,都会加入到栈数据结构中,在Android知识体系的术语中,被称为任务栈。任务栈是一种后进先出的数据结构。在后续的其它三种模式的介绍中,虽然其和标准模式存在差异,但无论如何是离不开栈这种数据结构的特性,亦即”后进先出“。这一句话,读起来不是很好理解,那么需要在后文中需要细致品味至理解。
那么标准模式正是每次启动一个Activity都会重新创建一个新的实例,不管这个实例是否已经存在。在这种模式下,启动了这个Activity,那么这个Activity就运行在启动它的那个Activity的任务栈中。比如Activity A启动了Activity B(B是标准模式),那么B就会进入到A所在任务栈中。
这里面有一个问题,android应用中的第一个Activity是如何启动的,第一个任务栈又是如何创建的?
在Android的应用工程中,我们应用程序能够启动,我们必须在Manifest.xml清单文件中,配置启动Activity,代码如下:
<activity
android:name=".business.user.activity.SplashActivity"
android:theme="@style/SplashTheme"
android:windowSoftInputMode="stateHidden|adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
本篇文章,主要是阐述Activity的启动模式,就不对Launcher Activity的过程做详细的分析,直接给出结论:应用的第一个Activity的启动,是通过系统交互行为完成的,并且在创建和启动这个Activity的时候,也会创建管理这个Activity的任务栈(点击桌面应用icon->创建用来管理Activity的任务栈->系统寻找对应的启动Activity)。
另外,标准模式的Activity是不能使用ApplicationContext去启动的,大多初学者,应该遇到过这样的错误,如下:AndroidRuntimeException: Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag.Is this really you want?
这里面的原因就是非Activity类型的context,并没有所谓的任务栈,解决这个问题的方法根据建议是设置标志位。但是通过设置标志位的方式,其实改变了当前Activity的启动模式,所以在实际应用开发的过程中,我们需要根据实际的场景去决定,是通过设置标志位的方式换掉当前要启动的Activity的启动模式,还是使用一个Activity的Context来满足当前Activity的启动。
使用场景结论:标准启动模式,适用于app里面的大部分普通页面,打开一个新的页面,就创建一个Activity,由于标准启动模式的是最普通的模式,所以在使用场景这块,主要是看页面交互需求里,哪些不适合标准启动模式,我们要用新的启动模式替换标准启动模式。
2、singleTop栈顶复用模式
这种模式,如果新Activity已经存在实例位于任务栈的栈顶,那么此Activity不会被重新创建,同时它的onNewIntent的方法会被回调,通过此方法,我们可以取出当前请求的信息。简单举个例子:如果当前的Activity的任务栈是ABCD,D是singleTop模式,那么再启动一次D,任务栈中仍然是ABCD;D若是标准模式,那么再启动一个D,任务栈中就是ABCDD。切记此种模式的是栈顶复用就好了。
那么singleTop启动模式,在什么样的场景下会使用到呢?
这里举一个搜狐新闻App的例子,假设主界面为 MainActivity,显示新闻的界面是 DetailActivity,显然显示任何一条新闻都会使用 DetailActivity,即把新闻内容id通过 Intent 传给 DetailActivity 就可以了。通过回调onNewIntent方法,可以获取请求当前页面的数据。为什么这样的页面适合使用singleTop这样的启动模式去启动呢,还有一个原因,当我们收到文章推送的系统通知栏通知的时候,如果是多条推送,给用户的体验,自然不希望用户点开一条推送,就重新创建一个页面吧。比如极端情况下,这样如果用户点击了10条通知栏推送,并进入到App,每次都重新创建了一个新的Activity,当用户要回到主页面的时候,用户可是要点击10次返回键。
使用场景结论:singleTop启动模式,适用于一些文章类的内容呈现页面,为了解决用户页面交互可能需要多次按返回键回到主页的场景(这种场景的发生,往往和系统通知栏打开页面有关,当然还有可能是特定的交互设计,希望快速回到任务栈中的特定页面),我们使用singleTop模式。
另外,启动模式的应用是灵活的,和我们文章开头的结论一致,大多数情况下,是为了解决用户页面交互更加友好而设计。
再举一个例子,假如是一个社交类的app,此app也会收到通知栏通知,点击通知栏通知会进入到好友的对方空间,当然也会出现上述网易新闻的交互问题,此时我们的所设计的对方空间页,也就可以设计成singleTop启动模式了。还有一些操作结果回调页面,比如微信支付回调页面,也是singleTop启动模式,微信支付的回调页面,是微信支付结果的数据呈现页面,为了规避多次回调(有可能是异常),出现了多个支付回调页面,交互体验也并不友好,那么设计singleTop启动模式,即使出现上述异常,用户感知的也只是页面上数据状态的变化。
使用singleTop模式,在我们实际的应用还有一个场景,比如解决重复点击,打开了相同的Activity页面,当然为了保证业务正常,我们也同时同时处理onNewIntent的回调方法,保证页面的业务数据正常。
3、singleTask栈内复用模式
栈内复用模式是一种单实例模式,在这种模式下,只要Activity在一个栈中存在,多次启动Activity都不会去重新创建Activity的实例。和SingleTop一样,系统也会回调onNewIntent的方法。下面通过表格的方式,来看一下以下几种情况,其中D为需要启动的singleTask模式的Activity:
序号 | 所需任务栈 | 启动前任务栈 | 启动D | 启动后任务栈 |
---|---|---|---|---|
1 | 无 | 创建任务栈S1,栈内无其它Activity实例 | 创建D并压入栈S1中 | D |
2 | 有 | S1任务栈中,ABC,无D | 创建D并压入栈S1中 | ABCD |
3 | 有 | S1任务栈中,ABCD | 不创建 | ABCD |
4 | 有 | S1任务栈中,ADBC | 不创建 | AD |
通过上述表格,基本能够概括singleTask模式在不同的情形下被启动后,是否要创建任务栈,是否要创建实例,以及singleTask模式的启动Activity后,对原有任务栈中Activity数据影响。
这里我们重点看一下序号4情形,为什么启动D后,任务栈中的BC消失了。很多同学,应该看过一些文章说,因为singleTask模式默认具有clearTop的效果,会导致栈内所有在D上面的Activity全部消失。首先这个观点的确是对的,我在这里只是想强调一下因果关系,在本篇文章standard标准模式介绍部分的第一段最后一句话,我们要深刻理解栈的数据结构的”后进先出“的特性。序号4情形出现最终AD的结果,是由于如果需要把ADBC栈中的D移到栈顶,则必须让BC先出栈,才能使得D停留在栈顶上。
所以是由于singleTask模式本身的实现+栈数据结构的特性导致了singleTask模式具有了clearTop的效果。
使用场景结论:如果在应用的整个存活生命周期中,希望当前页面交互的Activity只有一个,我们可以选择使用singleTask启动模式,一句话,如果希望当前Activity在当前应用内唯一,使用singleTask模式。
具体举几个场景的例子:
场景案例1:应用的主界面一般可以设计成singleTask模式,一般主页面的设计都是一个Activity加三四个tab页,tab页控制这三个Fragment。手机淘宝的主界面就设计成了singleTask模式,在文章的后半部分,还有对手机淘宝启动模式的代码层面的印证。
场景案例2:有些app有这样的一种设计,首先有一个登录页面LoginActivity-A包含账号、密码输入框,登录按钮,还有注册按钮,用户点注册按钮会进入到注册流程中,整个注册流程是有2个页面完成,比如选择性别昵称页面B(B页面支持返回到上一步,进行登录操作)->二次密码确认页面C(C页面支持返回到上一步,修改性别和昵称)>C页面点击注册,注册成功后,回到登录LoginActivity页面A,要求用户进行主动登录。那么这整个流程的任务栈情况是ABC,然后C页面点击注册成功后,进入登录A页面,那么如果A标准模式,当前任务栈应该是ABCA,这样应用程序的返回栈就奇怪了,此时页面交互层面是希望只有登录A页面,所以此时的登录A页面就可以设计singleTask模式,来解决上述场景的页面交互问题。
案例2在我们的实际开发中,并不算是常见的交互场景,很多app注册成功就直接进入到app的主页面了,那么通过案例2的场景呢,再一次强调了,如果希望一个页面Activity在应用内唯一,可以选择设计成singleTask设计模式。
4、singleInstance单实例模式
singleInstance单实例模式,是一种加强的singleTask模式,那么它除了具备singleTask模式的所有特性以外,加强的部分是,具有此种模式的Activity只能单独位于一个任务栈中。比如A是singleInstance模式Activity,当A启动后,系统会创建一个新的任务栈,然后A独自在这个新的任务栈中,由于仍然具有栈内复用的特性,后续都不会重新创建新的Activity,除非这个独特的任务栈被系统销毁了。
使用场景结论:
希望整个android手机系统全局唯一页面,我们使用此种模式。在我的实际工作中,singleInstance的场景基本没有用到。这里面截取网上大家可能都能搜到的一个场景案例:
你以前设置了一个闹铃:上午6点。在上午5点58分,你启动了闹铃设置界面,并按 Home 键回桌面;在上午5点59分时,你在微信和朋友聊天;在6点时,闹铃响了,并且弹出了一个对话框形式的 Activity(名为 AlarmAlertActivity) 提示你到6点了(这个 Activity 就是以 SingleInstance 加载模式打开的),你按返回键,回到的是微信的聊天界面,这是因为 AlarmAlertActivity 所在的 Task 的栈只有他一个元素,因此退出之后这个 Task 的栈空了。如果是以 SingleTask 打开 AlarmAlertActivity,那么当闹铃响了的时候,按返回键应该进入闹铃设置界面。
我们在研究一个技术的知识点的时候,一定不能离开场景,所以在前面的四个启动模式的分析结尾,都给出了使用场景结论,那么最后我们还是来看一下,一些我们常用的应用,在启动模式上的应用。
首先我们看一下launchMode在android api 28中的定义:
/**
* Constant corresponding to standard
in
* the {@link android.R.attr#launchMode} attribute.
*/
public static final int LAUNCH_MULTIPLE = 0;
/**
* Constant corresponding to singleTop
in
* the {@link android.R.attr#launchMode} attribute.
*/
public static final int LAUNCH_SINGLE_TOP = 1;
/**
* Constant corresponding to singleTask
in
* the {@link android.R.attr#launchMode} attribute.
*/
public static final int LAUNCH_SINGLE_TASK = 2;
/**
* Constant corresponding to singleInstance
in
* the {@link android.R.attr#launchMode} attribute.
*/
public static final int LAUNCH_SINGLE_INSTANCE = 3;
再看一下微信最新版本2019月11月19日在应用包下载的apk的清单文件中部分代码:
<activity
android:theme="@ref/0x7f0d009d"
android:label="@ref/0x7f0a1380"
android:name="com.tencent.mm.ui.LauncherUI"
android:launchMode="1"
android:configChanges="0xda0"
android:windowSoftInputMode="0x32">
...
activity>
LanucherUI 是微信主界面的Activity,其启动模式的是1,1代表的singleTop启动模式。
下面的代码是截取自淘宝的apk清单文件中的部分代码:
<activity
android:theme="@ref/0x7f11039c"
android:name="com.taobao.tao.TBMainActivity"
android:launchMode="2"
android:screenOrientation="1">
<intent-filter
android:label="@ref/0x7f101218">
<action
android:name="android.intent.action.VIEW" />
<category
android:name="android.intent.category.DEFAULT" />
<category
android:name="android.intent.category.BROWSABLE" />
....
intent-filter>
TBMainActivity 是手机淘宝的主界面Activity,其启动模式是2,2代表singleTask启动模式。
淘宝的主界面的确设置成了singleTask模式,但是微信的主界面并没有像我们前面分析的结论一样,而是使用的singleTop模式,可能有什么其他特定的原因,我们在设置主界面的启动模式时,仍然可以遵循我们前面分析的结论,应用的主界面,在应用程序的生命周期中,只希望有一个,就可以选择使用singleTask启动模式。
最终,我们呼应文章开始给出的结论:Activity的启动模式的设计的主要目的是为了解决应用页面交互需求的不同场景。在我们实际的场景应用中主要解决
1、App需要生成给其它app调用Activity,例如浏览器应用,照相机应用
2、解决快速点击造成的多个重复页面的交互问题
3、任务栈过深,解决按返回键回到指定页面的问题
总之,巧妙的使用启动模式,能够帮我们解决页面交互问题提供便捷的解决方案。