Android开发艺术探索知识回顾——第1章 Activity的生命周期和启动模式:2、启动模式

 

1.2 Activity的启动模式

上一节介绍了 Activity 在标准情况下和异常情况下的生命周期,我们对Activity的生命周期应该有了深入的了解。除了 Activity 的生命周期外,Activity 的启动模式也是一个难点, 原因是形形色色的启动模式和标志位实在是太容易被混淆了,但是Activity作为四大组件之首,它的的确确非常重要,有时候为了满足项目的特殊需求,就必须使用 Activity 的启动模式,所以我们必须要搞清楚它的启动模式和标志位,本节将会一一介绍。

 

1.2.1 Activity 的 LaunchMode

首先说一下 Activity 为什么需要启动模式。我们知道,在默认情况下,当我们多次启动同一个Activity的时候,系统会创建多个实例并把它们一一放入任务栈中,当我们单击 back 键,会发现这些Activity会一一回退。任务栈是一种“后进先出”的栈结构,这个比较好理解,每按一下 back 键就会有一个 Activity 出栈,直到栈空为止,当栈中无任何 Activity 的时候,系统就会回收这个任务栈

关于任务栈的系统工作原理,这里暂时不做说明,在后续章节会专门介绍任务栈。知道了 Activity 的默认启动模式以后,我们可能就会发现一个问题:多次启动同一个 Activity,系统重复创建多个实例,这样不是很傻吗?这样的确有点傻,Android在设计的时候不可能不考虑到这个问题,所以它提供了启动模式来修改系统的默认行为。目前有四种启动模式:standard、singleTop、singleTask 和 singlelnstance,下面先介绍各种启动模式的含义:

 

(1) standard:标准模式,这也是系统的默认模式。

每次启动一个 Activity 都会重新创建一个新的实例,不管这个实例是否已经存在。被创建的实例的生命周期符合典型情况下 Activity 的生命周期,如上节描述,它的 onCreate、onStart、onResume 都会被调用。这是一种典型的多实例实现,一个任务栈中可以有多个实例,每个实例也可以属于不同的任务栈。

在这种模式下,谁启动了这个 Activity,那么这个Activity 就运行在启动它的那个 Activity 所在的栈中。比如 Activity A 启动了 Activity B (B是标准模式),那么 B 就会进入到 A 所在的栈中。不知道读者是否注意到,当我们用 ApplicationContext 去启动 standard 模式的 Activity 的时候会报错,错误如下:

E/AndroidRuntime(674): android.util.androidruntiomException: Calling startActivity from 
outside of an Activity context requires the FLAG_ACTIVITY_TASK flag . Is this really what are want?

相信这句话读者一定不陌生,这是因为 standard 模式的 Activity 默认会进入启动它的 Activity 所属的任务栈中,但是由于非 Activity 类型的 Context (如ApplicationContext)  并没有所谓的任务栈,所以这就有问题了。

解决这个问题的方法是:为待启动 Activity 指定 FLAG_ACTIVITY_NEW_TASK 标记位,这样启动的时候就会为它创建一个新的任务栈,这个时候待启动 Activity实际上是以 singleTask 模式启动的,读者可以仔细体会。

 

(2) singleTop:栈顶复用模式。

在这种模式下,如果新 Activity 已经位于任务栈的栈顶,那么此 Activity 不会被重新创建,同时它的 onNewIntent 方法会被回调通过此方法的参数我们可以取出当前请求的信息。需要注意的是,这个 Activity 的 onCreate、onStart 不会被系统调用,因为它并没有发生改变。如果新 Activity 的实例已存在但不是位于栈顶,那么新 Activity 仍然会重新重建。

举个例子,假设目前栈内的情况为 ABCD,其中 ABCD 为四个Activity,A位于栈底,D位于栈顶,这个时候假设要再次启动D,如果D的启动模式为singleTop,那么栈内的情况仍然为ABCD;如果D的启动模式为standard,那么由于D被重新创建,导致栈内的情况就变为ABCDD。

 

(3) singleTask:栈内复用模式。

