Android使用Intent在活动中穿梭

Intent 是一个消息传递对象,Intent 可以通过多种方式促进组件之间的通信,Intent 分为两种类型:显示Intent和隐式Intent。

  • 显式 Intent:我们通常会在自己的应用中使用显式 Intent 来启动组件,这是因为您知道要启动的 Activity 或服务的类名。例如,您可能会启动您应用内的新 Activity 以响应用户操作,或者启动服务以在后台下载文件。
    我经常使用的是这个构造函数public Intent(Context packageContext, Class<?> cls)
  • 隐式 Intent :不会指定特定的组件,而是声明要执行的常规操作,从而允许其他应用中的组件来处理。例如,如需在地图上向用户显示位置,则可以使用隐式 Intent,请求另一具有此功能的应用在地图上显示指定的位置。

使用显示 Intent 对象显式命名某个具体的 Activity 组件时,系统立即启动该组件。使用隐式 Intent 时,Android 系统通过将 Intent 的内容与在设备上应用的清单文件中声明的 Intent 过滤器进行比较,从而找到要启动的相应组件。如果 Intent 与 Intent 过滤器匹配,则系统将启动该组件,并向其传递 Intent 对象。如果多个 Intent 过滤器匹配,则系统会显示一个对话框,支持用户选取要使用的应用。

如果您没有为 Activity 声明任何 Intent 过滤器,则 Activity 只能通过显式 Intent 启动。Intent 过滤器是应用清单文件中的一个表达式,用于指定该组件要接收的 Intent 类型。例如,通过为 Activity 声明 Intent 过滤器,您可利用其它应用能够直接使用某一特定的意图过滤器 启动 Activity。

注意:为了确保应用的安全性,启动 Service 时,请始终使用显式 Intent,且不要为服务声明 Intent 过滤器。使用隐式 Intent 启动服务存在安全隐患,因为您无法确定哪些服务将响应 Intent,且用户无法看到哪些服务已启动。从 Android 5.0(API 级别 21)开始,如果使用隐式 Intent 调用 bindService(),系统会抛出异常。

一 Intent 中包含的主要信息如下:

1 组件名称

要启动的组件名称。这是可选项,但也是构建显式 Intent 的一项重要信息,这意味着 Intent 应当仅传递给由组件名称定义的应用组件。如果没有组件名称,则 Intent 则为隐式,且系统将根据其他 Intent 信息(例如,操作、数据和类别)决定哪个组件应当接收 Intent。如需在应用中启动特定的组件,则应指定该组件的名称。如下所示:

    public Intent(Context packageContext, Class<?> cls) {
        mComponent = new ComponentName(packageContext, cls);
    }

    private final String mPackage;
    private final String mClass;

    public ComponentName(@NonNull Context pkg, @NonNull Class<?> cls) {
        mPackage = pkg.getPackageName();
        mClass = cls.getName();
    }

例如,如果在应用中构建一个名为 DownloadService、旨在下载文件的服务,则可使用以下代码启动该服务:

public void download(){
    Intent downloadIntent = new Intent(this, DownloadService.class);
    downloadIntent.setData(Uri.parse(fileUrl));
    startService(downloadIntent);
}

Intent 通过 ComponentName 对象,您可以使用目标组件的完全限定类名指定此对象,例如,com.example.ExampleActivity.

public @NonNull Intent setComponent(@Nullable ComponentName component) {
        mComponent = component;
        return this;
    }
public @NonNull Intent setClass(@NonNull Context packageContext, @NonNull Class<?> cls) {
        mComponent = new ComponentName(packageContext, cls);
        return this;
    }
public @NonNull Intent setClassName(@NonNull String packageName, @NonNull String className) {
        mComponent = new ComponentName(packageName, className);
        return this;
    }
public @NonNull Intent setClassName(@NonNull Context packageContext,
            @NonNull String className) {
        mComponent = new ComponentName(packageContext, className);
        return this;
    }
2 操作

指定要执行的通用操作。您可以指定自己的操作,供 Intent 在您的应用内使用(或者供其他应用在您的应用中调用组件)。您通常应该使用由Intent 类或其他框架类定义的操作常量。Intent 中通常还包括与操作相关的数据

  • 以下代码段展示了如何创建 Intent 来发起通话
    Uri number = Uri.parse("tel:5551234");
    Intent callIntent = new Intent(Intent.ACTION_DIAL, number);

当您的应用通过调用 startActivity() 调用此 Intent 时,“电话”应用会发起对指定手机号码的呼叫。

  • 查看网页
    Uri webpage = Uri.parse("https://www.android.com");
    Intent webIntent = new Intent(Intent.ACTION_VIEW, webpage);

3 数据

