Android必知必会——Intent和Intent Filter

3,038 阅读11分钟

关于Intent

Intent是一个消息传递对象,可以用来从其他应用组件请求操作。基本用例主要包括以下三个:

  • 启动Activity
  • 启动Service
  • 传递广播

Intent的类型

  • 显式Intent

通过提供目标应用的软件包名称或完全限定的组件类名来指定可处理Intent的应用。

  • 隐式Intent

不会指定特定的组件,而是声明要执行的常规操作,从而允许其他应用中的组件来处理。例如,需要在地图上向用户显示位置等。

使用隐式Intent,Android系统通过将Intent的内容与设备上其他应用的清单文件中声明的Intent过滤器进行比较,从而找到要启动的相应组件。如果只有一个,那么直接启动;如果有多个,那么会显示一个对话框,支持用户选取要使用的应用。

Intent过滤器,是应用清单文件中的一个表达式,用于指定该组件要接收的Intent类型。例如,通过为Activity声明Intent过滤器,可以使其他应用能够直接使用某一特定类型的Intent启动该Activity;如果未给Activity声明任何Intent过滤器,那么该Activity只能显示Intent启动。

需要注意的是:

启动Service时,应始终使用显示Intent,且不要为服务声明Intent过滤器。使用隐式Intent启动服务存在安全隐患,因为无法确定哪些服务将响应Intent,且用户也无法看到哪些服务已启动。从Android5.0开始,如果使用隐式Intent调用bindService,系统会抛出异常。

构建Intent

Intent对象携带Android系统用来确定要启动哪个组件的信息,以及收件人组件为了正确执行操作而使用的信息。也就是说主要分为两个方面:

  • 组件名称

要启动的组件名称,为可选项。如果没有那么则为隐式Intent。

对应这一属性的字段是ComponentName,可以通过方法setComponent()、setClass()、setClassName(),或Intent构造函数设置组件名称。

  • 操作

指定要执行的通用操作的字符串。

对于广播Intent,这是指已发生且正在报告的操作。操作会在很大程度上决定其余Intent的构成,特别是数据和extra中包含的内容。

可以指定自己的操作,共应用内使用。也可以使用Intent中既定好的操作如:

ACTION_VIEW:通过一些Activity展示某些特定的信息,例如照片或地图

ACTION_SEND:也称为共享Intent。一些用户可通过其他应用共享的数据,如邮件或社交共享应用等。

可以使用setAction()或Intent构造函数指定操作。

  • 数据

引用待操作数据和/或该数据MIME类型的URI。

创建Intent时,除了指定URI外,指定数据类型即MIME类型往往也很重要,这有助于Android系统找到接收Intent的最佳组件。当URI为content:xxx 类型时,表明数据位于设备中,且由ContentProvider控制,系统可以通过URI获知数据类型。

仅设置数据通过setData(),如果要设置MIME类型则用setType()。需要注意的是,不能既调setData又调setType,因为这两个方法会互相清除对方的设置。同时指定时应使用setDataAndType()。

  • 类别

一个包含应处理Intent组件类型的附加信息的字符串。可以将任意数量的类别描述放入一个Intent中,但多数Intent均不需要类别。

常见类别:

CATEGORY_BROWSABLE:目标Activity允许本身通过网络浏览器启动,以显示连接引用的数据。

CATEGORY_LAUNCHER:该Activity是任务的初始Activity,在系统的应用启动器中列出。

指定类别通过方法addCategory()。

  • Extra

携带完成请求操作所需的附加信息的键值对。通过Bundle保存键值对信息。

在发给另一个应用接收的Intent时,不要使用Parcelable或Serializable数据,如果某个应用尝试访问Bundle对象中的数据,但没有对打包或序列化的访问权限,系统会触发RuntimeException。

对应方法是putExtra()。

  • 标志

可以指示Android系统如何启动Activity,以及启动之后如何处理。

对应方法是setFlags()。

隐式Intent安全校验

通过隐式Intent可以将指定动作交由可处理的应用执行。但这其中也可能存在用户设备上没有任何应用能够处理此隐式Intent,或者由于配置文件限制或管理员执行了设置,导致该隐式Intent最终执行失败,一旦发生了这种情况,调用失败,发起此行为的应用也会崩溃。

解决此种情况了方式也很简单,就是对Intent对象调用resolveActivity进行安全校验,如果有返回结果,那么至少有一个应用能够处理该Intent,这时使用隐式Intent是安全的。如果结果为空,那么该隐式Intent则不可用。

// 创建一个隐式Intent
val sendIntent = Intent().apply {
    action = Intent.ACTION_SEND
    putExtra(Intent.EXTRA_TEXT, textMessage)
    type = "text/plain"
}

// 安全校验后,再使用该隐式Intent
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(sendIntent)
}

强制使用应用选择器

通过隐式Intent发送目标意图时,可能存在多个应用均能处理此行为的情况,这时系统会弹出选择框,让用户选择一个应用处理此行为。同时,默认情况下,用户还可以将某款应用设置为此行为的默认处理应用,那么在下次触发时,则不会再显示选择框,直接交由之前用户选择的应用处理。

当然,Android系统也提供了特定的API,可以使发起的隐式Intent每次都弹出应用选择框,用户则无法为该操作选择默认应用。

val sendIntent = Intent(Intent.ACTION_SEND)
...
val title: String = resources.getString(R.string.chooser_title)
// 创建显示应用选择框的隐式Intent
val chooser: Intent = Intent.createChooser(sendIntent, title)
// 安全验证后发起操作
if (sendIntent.resolveActivity(packageManager) != null) {
    startActivity(chooser)
}

接收隐式Intent

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