这是一种单实例模式,在这种模式下,只要 Activity 在一个栈中存在,那么多次启动此 Activity 都不会重新创建实例,和 singleTop 一样,系统也会回调其onNewIntent。

具体一点,当一个具有 singleTask 模式的 Activity 请求启动后,比如Activity A,系统首先会寻找是否存在 A 想要的任务栈。如果不存在,就重新创建一个任务栈,然后创建 A 的实例后把 A 放到栈中。

如果存在 A 所需的任务栈,这时要看 A 是否在栈中有实例存在,如果有实例存在,那么系统就会把 A 调到栈顶并调用它的 onNewIntent 方法。如果实例不存在,就创建 A 的实例并把 A 压入栈中。举几个例子:

  • 比如目前任务栈 S1 中的情况为 ABC,这个时候 Activity D 以 singleTask 模式请求启动,其所需要的任务栈为 S2,由于 S2 和 D 的实例均不存在,所以系统会先创建任务栈 S2,然后再创建 D 的实例并将其入栈到 S2。
  • 另外一种情况,假设 D 所需的任务栈为 S1,其他情况如上面例子 1 所示,那么由于 S1 已经存在,所以系统会直接创建 D 的实例并将其入栈到 S1。
  • 如果 D 所需的任务栈为 S1,并且当前任务栈 S1 的情况为 ADBC,根据栈内复用的原则,此时 D 不会重新创建,系统会把 D 切换到栈顶并调用其 onNewIntent 方法, 同时由于 singleTask 默认具有 clearTop 的效果,会导致栈内所有在 D 上面的 Activity 全部岀栈,于是最终 S1 中的情况为 AD。这一点比较特殊,在后面还会对此种情况详细地分析。

通过上述 3 个例子,读者应该能比较清晰地理解 singleTask 的含义了。

 

(4) singlelnstance:单实例模式。

这是一种加强的 singleTask 模式,它除了具有 singleTask 模式的所有特性外,还加强了一点,那就是具有此种模式的 Activity 只能单独地位于一个任务栈中,换句话说,比如 Activity A 是 singlelnstance 模式,当 A 启动后,系统会为它创建一个新的任务栈,然后 A 独自在这个新的任务栈中,由于栈内复用的特性,后续的请求均不会创建新的 Activity,除非这个独特的任务栈被系统销毁了。

 

上面介绍了几种启动模式,这里需要指出一种情况:我们假设目前有 2 个任务栈,前台任务栈的情况为AB,而后台任务栈的情况为CD,这里假设 CD 的启动模式均为singleTask。

现在请求启动 D,那么整个后台任务栈都会被切换到前台,这个时候整个后退列表变成了 ABCD。当用户按 back 键的时候,列表中的 Activity 会出栈,如图1-7所示。如果不是请求启动 D 而是启动 C,那么情况就不一样了,请看图1-8,具体原因在本节后面会再进行详细分析。

  

 

什么是 Activity 所需要的任务栈呢?

另外一个问题是,在 singleTask 启动模式中,多次提到某个 Activity 所需的任务栈,什么是 Activity 所需要的任务栈呢?这要从一个参数说起:TaskAffinity,可以翻译为任务相关性。这个参数标识了一个Activity所需要的任务栈的名字,默认情况下,所有Activity 所需的任务栈的名字为应用的包名。

当然,我们可以为每个Activity都单独指定 TaskAffinity 属性,这个属性值必须不能和包名相同,否则就相当于没有指定。TaskAffinity 属性主要和 singleTask 启动模式或者 allowTaskReparenting 属性配对使用,在其他情况下没有意义。

另外,任务栈分为前台任务栈和后台任务栈,后台任务栈中的 Activity 位于暂停状态,用户可以通过切换将后台任务栈再次调到前台。当 TaskAffinity 和 singleTask 启动模式配对使用的时候,它是具有该模式的 Activity 的目前任务栈的名字,待启动的 Activity 会运行在名字和 TaskAffinity 相同的任务栈中。