创建 Intent 时,除了指定 URI 以外,指定数据类型(其 MIME 类型)往往也很重要。例如,能够显示图像的 Activity 可能无法播放音频文件,即便 URI 格式十分类似时也是如此。因此,指定数据的 MIME 类型有助于 Android 系统找到接收 Intent 的最佳组件。但,有时 MIME 类型可以从 URI 中推断得出,特别当数据是 content: URI 时尤其如此。content: URI 表明数据位于设备中,且由 ContentProvider 控制,这使得数据 MIME 类型对系统可见。

要仅设置数据 URI,请调用 setData()。要仅设置 MIME 类型,请调用 setType()。如有必要,您可以使用 setDataAndType() 同时显式设置二者。如下所示:

    private Uri mData;
    private String mType;
    private String mPackage;
    private ComponentName mComponent;

public @NonNull Intent setData(@Nullable Uri data) {
        mData = data;
        mType = null;
        return this;
    }
public @NonNull Intent setType(@Nullable String type) {
        mData = null;
        mType = type;
        return this;
    }
public @NonNull Intent setDataAndType(@Nullable Uri data, @Nullable String type) {
        mData = data;
        mType = type;
        return this;
    }

若要同时设置 URI 和 MIME 类型,请勿调用 setData() 和 setType(),因为它们会互相抵消彼此的值。请始终使用 setDataAndType() 同时设置 URI 和 MIME 类型。

4 类别

您可以将任意数量的类别描述放入一个 Intent 中,但大多数 Intent 均不需要类别。以下是一些常见类别:

  • CATEGORY_LAUNCHER
    该 Activity 是任务的初始 Activity,在系统的应用启动器中列出。您可以使用 addCategory() 指定类别。
public @NonNull Intent addCategory(String category) {
        if (mCategories == null) {
            mCategories = new ArraySet<String>();
        }
        mCategories.add(category.intern());
        return this;
    }

以上列出的这些属性(组件名称、操作、数据和类别)表示 Intent 的既定特征。通过读取这些属性,Android 系统能够解析应当启动哪个应用组件。Intent 也有可能会携带一些不影响其如何解析为应用组件的信息。Intent 还可以提供以下信息:
Extra
携带完成请求操作所需的附加信息的键值对,您可以使用各种 putExtra() 方法添加 extra 数据,每种方法均接受两个参数:键名和值。您还可以创建一个包含所有 extra 数据的 Bundle 对象,然后使用 putExtras() 将 Bundle 插入 Intent 中。如下所示:

public @NonNull Intent putExtra(String name, boolean value) {
        if (mExtras == null) {
            mExtras = new Bundle();
        }
        mExtras.putBoolean(name, value);
        return this;
    }

public @NonNull Intent putExtra(String name, String value) {
        if (mExtras == null) {
            mExtras = new Bundle();
        }
        mExtras.putString(name, value);
        return this;
    }

public @NonNull Intent putExtra(String name, Parcelable value) {
        if (mExtras == null) {
            mExtras = new Bundle();
        }
        mExtras.putParcelable(name, value);
        return this;
    }

在发送您希望另一个应用接收的 Intent 时,请勿使用 Parcelable 或 Serializable 数据。如果某个应用尝试访问 Bundle 对象中的数据,但没有对序列化类的,则系统将提出一个 RuntimeException。

5 标志

标志在 Intent 类中定义,充当 Intent 的元数据(描述数据属性)。标志可以指示 Android 系统如何启动 Activity(例如,Activity 应属于哪个任务),以及启动之后如何处理(例如,Activity 是否属于最近的 Activity 列表)。

二 隐式 Intent 示例

您通过隐式意图调用相册,浏览器,微信等。

可能没有任何应用处理您发送到 startActivity() 的隐式 Intent,如果发生这样的情况,调用失败,应用也会崩溃。要验证 Activity 是否会接收 Intent,请对 Intent 对象调用 resolveActivity()。如果结果为非空,则至少有一个应用能够处理该 Intent,并且可以安全调用 startActivity()。如果结果为空,不要使用该 Intent。以下示例说明如何验证 Intent 是否解析为 Activity。此示例没有使用 URI,但已声明 Intent 的数据类型,用于指定 extra 携带的内容。

public void verifyActivity() {
        // Create the text message with a string
        Intent sendIntent = new Intent();
        sendIntent.setAction(Intent.ACTION_SEND);
        sendIntent.putExtra(Intent.EXTRA_TEXT, "Hello World");
        sendIntent.setType("text/plain");

// Verify that the intent will resolve to an activity
        if (sendIntent.resolveActivity(getPackageManager()) != null) {
            startActivity(sendIntent);
        }
    }