<intent-filer>标签内部,可以使用以下三种元素中的一个或多个指定要接受的Intent类型:

  • <action>

在name属性中,声明接受的Intent操作。值必须是操作的文本字符串值,而不是类常量。

  • <data>

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

  • <category>

在name属性中,声明接受的Intent类别。值必须是操作的文本字符串值,而不是类常量。

需要特别注意的一点是,如果要接受隐式Intent,必须将CATEGORY_DEFAULT类别包括在过滤器中。方法startActivity()和startActivityForResult()将按照其声明CATEGORY_DEFAULT类别的方式处理所有Intent。如果未在Intent过滤器中声明此类别,则隐式Intent不会解析为此Activity。

Activity组件的Intent过滤器必须在清单文件中声明,但是广播的过滤器可以通过调用registerReceiver()动态注册。Service则应始终通过显式Intent开启。

使用PendingIntent

PendingIntent对象是Intent对象的包装器。主要目的是授权外部应用使用包含的Intent,就像是它从应用本身的进程中执行的一样。

PendingIntent的主要用途:

  • 声明用户使用应用通知执行操作时所要执行的Intent(NotificationManager执行的Intent)。
  • 声明用户使用应用微件执行操作时要执行的Intent(主屏幕应用执行的Intent)。
  • 声明未来某一特定时间要执行的Intent(AlarmManager执行的Intent)。

由于每个Intent对象均设计为由特定类型的应用组件(Activity、Service或BroadcastReceiver)进行处理,因此必须基于相同的考虑因素创建PendingIntent:

  • PendingIntent.getActivity(),适用于启动Activity的Intent。
  • PendingIntent.getService(),适用于启动Service的Intent。
  • PendingIntent.getBroadcast(),适用于启动BroadcastReceiver的Intent。

调用上诉三个方法时,均需要传入int类型的flag参数,而此参数是被限定在几个特定值中的,这其中就包含PendingIntent的几个常量:

  • PendingIntent.FLAG_ONE_SHOT

标识此PendingIntent只能被使用一次。

  • PendingIntent.FLAG_NO_CREATE

若描述的Intent不存在则返回NULL值。

  • PendingIntent.FLAG_CANCEL_CURRENT

如果描述的PendingIntent已经存在,则在使用新的Intent之前会先取消掉当前的。

  • PendingIntent.FLAG_UPDATE_CURRENT

如果所描述的PendingIntent已经存在,请保留该标志,但用此新Intent中的内容替换其额外数据。

  • PendingIntent.FLAG_IMMUTABLE

指示创建的PendingIntent应该是不可变的标志。

Intent解析

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

Action匹配

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

<intent-filter>中可以不声明<action>,也可以声明多个。但如果声明了一个或多个,则匹配时,如果隐式Intent还有action,那么该action必须与过滤器中列出的某一个相匹配。即 1 in N

如果Activity过滤器中没有指定action,那么所有隐式Intent均无法通过。

但是如果隐式Intent中未指定action,那当Activity过滤器中至少有一项action时,该隐式Intent就可以通过匹配。即 0 in N

Category匹配

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

同action类似,在过滤器中可以不声明,也可以同时声明多个。

如果隐式Intent中有category,那其所有category均必须与过滤器中的类别匹配。即 N in NN in M(N < M,但每个都在M中)

如果隐式Intent中没有category,那么可以通过匹配。即 0 in N

需要注意的是,Android会自动将CATEGORY_DEFAULT类别应用于传递给startActivity()和startActivityForResult()的所有隐式Intent。

data匹配

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

同action类似,在过滤器中可以不声明,也可以同时声明多个。

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

URI的每个属性均为可选,但是存在线性依赖关系:

  • 如果未指定scheme,那么会忽略host。

  • 如果未指定host,那么会忽略port。

  • 如果未指定scheme和host,那么会忽略path。

Intent中的URI与过滤器URI的匹配规则:

  • 如果过滤器仅指定scheme,那么具有此scheme的URI均匹配。

  • 如果过滤器指定scheme权限,但未指定path,那么具有相同scheme和权限的URI均匹配,无视path。

  • 如果过滤器指定了scheme权限path,则需匹配此三项。

Intent中的URI和MIME类型,与过滤器中指定的URI和MIME类型匹配规则:

  • 如果过滤器什么都未指定,那么不含URI和MIME的Intent才匹配。

  • 如果隐式Intent包含URI但不包含MIME(通过URI也无法识别出),则仅当其URI与过滤器URI匹配、且过滤器同样未指定MIME时,才匹配。

  • 如果过滤器指定了MIME而未指定URI时,那么包含指定MIME,但不含URI的隐式Intent才匹配。

  • 如果隐式Intent同时还有URI和MIME,或则只还有URI但是通过URI可以推断出MIME类型,那么此Intent必须和过滤器规定的MIME匹配;如果隐式Intent中的URI与过滤器中的URI匹配,或则说过滤器未指定URI,而且隐式Intent的URI为content:或file:,那么此Intent可以通过URI匹配,也就是说,如果过滤器只列出了MIME类型,那么假定该组件支持content:和file:的数据。

由于大部分数据可用ContentProvider分发,因此指定数据类型而非URI的过滤器也许最为常见。

Intent匹配

通过PackageManager的方法queryIntentActivities()将返回能够执行作为参数传递的Intent中列出的所有Activity、queryIntentServices()则可返回类似的一系列Service,还有queryIntentContentProviders()返回类似的一系列ContentProvider,这两个方法均不会激活组件,只是列出。

同样还有方法resolveActivity()、resolveService()、resolveContentProvider()方法,可以返回一个当前隐式Intent的最佳处理者。