当 TaskAffinity 和 allowTaskReparenting 结合的时候,这种情况比较复杂,会产生特殊的效果。当一个应用 A 启动了应用 B 的某个Activity 后,如果这个 Activity 的 allowTaskReparenting 属性为 true 的话,那么当应用 B 被启动后,此 Activity 会直接从应用 A 的任务栈转移到应用 B 的任务栈中。

这还是很抽象,再具体点,比如现在有2个应用 A 和 B,A 启动了 B 的一个Activity C,然后按Home 键回到桌面,然后再单击 B 的桌面图标,这个时候并不是启动了 B 的主 Activity。而是重新显示了已经被应用 A 启动的 Activity C,或者说,C 从 A 的任务栈转移到了 B 的任务栈中。

可以这么理解,由于 A 启动了 C,这个时候 C 只能运行在 A 的任务栈中,但是 C 属于 B 应用,正常情况下,它的 TaskAffinity 值肯定不可能和 A 的任务栈相同(因为包名不同)。所以,当 B 被启动后,B 会创建自己的任务栈,这个时候系统发现 C 原本所想要的任务栈已经被创建了,所以就把 C 从 A 的任务栈中转移过来了。这种情况读者可以写个例子测试一下,这里就不做示例了。

 

如何给Activity指定启动模式呢?

如何给Activity指定启动模式呢?有两种方法,第一种是通过 AndroidMenifest 为 Activity 指定启动模式,如下所示。

另一种情况是通过在 Intent 中设置标志位来为 Activity 指定启动模式,比如:

这两种方式都可以为 Activity 指定启动模式,但是二者还是有区别的。首先,优先级上,第二种方式的优先级要高于第一种,当两种同时存在时,以第二种方式为准;其次,上述两种方式在限定范围上有所不同,比如,第一种方式无法直接为 Activity 设定 FLAG_ACTIVITY_CLEAR_TOP 标识,而第二种方式无法为 Activity 指定 singlelnstance 模式。

 

体验启动模式——代码示例

关于 Intent 中为 Activity 指定的各种标记位,在下面的小节中会继续介绍。下面通过一个例子来体验启动模式的使用效果。还是前面的例子,这里我们把 MainActivity 的启动模式设为 singleTask,然后重复启动它,看看是否会重复创建,代码修改如下:

1、把 MainActivity 的启动模式设为 singleTask

<activity
   android:name="com.yyh.demo4.launchMode.MainActivity"
   android:configChanges="orientation|screenSize"
   android:label="@string/app_name" 
   android:launchMode="singleTask"
   >
   <intent-filter>
       <action android:name="android.intent.action.MAIN" />
       <category android:name="android.intent.category.LAUNCHER" />
   </intent-filter>
</activity>

 

2、MainActivity里面添加如下代码:

findViewById(R.id.button1).setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent();
        intent.setClass(MainActivity.this, MainActivity.class);
        intent.putExtra("time", System.currentTimeMillis());
        startActivity(intent);
    }
});


@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    Log.d(TAG, "onNewIntent, time=" + intent.getLongExtra("time", 0));
}

根据上述修改,我们做如下操作,连续单击三次按钮启动3次 MainActivity,算上原本的 MainActvity 的实例,正常情况下,任务栈中应该有4个 MainActivity 的实例,但是我们为其指定了 singleTask 模式,现在来看一看到底有何不同。

 

执行 adb shell dumpsys activity 命令分析

mac 上打开 terminal终端,输入命令 adb shell dumpsys activity,

command + f,搜索关键字:ACTIVITY MANAGER ACTIVITIES

具体日志如下:

从上面导出的 Activity 信息可以看出,尽管启动了 4 次 MainActivity,但是它始终只有一个实例在任务栈中。从图1-9的 log 可以看出,Activity 的确没有重新创建,只是暂停了一 下,然后调用了onNewIntent,接着调用 onResume 就又继续了。

现在我们去掉singleTask,再来对比一下,还是同样的操作,单击三次按钮启动 MainActivity 三次。

执行 adb shell dumpsys activity 命令:

mac 上打开 terminal终端,输入命令 adb shell dumpsys activity,

command + f,搜索关键字:ACTIVITY MANAGER ACTIVITIES

导出信息很多,我们可以有选择地看,比如就看 Running activities (most recent first)这一块,如下所示。

我们能够得出目前总共有 2 个任务栈,前台任务栈的 taskAffinity 值为 com.ryg.chapter_1,它里面有 4 个 Activity,后台任务栈的 taskAffinity 值为 com.mumu.launcher,它里面有1个Activity,这个Activity就是桌面。( 注意:后台任务栈的 taskAffinity 值不是 com.android.launcher,因为测试用的网易MuMu模拟器,所以是 com.mumu.launcher )

通过这种方式来分析任务栈信息就清晰多了。从上面的导出信息中可以看到,在任务栈中有 4 个MainActivity,这也就验证了 Activity 的启动模式的工作方式。

 

 singleTask 特殊情况说明

上述四种启动模式,standard 和 singleTop 都比较好理解,singlelnstance 由于其特殊性也好理解,但是关于 singleTask 有一种情况需要再说明一下。如图1-7所示,如果在 Activity B 中请求的不是 D 而是 C,那么情况如何呢?这里可以告诉读者的是,任务栈列表变成了 ABC,是不是很奇怪呢? Activity D 被直接出栈了。

  

 

修改代码进行说明

下面我们再用实例验证看看是不是这样。 首先,还是使用上面的代码,但是我们做一下修改:

        <activity
            android:name="com.yyh.demo5.singleTask.MainActivity"
            android:configChanges="orientation|screenSize"
            android:label="@string/app_name"
            android:launchMode="standard" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <activity
            android:name="com.yyh.demo5.singleTask.SecondActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:taskAffinity="com.ryg.task1" 
            android:launchMode="singleTask"/>
        
        <activity
            android:name="com.yyh.demo5.singleTask.ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:taskAffinity="com.ryg.task1" 
            android:launchMode="singleTask"/>

 

我们将 SecondActivity 和 ThirdActivity 都设成 singleTask 并指定它们的 taskAffinity 属性为“com.ryg.task1”,注意这个taskAffinity属性的值为字符串,且中间必须含有包名分隔符“.”。

然后做如下操作,在 MainActivity 中单击按钮启动 SecondActivity,在 SecondActivity 中单击按钮启动 ThirdActivity,在ThirdActivity 中单击按钮又启动 MainActivity,最后再在 MainActivity 中单击按钮启动 SecondActivity,现在按back键,然后看到的是哪个Activity?答案:是回到桌面。是不是有点摸不到头脑了?没关系,接下来我们分析这个问题。

 

从理论上分析这个问题

首先,从理论上分析这个问题,先假设 MainActivity 为ASecondActivity 为B,ThirdActivity 为C。我们知道 A 为standard模式,按照规定,A 的taskAffinity 值继承自 Application 的 taskAffinity,而 Application 默认 taskAffinity 为包名,所以 A 的 taskAffinity 为包名。由于我们在 XML 中为 B 和 C 指定了 taskAffinity 和启动模式,所以 B 和 C 是 singleTask 模式且有相同的 taskAffinity 值“com.ryg.task1”。

A 启动 B 的时候,按照 singleTask 的规则,这个时候需要为 B 重新创建一个任务栈“com.ryg.task1”。B 再启动 C,按照singleTask 的规则,由于C所需的任务栈(和B为同一任务栈)已经被B创建,所以无须再创建新的任务栈,这个时候系统只是创建 C 的实例后将C入栈了。

接着 C 再启动 A,A 是standard 模式,所以系统会为它创建一个新的实例,并将它加到启动它的那个 Activity 的任务栈,由于是 C 启动了 A,所以 A 会进入 C 的任务栈中并位于栈顶。这个时候己经有两个任务栈了,一个是名字为包名的任务栈,里面只有 A,另一个是名字为“com.ryg.task1”的任务栈,里面的 Activity 为BCA。