调用 startActivity() 时,系统将检查已安装的所有应用,确定哪些应用能够处理这种 Intent(即:含 ACTION_SEND 操作并携带“text/plain”数据的 Intent)。如果只有一个应用能够处理,则该应用将立即打开并为其提供 Intent。如果多个 Activity 接受 Intent,则系统将显示一个对话框,使用户能够选取要使用的应用。

三 强制使用应用选择器

如果有多个应用响应隐式 Intent,则用户可以选择要使用的应用,并将其设置为该操作的默认选项。如果用户可能希望每次使用相同的应用执行某项操作(例如,打开网页时,用户往往倾向于仅使用一种网络浏览器),则选择默认选项的功能十分有用。

但是,如果多个应用可以响应 Intent,且用户可能希望每次使用不同的应用,则应采用显式方式显示选择器对话框。选择器对话框会要求用户选择用于操作的应用(用户无法为该操作选择默认应用)。例如,当应用使用 ACTION_SEND 操作执行“共享”时,用户根据目前的状况可能需要使用另一不同的应用,因此应当始终使用选择器对话框。

public void verifyActivity() {
        Intent sendIntent = new Intent(Intent.ACTION_SEND);

        String title = "选择标题";
        // Create intent to show the chooser dialog
        Intent chooser = Intent.createChooser(sendIntent, title);

        // Verify the original intent will resolve to at least one activity
        if (sendIntent.resolveActivity(getPackageManager()) != null) {
            startActivity(chooser);
        }
    }

四 接收隐式 Intent

应用可以接收哪些隐式 Intent,请在清单文件中使用 <intent-filter> 元素为每个应用组件声明一个或多个 Intent 过滤器。每个 Intent 过滤器均根据 Intent 的操作、数据和类别指定自身接受的 Intent 类型。仅当隐式 Intent 可以通过 Intent 过滤器之一传递时,系统才会将该 Intent 传递给应用组件。

显式 Intent 始终会传递给其目标,无论组件声明的 Intent 过滤器如何均是如此。

应用组件应当为自身声明单独的过滤器。例如,图库应用中的一个 Activity 可能会有两个过滤器,分别用于查看图像和编辑图像。当 Activity 启动时,将检查 Intent 并根据 Intent 中的信息决定具体的行为(例如,是否显示编辑器控件)。

每个 Intent 过滤器均由应用清单文件中的 <intent-filter> 元素定义,并嵌套在相应的应用组件。在 <intent-filter> 内部,您可以使用以下三个元素中的一个或多个指定要接受的 Intent 类型:

  • <action>
    在 name 属性中,声明接受的 Intent 操作。该值必须是操作的文本字符串值。

  • <data>
    使用一个或多个指定数据 URI(scheme、host、port、path)各个方面和 MIME 类型的属性,声明接受的数据类型。

  • <category>
    在 name 属性中,声明接受的 Intent 类别。该值必须是操作的文本字符串值。

要接收隐式 Intent,必须将 CATEGORY_DEFAULT 类别包括在 Intent 过滤器中。方法 startActivity() 和 startActivityForResult() 自家在Intent对象上加上 CATEGORY_DEFAULT 类别。如果未在 Intent 过滤器中声明此类别,则隐式 Intent 不会解析为您的 Activity。

例如,以下是一个使用包含 Intent 过滤器的 Activity 声明,当数据类型为文本时,系统将接收 ACTION_SEND Intent :

<activity android:name="ShareActivity">
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>

您可以创建一个包括多个 <action>、<data> 或 <category> 实例的过滤器。创建时,需确定组件能够处理这些过滤器元素的任何及所有组合。

public void send() {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_SEND);
        intent.setType("text/plain");
        if (intent.resolveActivity(getPackageManager()) != null) {
            startActivity(intent);
        }
    }

隐式 Intent 若要传递给组件,必须通过所有这三项测试。如果 Intent 不匹配其中任何一项测试,则 Android 系统不会将其传递给组件。但是,由于一个组件可能有多个 Intent 过滤器,因此未能通过某一过滤器的 Intent 可能会通过另一过滤器。

六 Intent 解析

当收到隐式 Intent 以启动 Activity 时,系统会根据以下三个方面将该 Intent 与 Intent 过滤器进行比较,搜索该 Intent 的最佳 Activity:

  • 操作。
  • 数据(URI 和数据类型)。
  • 类别。
1 操作测试

要指定接受的 Intent 操作,Intent 过滤器既可以不声明任何 <action> 元素,也可以声明多个此类元素,如下例所示:

<intent-filter>
    <action android:name="android.intent.action.EDIT" />
    <action android:name="android.intent.action.VIEW" />
    ...
</intent-filter>

要通过此过滤器,您在 Intent 中指定的操作必须与过滤器中列出的某一操作匹配。

