在介绍 Android 启动模式之前,先介绍两个概念,一个是 task ,另一个是在 Androidmanifest 文件中 <activity> 的一个重要属性, taskAffinity 。
- task:翻译过来就是“任务”,是一组相互有关联的 activity 集合,可以理解为 Activity 是在 task 里面活动的。 task 存在于一个称为 back stack 的数据结构中,也就是说, task 是以栈的形式去管理 activity 的,所以也叫可以称为“任务栈”。
- taskAffinity:官方文档解释是:"The task that the activity has an affinity for.",可以翻译为 activity 相关或者亲和的任务,这个参数标识了一个 Activity 所需要的任务栈的名字。默认情况下,所有Activity所需的任务栈的名字为应用的包名。 taskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用。
先初浅地了解一下这两个概念,接下来会对它们进行详细的解读。
Android有以下四种启动模式:
-
standard:标准模式,也是系统默认的启动模式。在该模式下,
- 假如 activity A 启动了 activity B , activity B 则会运行在 activity A 所在的任务栈中。
- 而且每次启动一个 Activity ,都会重新创建新的实例,不管这个实例在任务中是否已经存在。
- 非 Activity 类型的 context (如 ApplicationContext )启动 standard 模式的 Activity 时会报错。非 Activity 类型的 context 并没有所谓的任务栈,由于上面第 1 点的原因所以系统会报错。此解决办法就是为待启动 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记位,这样启动的时候系统就会为它创建一个新的任务栈。这个时候待启动 Activity 其实是以 singleTask 模式启动的。
示例如下,
<?xml version="1.0" encoding="utf-8"?>
<manifest package="com.chen.activitylaunchtest"
xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity"
android:taskAffinity="com.chen.main">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity"
android:launchMode="standard"></activity>
<activity android:name=".ThirdActivity"></activity>
</application>
</manifest>
- 示例程序的包名为: com.chen.activitylaunchtest ,所以所有 Activity 默认任务栈是应该就是 com.chen.activitylaunchtest 。
- MainActivity 的 taskAffinity 的属性值为 com.chen.main ,
我们打开 MainActivity ,再从 MainActivity 跳转到 SecondActivity ,并启动 SecondActivity 两次。得到任务栈活动如下图:
可以看出,
- 当 MainActivity 启动时, MainActivity 的 taskAffinity 指定为 com.chen.main ,所以上图任务栈的名称为 com.chen.main 。
- 又从 MainActivity 中启动 SecondActivity ,上面有说过, activity A 启动了 activity B , activity B 则会运行在 activity A 所在的任务栈中。所以 SecondActivity 会在 MainActivity 的任务栈中启动。
- 因为在 standard 模式下,每次启动一个 Activity ,都会重新创建新的实例,不管这个实例在任务中是否已经存在,所以任务栈中有三个 SecondActivity 的实例。
singleTop: 栈顶复用模式。假如 activity A 启动了 activity B ,就会判断 A 所在的任务栈栈顶是否是 B 的实例。如果是,则不创建新的 activity B 实例而是直接引用这个栈顶实例,同时 onNewIntent 方法会被回调,通过该方法的参数可以取得当前请求的信息;如果不是,则创建新的 activity B 实例。
-
singleTask:栈内复用模式。
其实要达到官方所说的效果,需要满足一定条件的。那就是需要设置 taskAffinity 属性。前面也说过了, taskAffinity 属性是和 singleTask 模式搭配使用的。具体分析请参考解开Android应用程序组件Activity的"singleTask"之谜。 singleInstance:单实例模式。这个是 singleTask 模式的加强版,它除了具有 singleTask 模式的所有特性外,它还有一点独特的特性,那就是此模式的 Activity 只能单独地位于一个任务栈,不与其他 Activity 共存于同一个任务栈。
在一开始还提到, taskAffinity 属性一般和 singleTask 模式或者 allowTaskReparenting 属性配对使用。 taskAffinity 属性和 allowTaskReparenting 属性配对使用是一个怎样的工作过程呢?场景如下:
比如现在有 2 个应用 A 和 B , A 启动了 B 的一个 Activity C , Activity C 的 allowTaskReparenting 属性为 true ,然后按 Home 键回到桌面,然后再单击 B 的桌面图标,这个时候并不是启动了 B 的主 Activity ,而是重新显示了已经被应用 A 启动的 Activity C ,或者说, C 从 A 的任务栈转移到了 B 的任务栈中。
可以这么理解,由于 A 启动了 C ,这个时候 C 只能运行在 A 的任务栈中,但 C 属于 B 应用,正常情况下,它的 taskAffinity 属性值肯定不可能和 A 的任务栈相同(因为包名不同)。所以当 B 启动后, B 会创建自己的任务栈,这个时候系统发现 C 的原本所想要的任务栈已经被创建了,所以把 C 从 A 的任务栈中转移过来。
两种方法能给 Activity 指定启动模式:
- 通过 AndroidMenifest 给 Activity 指定启动模式:
<activity android:name=".SecondActivity"
android:launchMode="singleTask"></activity>
- 通过在 Intent 中设置标志位来为 Activity 指定启动模式:
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
常用的 Activity 的 Flags 有:
- FLAG_ACTIVITY_NEW_TASK:
这个标记位的作用是为 Activity 指定 “singleTask” 启动模式,其效果和在 XML 中指定该启动模式相同。 - FLAG_ACTIVITY_SINGLE_TOP:
这个标记位的作用是为 Activity 指定 “singleTop” 启动模式,其效果和在 XML 中指定该启动模式相同。 - FLAG_ACTIVITY_CLEAR_TOP:
具有此标志位的 Activity ,当它启动时,在同一个任务栈中所有位于它上面的 Activity 都要出栈。这个模式一般需要和 FLAG_ACTIVITY_NEW_TASK 配合使用。 singleTask 启动模式默认具有此标志位的效果。 - FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:
具有这个标记的 Activity 不会出现在历史 Activity 的列表中,它等同于在 XML 中指定 Activity 的属性 android:excludeFromRecents="true" 。
**显式启动 Activity **
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
**隐式启动 Activity **
需要在 AndroidMenifest 里面为 Activity 设置 IntentFilter 匹配规则,并且intent也要添加过滤器。
<activity android:name=".SecondActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<data android:scheme="http"
android:pathPattern=".*\\.jpg"
android:mimeType="image/jpg">
</data>
</intent-filter>
</activity>
Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.setDataAndType(Uri.parse("http://www.baidu.com/search/info/hello.jpg"), "image/jpg");
startActivity(intent);
** IntentFilter 的匹配规则**
- IntentFilter 中的过滤信息有 action 、 category 、 data 。一个过滤列表中的 action 、 category 、 data 可以有多个。
- 一个 Activity 中可以有多个 intent-filter ,一个 Intent 只要能匹配任何一组 intent-filter 即可成功启动对应的 Activity 。
- ** action 的匹配规则**:
action 是字符串,系统预定义了一些 action ,同时我们也可以在应用中自定义自己的 action 。 action 的匹配规则是:
要求 Intent 中的 action 必须存在,而且必须和 <intent-filter> 过滤规则中的其中一个 action 相同; action 还区分大小写。 - ** category 的匹配规则**:
category 是字符串,系统预定义了一些 category ,同时我们也可以在应用中自定义自己的 category 。 category 的匹配规则是:
- 要求 Intent 可以没有 category ,如果没有 category 的话,系统会默认为 intent 加上 "android.intent.category.DEFAULT" 这个 category 。
- 但是如果一旦有 category ,不管有几个,每个都要能够和过滤规则中的任何一个 category 相同。
- 为了 activity 能够接收隐式调用,就必须在 intent-filter 中指定 "android.intent.category.DEFAULT" 这个 category 。
- ** data 的匹配规则**:
data 的匹配规则与 action 类似, data 的语法如下:
<data android:scheme="string" android:host="string" android:post="string" android:path="string" android:pathPattern="string" android:pathPrefix="string" android:mimeType="string" />
- ** action 的匹配规则**:
data 由两部分组成, mimeType 和 URL 。
- mimeType 指媒体类型,比如 image/jpeg 和 video 等,可以表示图片、文本、视频等不同的媒体格式。
- URL 的结构
><scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
例如:**```http://www.baidu.com:80/search/info/hello.jpg```**
Scheme: URI 的模式,比如 http ,file,content 等,如果 URI 中没有指定 scheme ,那么整个 URI 无效。在上述例子里,scheme="http"。
Host:URI 的主机名,比如 www.baidu.com,如果 URI 没有指定 host,那么整个 URI 无效。在上述例子里,host="www.baidu.com"。
Port:URI 中的端口号,比如 80。在上述例子里,port="80"。
Path:表示完整的路径信息。在上述例子里,path="/search/info"。
pathPrefix:表示路径的前缀信息。在上述例子里,pathPrifix="/search"。
pathPattern:也可以表示完整的路径信息,但是它里面可以包含通配符。在上述例子里,pathPattern="\.\*\\\\.jpg"
- 匹配符号:"\*" 用来匹配0次或更多,如:"a\*" 可以匹配"a"、"aa"、"aaa"...
- "\." 用来匹配任意字符,如:"\." 可以匹配 "a"、"b","c"...
- "\.\*" 就是用来匹配任意字符0次或更多,如:"\.\*chen" 可以匹配 "chen"、"androidchen" ...
- 转义:因为当读取 Xml 的时候,"\\" 是被当作转义字符的(当它被用作 pathPattern 转义之前),因此这里需要两次转义,读取 Xml 是一次,在 pathPattern 中使用又是一次。如:"\*" 这个字符就应该写成 "\\\\*","\\" 这个字符就应该写成 “\\\\\\\\”。
- 例子: 如果我们想要匹配 http 以 ".jpg" 结尾的路径,使得别的程序想要打开网络 jpg 时,用户能够可以选择我们的程序进行下载查看。我们可以将 scheme 设置为 "http",pathPattern 设置为 "\.\*\\\\.jpg",整个 intent-filter 设置为:
```XML
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<data android:scheme="http" android:pathPattern=".*\.jpg"></data>
</intent-filter>
如果你只想处理某个站点的 jpg,那么在 data 标签里增加
android:host="yoursite.com"则只会匹配
http://yoursite.com/xxx/xxx.jpg```,但这不会匹配www.yoursite.com,如果你也想匹配这个站点的话,你就需要再添加一个 data 标签,除了 android:host 改为 "www.yoursite.com" 其他都一样。
- 要注意的事项:
- 如果要为 Intent 指定完整的 data,必须要调用 setDataAndType 方法,不能用 setData 再调用 setType,因为这两个方法彼此会清除对方的值。
```java
intent.setDataAndType (Uri.parse ("http://www.baidu.com/search/info/hello.jpg"), "image/jpg");
```
- Intent-filter 的匹配规则对于 Service 和 BroadcastReceiver 也是同样的道理,不过系统对于 Service 的建议是尽量使用显式调用方式来启动服务。
- 通过隐式方式启动一个 Activity 时,可以采用 PackageManager 的 resolveActivity 方法或者 Intent 的 resolveActivity 方法先做一下判断,判断是否有 Activity 能匹配隐式 Intent。
```java
Intent intent = new Intent();
intent.setAction("android.intent.action.VIEW");
intent.setDataAndType(Uri.parse("http://www.baidu.com/search/info/hello.jpg"), "image/jpg");
if (intent.resolveActivity(getPackageManager()) == null)
Log.d("LaunchTest", "Go to SecondActivity unsuccessfully.");
else
startActivity(intent);
```