接下来,A 再启动 B,由于 B 是 singleTask,B 需要回到任务栈的栈顶,由于栈的工作模式为“后进先出”,B想要回到栈顶,只能是CA出栈。所以,到这里就很好理解了,如果再按back键,B 就出栈了,B 所在的任务栈已经不存在了,这个时候只能是回到后台任务栈并把 A 显示出来。

注意这个 A 是后台任务栈的 A,不是 "com.ryg.task1”任务栈的 A,接着再继续back,就回到桌面了。分析到这里,我们得岀一条结论,singleTask 模式的 Activity 切换到栈顶会导致在它之上的栈内的 Activity 出栈。

 

釆用dumpsys命令验证说明

接着我们在实践中再次验证这个问题,还是釆用dumpsys命令。我们省略中间的过程,直接看 C 启动 A 的那个状态,执行 adb shell dumpsys activity 命令,

mac 上打开 terminal终端,输入命令 adb shell dumpsys activity,

command + f,搜索关键字:ACTIVITY MANAGER ACTIVITIES

查看关键信息 Running activities (most recent first)

日志如下:

可以清楚地看到有 2 个任务栈,第一个(com.ryg.chapter_1)只有 A,第二个(com.ryg.task1) 有BCA,就如同我们上面分析的那样,然后再从 A 中启动 B ,再看一下日志

可以发现在任务栈 com.ryg.task1 中只剩下 B 了,C、A 都已经出栈了。这个时候再按 back 键,任务栈 com.ryg.chapter_1 中的 A 就显示出来了,如果再 back 就回到桌面了。分析到这里,相信读者对 Activity 的启动模式已经有很深入的理解了。

注意:

1、用网易的MuMu模拟器进行测试,在点击跳转过程中,会出现黑屏现象,所以改用真机测试了!

2、其他任务栈为手机系统自带的任务栈,这块可以忽略!

下面介绍Activity中常用的标志位。

 

 

1.2.2 Activity 的 Flags

Activity 的 Flags 有很多,这里主要分析一些比较常用的标记位。标记位的作用很多,有的标记位可以设定 Activity 的启动模式,比如FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_SINGLE_TOP等;还有的标记位可以影响 Activity 的运行状态,比如 FLAG_ACTIVITY_CLEAR_TOP 和 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 等。 下面主要介绍几个比较常用的标记位,剩下的标记位读者可以查看官方文档去了解,大部分情况下,我们不需要为 Activity 指定标记位,因此,对于标记位理解即可。在使用标记位的时候,要注意有些标记位是系统内部使用的,应用程序不需要去手动设置这些标记位以防出现问题。

FLAG_ACTIVITY_NEW_TASK

这个标记位的作用是为 Activity 指定 “singleTask” 启动模式,其效果和在 XML 中指定该启动模式相同。

FLAG_ACTIVITY_SINGLE_TOP

这个标记位的作用是为 Activity 指定 “singleTop” 启动模式,其效果和在 XML 中指定该启动模式相同。

FLAG_ACTIVITY_CLEAR_TOP

具有此标记位的 Activity ,当它启动时,在同一个任务栈中所有位于它上面的 Activity 都要出栈。这个模式一般需要和FLAG_ACTIVITY_NEW_TASK 配合使用,在这种情况下,被启动 Activity 的实例如果已经存在,那么系统就会调用它的 onNewIntent。如果被启动的 Activity 釆用 standard 模式启动,那么它连同它之上的 Activity 都要出栈,系统会创建新的 Activity 实例并放入栈顶。通过1.2.1节中的分析可以知道,singleTask启动模式默认就具有此标记位的效果。

FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS

具有这个标记的 Activity 不会出现在历史 Activity 的列表中,当某些情况下我们不希望用户通过历史列表回到我们的 Activity 的时候这个标记比较有用,它等同于在 XML 中指定 Activity 的属性 android:excludeFromRecents="true"。

 

1.3 IntentFilter的匹配规则