2 类别测试

要指定接受的 Intent 类别,Intent 过滤器既可以不声明任何 <category> 元素,也可以声明多个此类元素,如下例所示:

<intent-filter>
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    ...
</intent-filter>

若要使 Intent 通过类别测试,则 Intent 中的每个类别均必须与过滤器中的类别匹配。反之则未必然。

Android 会自动将 CATEGORY_DEFAULT 类别应用于传递给 startActivity() 和 startActivityForResult() 的所有隐式 Intent。如需 Activity 接收隐式 Intent,则必须将 "android.intent.category.DEFAULT" 的类别包括在其 Intent 过滤器中(如上文的 <intent-filter> 示例所示)。

3 数据测试

要指定接受的 Intent 数据,Intent 过滤器既可以不声明任何 <data> 元素,也可以声明多个此类元素,如下例所示:

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

每个 <data> 元素均可指定 URI 结构和数据类型(MIME 媒体类型)。URI 的每个部分都是一个单独的属性:scheme、host、port 和 path:
<scheme>://<host>:<port>/<path>

下例所示为这些属性的可能值:
content://com.example.project:200/folder/subfolder/etc
在此 URI 中,架构是 content,主机是 com.example.project,端口是 200,路径是 folder/subfolder/etc。
在 <data> 元素中,上述每个属性均为可选,但存在线性依赖关系:

  • 如果未指定架构,则会忽略主机。
  • 如果未指定主机,则会忽略端口。
  • 如果未指定架构和主机,则会忽略路径。
    将 Intent 中的 URI 与过滤器中的 URI 进行比较时,它仅与过滤器中包含的部分 URI 进行比较。例如:
  • 如果过滤器仅指定架构,则具有该架构的所有 URI 均与该过滤器匹配。
  • 如果过滤器指定架构和权限,但未指定路径,则具有相同架构和权限的所有 URI 都会通过过滤器,无论其路径如何均是如此。
  • 如果过滤器指定架构、权限和路径,则仅具有相同架构、权限和路径的 URI 才会通过过滤器。

数据测试会将 Intent 中的 URI 和 MIME 类型与过滤器中指定的 URI 和 MIME 类型进行比较。规则如下:

  1. 仅当过滤器未指定任何 URI 或 MIME 类型时,不含 URI 和 MIME 类型的 Intent 才会通过测试。
  2. 对于包含 URI 但不含 MIME 类型(既未显式声明,也无法通过 URI 推断得出)的 Intent,仅当其 URI 与过
  3. 滤器的 URI 格式匹配、且过滤器同样未指定 MIME 类型时,才会通过测试。仅当过滤器列出相同的 MIME 类型且未指定 URI 格式时,包含 MIME 类型但不含 URI 的 Intent 才会通过测试。
  4. 仅当 MIME 类型与过滤器中列出的类型匹配时,同时包含 URI 类型和 MIME 类型(通过显式声明,或可以通过 URI 推断得出)的 Intent 才会通过测试的 MIME 类型部分。如果 Intent 的 URI 与过滤器中的 URI 匹配,或者如果 Intent 具有 content: 或 file: URI 且过滤器未指定 URI,则 Intent 会通过测试的 URI 部分。换言之,如果过滤器只是列出 MIME 类型,则假定组件支持 content: 和 file: 数据。

最后一条规则,即规则 (data),反映出对组件能够从文件中或内容提供程序处获得本地数据的预期。因此,其过滤器只能列出数据类型,不需要显式命名 content: 和 file: 架构。以下是一个典型示例,说明 <data> 元素向 Android 指出,组件可从内容提供程序处获得并显示图像数据:

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

由于大部分可用数据均由内容提供程序分发,因此指定数据类型(而非 URI)的过滤器也许最为常见。

另一常见的配置是具有架构和数据类型的过滤器。例如,下文中的 <data> 元素向 Android 指出,组件可从网络中检索视频数据以执行操作:

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

尽管 Intent 过滤器将组件限制为仅响应特定类型的隐式 Intent,但如果开发者确定您的组件名称,则其他应用有可能通过使用显式 Intent 启动您的应用组件。如果必须确保只有您自己的应用才能启动您的某一组件,将该组件的 exported 属性设置为 "false"。

同样,为了避免无意中运行不同应用的 Service,请始终使用显式 Intent 启动您自己的服务。

对于所有 Activity,您必须在清单文件中声明 Intent 过滤器。但是,广播接收器的过滤器可以通过调用 registerReceiver() 动态注册。稍后,您可以使用 unregisterReceiver() 注销该接收器。这样一来,应用便可仅在应用运行时的某一指定时间段内侦听特定的广播。

常见的拍摄照片或视频并将其返回的通用 Intent