我们知道,启动 Activity 分为两种,显式调用和隐式调用。二者的区别这里就不多说,显式调用需要明确地指定被启动对象的组件信息,包括包名和类名,而隐式调用则不需要明确指定组件信息。原则上一个 Intent 不应该既是显式调用又是隐式调用,如果二者共存的话以显式调用为主

显式调用很简单,这里主要介绍一下隐式调用。隐式调用需要 Intent 能够匹配目标组件的 IntentFilter 中所设置的过滤信息,如果不匹配将无法启动目标 Activity。IntentFilter 中的过滤信息有 action、category、data,下面是一个过滤规则的示例:

        <activity
            android:name="com.yyh.demo6.IntentFilter.ThirdActivity"
            android:configChanges="screenLayout"
            android:label="@string/app_name"
            android:taskAffinity="com.ryg.task1" 
            android:launchMode="singleTask">
            <intent-filter>
                <action android:name="com.ryg.charpter_1.c" />
                <action android:name="com.ryg.charpter_1.d" />
                <category android:name="com.ryg.category.c" />
                <category android:name="com.ryg.category.d" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
         	</intent-filter>
         </activity>

为了匹配过滤列表,需要同时匹配过滤列表中的 action、category、data 信息,否则匹配失败。一个过滤列表中的 action、category 和data 可以有多个,所有的 action、category、 data 分别构成不同类别,同一类别的信息共同约束当前类别的匹配过程。

只有一个 Intent 同时匹配action类别、category类别、data类别才算完全匹配,只有完全匹配才能成功启动目标 Activity。另外一点,一个 Activity 中可以有多个 intent-filter,一个 Intent 只要能匹配任何一组 intent-filter 即可成功启动对应的 Activity,如下所示。

         <activity android:name="ShareActivity">
            <!-- This activity handlers "SEND" actions with text data-->
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="text/plain" />
            </intent-filter>
            <!--This activity also handlers "SEND" and "SEND_MULTIPLE" with media data-->
            <intent-filter>
                <action android:name="android.intent.action.SEND" />
                <action android:name="android.intent.action.SEND_MULTIPLE" />
                <category android:name="android.intent.category.DEFAULT" />
                <data android:mimeType="application/vnd.google.panorama360+jpg"/>
                <data android:mimeType="image/*" />
                <data android:mimeType="video/*" />
            </intent-filter>
        </activity>

 

下面详细分析各种属性的匹配规则。

1. action的匹配规则

action 是一个字符串,系统预定义了一些 action,同时我们也可以在应用中定义自己的 action。action 的匹配规则是 Intent 中的 action 必须能够和过滤规则中的 action 匹配,这里说的匹配是指 action 的字符串值完全一样。

一个过滤规则中可以有多个 action,那么只要 Intent 中的 action 能够和过滤规则中的任何一个 action 相同即可匹配成功。针对上面的过滤规则,只要我们的 Intent 中 action 值为 “com.ryg.charpter_l.c” 或者 “com.ryg.charpter_ l.d” 都能成功匹配。需要注意的是,Intent中如果没有指定 action,那么匹配失败。

总结一下,action的匹配要求 Intent 中的 action 存在且必须和过滤规则中的其中一个 action 相同,这里需要注意它和 category 匹配规则的不同。另外,action 区分大小写,大小写不同字符串相同的 action 会匹配失败。

 

2. category的匹配规则

category 是一个字符串,系统预定义了一些 category,同时我们也可以在应用中定义自己的 category。category 的匹配规则和 action 不同,它要求 Intent 中如果含有 category,那么所有的 category 都必须和过滤规则中的其中一个 category 相同。换句话说,Intent 中如果出现了 category,不管有几个category,对于每个category来说,它必须是过滤规则中已经定义了的 category。

当然,Intent中可以没有 category,如果没有 category 的话,按照上面的描述,这个 Intent 仍然可以匹配成功。这里要注意下它和 action 匹配过程的不同,action 是要求 Intent 中必须有一个 action 且必须能够和过滤规则中的某个 action 相同,而 category 要求 Intent 可以没有category,但是如果你一旦有 category,不管有几个,每个都要能够和过滤规则中的任何一个 category 相同。

为了匹配前面的过滤规则中的 category,我们可以写出下面的 Intent,intent.addcategory ("com.ryg.category.c") 或者 Intent.addcategory ("com.ryg. category.d")  亦或者不设置 category。

为什么不设置 category 也可以匹配呢?

原因是系统在调用 startActivity 或者 startActivityForResult 的时候会默认为 Intent 加上 " android.intent.category.DEFAULT " 这个 category,所以这个 category 就可以匹配前面的过滤规则中的第三个 category。同时,为了我们的activity能够接收隐式调用,就必须在 intent-filter 中指定 "android.intent.category.DEFAULT" 这个 category,原因刚才已经说明了。

 

3. data的匹配规则

data 的匹配规则和 action 类似,如果过滤规则中定义了 data,那么 Intent 中必须也要定义可匹配的 data。在介绍 data 的匹配规则之前,我们需要先了解一下 data 的结构,因为 data 稍微有些复杂。

data的语法如下所示:

    <data 
       android:scheme="string"
       android:host="string"
       android:port="string"
       android:path="string"
       android:pathPattern="string"
       android:pathPrefix="string"
       android:mimeType="string"
        />

data 由两部分组成,mimeType 和 URI。mimeType指媒体类型,比如 image/jpeg、audio/mpeg4-generic 和 video/* 等,可以表示图片、文本、视频等不同的媒体格式,而 URI 中包含的数据就比较多了,下面是 URI 的结构:

    <scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]

这里再给几个实际的例子就比较好理解了,如下所示。
 

content://com.example.project:200/folder/subfolder/etc 

http://www.baidu.com:80/search/info

看了上面的两个示例应该就瞬间明白了,没错,就是这么简单。不过下面还是要介绍 一下每个数据的含义。

Scheme:URI的模式,比如 http、file、content 等,如果URI中没有指定 scheme。那 么整个 URI 的其他参数无效,这也意味着 URI 是无效的。

Host:URI的主机名,比如 www.baidu.com,如果 host 未指定,那么整个URI中的其他参数无效,这也意味着 URI 是无效的。

Port:URI中的端口号,比如80,仅当 URI 中指定了 scheme 和 host 参数的时候 port 参数才是有意义的。

Path、pathPattem 和 pathPrefix:这三个参数表述路径信息,其中 path 表示完整的路径信息;pathPattem 也表示完整的路径信息,但是它里面可以包含通配符 "*","*" 表示 0 个或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表示真实的字符串,那么"*"要写成 "\\*","\"要写成"\\\\";pathPrefix表示路径的前缀信息。

介绍完 data 的数据格式后,我们要说一下 data 的匹配规则了。前面说到,data 的匹配规则和 action 类似,它也要求 Intent 中必须含有data 数据,并且 data 数据能够完全匹配过滤规则中的某一个 data。这里的完全匹配是指过滤规则中出现的 data 部分也出现在了 Intent 中的 data 中。下面分情况说明。

(1)如下过滤规则:

       <intent-filter>
          <data android:mimeType="image/*"/>
      </intent-filter>

这种规则指定了媒体类型为所有类型的图片,那么 Intent 中的 mimeType 属性必须为 "image/*" 才能匹配,这种情况下虽然过滤规则没有指定 URI,但是却有默认值,URI 的默认值为 content 和 file 。

也就是说,虽然没有指定 URI,但是 Intent 中的 URI 部分的 schema 必须为 content 或者 file 才能匹配,这点是需要尤其注意的。为了匹配 (1) 中规则,我们可以写出如下示例:

    intent.setDataAndType(Uri.parse("file://abc"),"image/png");

另外,如果要为 Intent 指定完整的 data,必须要调用 setDataAndType 方法,不能先调用 setData 再调用 setType,因为这两个方法彼此会清除对方的值,这个看源码就很容易理解,比如setData:

    public Intent setData(Uri data) {
        mData = data;
        mType = null;
        return this;
    }

可以发现,setData 会把 mimeType 置为null,同理 sctType 也会把 URI 置为 null。

 

(2)如下过滤规则:

      <intent-filter>
           <data android:mimeType="video/mpeg" android:scheme="http" .../>
           <data android:mimeType="audio/mpeg" android:scheme="http" .../>
      </intent-filter>

这种规则指定了两组 data 规则,且每个 data 都指定了完整的属性值,既有 URI 又有 mimeType。为了匹配 (2) 中规则,我们可以写岀如下示例:

    intent.setDataAndType(Uri.parse("http://abc"),"video/png");

或者

    intent.setDataAndType(Uri.parse("http://abc"),"audio/png"); 

通过上面两个示例,读者应该已经明白了 data 的匹配规则,关于 data 还有一个特殊情况需要说明下,这也是它和 action 不同的地方,如下两种特殊的写法,它们的作用是一样的:

  <intent-filter >
     <data android:scheme="file" android:host="www.baidu.com"/>
         ...
 </intent-filter>


 <intent-filter >
       <data android:scheme="file" />
       <data android:host="www.baidu.com"/>
         ...
  </intent-filter>

到这里我们已经把 IntentFilter 的过滤规则都讲解了一遍,还记得本节前面给出的一个 intent-filter 的示例吗?现在我们给出完全匹配它的Intent:

    Intent intent = new Intent("com.ryg.charpter_1.c");
    intent.addCategory("com.ryg.category.c");
    intent.setDataAndType(Uri.parse("file//abc"),"text/plain");
    startActivity(intent);

还记得 URI 的 schema 是有默认值的吗?

如果把上面的 intent.setDataAndType(Uri.parse("file://abc"), text/plain") 这句成 intent.setDataAndType(Uri.parse("http://abc"), "text/plain")打开 Activity 的时候就会报错,提示无法找到 Activity,如图1-10所示。

另外 一点,Intent-filter的匹配规则对于Service和BroadcastReceiver 也是同样的道理,不过系统对于 Service 的建议是尽量使用显式调用方式来启动服务。

 

最后,当我们通过隐式方式启动一个 Activity 的时候,可以做一下判断,看是否有 Activity 能够匹配我们的隐式 Intent,如果不做判断就有可能出现上述的错误了。判断方法有两种:釆用 PackageManager 的 resolveActivity 方法或者 Intent 的 resoIveActivity 方法,如果它们找不到匹配的 Activity 就会返回 null,我们通过判断返回值就可以规避上述错误了。

另外,PackageManager 还提供了 querylntentActivities 方法,这个方法和 resolveActivity 方法不同的是:它不是返回最佳匹配的 Activity信息而是返回所有成功匹配的 Activity 信息。 我们看一下 querylntentActivities 和 resolveActivity 的方法原型:

    public abstract List<ResolveInfo>queryIntentActivities(Intent intent,int fladgs);
    public abstract ResolveInfo resolveActivity(Intent intent,int flags);

上述两个方法的第一个参数比较好理解,第二个参数需要注意,我们要使用 MATCH_ DEFAULT_ONLY 这个标记位,这个标记位的含义是仅仅匹配那些在 intent-filter 中声明了 <category android:name="android.intent.category.DEFAULT"/> 这个 category 的 Activity。

使用这个标记位的意义在于,只要上述两个方法不返回null,那么 startActivity 一定可以成功。 如果不用这个标记位,就可以把intent-filter中 category 不含 DEFAULT 的那些 Activity 给匹配出来,从而导致 startActivity 可能失败。因为不含有 DEFAULT 这个 category 的 Activity 是无法接收隐式 Intent 的。在 action 和 category 中,有一类 action 和 category 比较重要,它们是:

    <action android:name="android.intent.action.MAIN" />
    <category android:name="android.intent.category.LAUNCHER" />

这二者共同作用是用来标明这是一个入口 Activity 并且会出现在系统的应用列表中,少了任何一个都没有实际意义,也无法出现在系统的应用列表中,也就是二者缺一不可。 另外,针对 Service 和 BroadcastReceiver,PackageManager 同样提供了类似的方法去获取成功匹配的组件信息。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章