1. 前言

       在Android中有四大组件,这些组件中有三个组件与Intent相关,可见Intent在Android整个生态中的地位高度。Intent是信息的载体,用它可以去请求组件做相应的操作,但是相对于这个功能,Intent本身的结构更值得我们去研究。

2. Intent与组件

       Intent促进了组件之间的交互,这对于开发者非常重要,而且它还能做为消息的载体,去指导组件做出相应的行为,也就是说Intent可以携带数据,传递给Activity/Service/BroadcastReceiver。

  • 启动Activity。Activity可以简单的理解为手机屏幕中的一个页面,你可以通过将Intent传入startActivity方法来启动一个Activity的实例,也就是一个页面,同时,Intent也可以携带数据,传递给新的Activity。如果想要获取新建的Activity执行结果,可以通过onActivityResult()方法去启动Activity。
  • 启动Service。Service是一个不呈现交互画面的后台执行操作组件,可以通过将Intent穿入startService()方法来启动一个Service来启动服务。
  • 传递广播BroadCast。广播是任何应用都可以接收到的消息,通过将Intent传递给 sendBroadcast()sendOrderedBroadcast()sendStickyBroadcast()方法,可以将广播传递接收方。

3. Intent类型

在Android中,Intent分为两种类型,显式和隐式。

  • 显式Intent,可以通过类名来找到相应的组件,在应用中用显式Intent去启动一个组件,通常是因为我们知道这个组件(Activity或者Service)的名字。如下代码,我们知道具体的Activity的名字,要启动一个新的Activity,下面就是用的显示Intent。

    Intent intent = new Intent(context,XXActivity.class);
    startActivity(intent);
  • 隐式Intent,不指定具体的组件,但是它会声明将要执行的操作,从而匹配到相应的组件。最简单的Android中调用系统拨号页面准备打电话的操作,就是隐式Intent。

    Intent intent = new Intent(Intent.ACTION_DIAL);
    Uri data = Uri.parse("tel:" + "135xxxxxxxx");
    intent.setData(data);
    startActivity(intent);
使用显示Intent去启动Activity或者Service的时候,系统将会立即启动Intent对象中指定的组件。

       使用隐式Intent的时候,系统通过将Intent对象中的IntentFilter与组件在AndroidManifest.xml或者代码中动态声明的IntentFilter进行比较,从而找到要启动的相应组件。如果组件的IntentFilter与Intent中的IntentFilter正好匹配,系统就会启动该组件,并把Intent传递给它。如果有多个组件同时匹配到了,系统则会弹出一个选择框,让用户选择使用哪个应用去处理这个Intent,比如有时候点击一个网页链接,会弹出多个应用,让用户选择用哪个浏览器去打开该链接,就是这种情况。

       IntentFilter通常是定义在AndroidManifest.xml文件中,也可以动态设置,通常是用来声明组件想要接受哪种Intent。例如,你如果为一个Activity设置了IntentFilter,你就可以在应用内或者其他应用中,用特定的隐式Intent来启动这个Activity,如果没有为Activity设置IntentFilter,那么你就只能通过显示Intent来启动这个Activity。

4. Intent的属性

       Intent作为消息的载体,系统根据它去决定启动哪个具体的组件同时将组件执行中需要的信息传递过去。Intent能够包含的属性有Component、Action、Data、Category、Extras、Flags,关于这些属性的更详细信息可查看这里

  • Component,要启动的组件名称。这个属性是可选的,但它是显式Intent的一个重要属性,设置了这个属性后,该Intent只能被传递给由Component定义的组件。隐式Intent是没有该属性的,系统是根据其他的信息(例如,Action、Data等)来判断该Intent应该传递给哪个组件。这个属性是目标的组件的具体名称(完全限定类名),例如,com.example.DemoActivity。该属性可以通过setComonentName()setClass()setClassName()或者Intent的构造函数来设置。
  • Action,表明执行操作的字符串。它会影响Intent的其余信息,比如Data、Extras。该属性可以通过setAction()方法或者Intent的构造函数来设置。用户可以自定义这个属性,也可以使用系统中已经有的Action值。下面列出启动Activity时候的一些通用Action属性。

    • ACTION_VIEW,当有一些信息需要展示出来,可以设置Intent的Action为这个值,并调用startActivity()方法
    • ACTION_SEND,当用户有一些信息需要分享到其他应用,可以设置Intent的Action为这个值,并调用startActivity()方法
    • ACTION_DIAL,拨打电话,可以设置Intent的Action为这个值,并调用startActivity()方法
    • ACTION_EDIT,编辑某些文件,可以设置Intent的Action为这个值,并调用startActivity()方法
  • Data,它是待操作数据的引用URI或者数据MIME类型的URI,它的值通常与Intent的Action有关联。比如,如果设置Action的值为ACTION_EDIT,那么Data的值就必须包含被编辑文档的URI。当我们创建Intent的时候,设置MIME类型非常重要。例如,一个可以显示图片的Activity可能不能播放音频,图片和音频的URI非常类似,如果我们设置了MIME类型,可以帮助系统找到最合适的组件接受Intent。有时候,MIME类型也可以从URI判断出来,例如当Data是一个包含content:字符串的URI时候,可以明确的知道,待处理的数据存在设备中,而且由ContentProvider控制。

    • 使用setData()方法设置数据引用的URI,使用setType()方法设置数据的MIME类型,使用setDataAndType()方法同时设置这两个属性。
    • 注意:如果想要设置两个的属性,直接用setDataAndType()方法,不要同时调用setData()setType()方法,因为这两个方法设置的值会相互覆盖

      public Intent setData(Uri data) {
          mData = data;
          mType = null;
          return this;
      }
      
      public Intent setType(String type) {
          mData = null;
          mType = type;
          return this;
      }
  • Category,这个属性是对处理该Intent组件信息的补充。它是一个ArraySet类型的容器,所以可以向里面添加任意数量的补充信息,同时,Intent没有设置这个属性不会影响解析组件信息。可以通过addCategory()方法来设置该属性。下面列出一些常用的Category的值。

    • CATEGORY_BROWSABLE,设置Category为该值后,在网页上点击图片或链接时,系统会考虑将此目标Activity列入可选列表,供用户选择以打开图片或链接。
    • CATEGORY_LAUNCHER,应用启动的初始Activity,这个Activity会被添加到系统启动launcher当中。

以上列出的这些关于Intent的属性(Component、Action、Data、Category)可以帮助系统来确定具体的组件,但是有一些Intent的属性,不会影响到组件的确定。

  • Extras,以key-value键值对的形式来存储组件执行操作过程中需要的额外信息,可以调用putExtra()方法来设置该属性,这个方法接受两个参数,一个是key,一个是value。也可以通过实例化一个储存额外信息的Bundle对象,然后调用putExtras()方法将我们实例化的Bundle添加到Intent中。
  • Flags,这个属性可以指示系统如何启动一个Activity,以及启动之后如何处理。例如Activity属于哪一个task(参考Activity的四种启动方式)。

5. 显式Intent示例

       上文说到,显式Intent是用于启动某个特定的组件(Activity或者Service)的Intent,穿创建显式的Intent的时候需要设置组件名称(Component)属性,其他的属性都是可选属性。

// fileUrl是一个URL字符串,例如 "http://www.example.com/image.png"
Intent downloadIntent = new Intent(context, DownloadService.class);
downloadIntent.setData(Uri.parse(fileUrl));
startService(downloadIntent);

这里的Intent的构造函数传入了两个参数,context和组件名(Component),调用了startService()方法后,会在当前的应用中启动DownloadService这个服务。
显示Intent中设置的组件名(Component)需要在AndroidManifest.xml进行注册,所以它一般用来启动当前应用内的组件。

6. 隐式Intent示例

       隐式Intent比显示的Intent会复杂一些,它既可以启动当前应用内的组件,也可以启动当前应用外的组件。如果当前应用无法处理隐式Intent,但是其他应用中的组件可以处理,那么系统会弹框让用户选择启动哪个应用中的组件。

       例如,如果用户有内容想分享给其他应用,就创建一个Intent,将它的Action属性设置为ACTION_SEND,然后将要分享的内容设置到Extras属性中,然后调用startActivity()方法,用户就可以选择将内容分享到哪一个应用。

// 要将textMessage信息分享出去
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");

// 确认是否有组件能够处理这个隐式Intent
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(sendIntent);
}

       调用startActivity()传入一个隐式Intent时候,系统会检查设备中所有的应用,确定哪些应用可以处理这个隐式的Intent(含有startActivity()操作并携带text/plain类型的Intent),如果只有一个应用可以处理这个Intent,那么直接唤起这个应用,并将Intent传给它;如果有多个应用可以处理这个Intent,那么系统会弹出一个选择框,让用户选择唤起哪个应用。

7. 强制唤起选择框

       上文说了,如果多个应用可以处理同一个隐式Intent,系统会弹出选择框,让用户选择唤起哪个应用,并设置该应用为默认的打开方式,以后就不会弹出选择框了。如果用户希望以后一直使用该用户处理这个隐式Intent(比如打开网页,用户通常会倾向于使用同一个web浏览器),那么十分方便。

       但是如果用户想每一次都用不同的应用去处理这个隐式的Intent的,就应该每次弹出选择框,用户可以在选择框中选择唤起的应用,但是无法设置默认的打开方式。例如,当用户想根据当前的位置将内容分享到不同的应用,所以每次都需要弹出选择框。

用户需要通过Intent.createChooser()创建一个Intent,然后调用startActivity()

Intent sendIntent = new Intent(Intent.ACTION_SEND);
...

// 分享的标题
String title = getResources().getString(R.string.chooser_title);
// 创建一个调用选择框的Intent
Intent chooser = Intent.createChooser(sendIntent, title);

// 确认是否有应用可以处理这个Intent
if (sendIntent.resolveActivity(getPackageManager()) != null) {
    startActivity(chooser);
}

8. 接受隐式Intent

       想配置你的应用可以处理哪些隐式的Intent,需要在AndroidManifest.xml文件中使用<intent-filter>标签为组件设置一个或者多个过滤器。每一个过滤器基于ActionDataCategory来指定自身可以处理的Intent类型。如果隐式Intent的能够匹配到用户设置的其中一个过滤器,系统才能唤起这个应用相应的组件并将Intent传递给这个组件。

       组件应该为为一个它可以处理的操作单独设置一个处理器。例如,相册中的Activity可能有两个过滤器,一个过滤器对应浏览照片的操作,另一个过滤器对应编辑照片的操作。当这个Activity被启动的时候,根据Intent中携带的信息来决定执行哪种操作。

       每一个过滤器是在AndroidManifest.xml使用<intent-filter>标签来定义的,嵌套在组件标签中,例如<activity><service>标签。在<intent-filter>标签中,用户可以使用一下三个属性中的一个或者多个来指定可以接受的Intent。

  • <action>,在这个属性中,声明该组件可以执行的操作。该值必须是关于操作的一个字符串,并不是类常量
  • <data>,使用一个或者多个数据URI(scheme、host、port、path等等)和数据的MIME类型来指定接受的数据类型
  • <category>,声明接受的Intent类型

       Activity组件要接受隐式Intent,它必须有一个<category>属性为CATEGORY_DEFAULT的过滤器,因为startActivity()和startActivityForResult()方法处理Intent时候,默认的认为接受组件有一个<category>属性为CATEGORY_DEFAULT的过滤器。如果一个Activity组件不声明这样一个过滤器,它就接收不到隐式Intent。

例如,以下代码声明了一个Activity组件,这个组件可以处理action属性为ACTION_SEND,数据类型是文本(text/plain)的隐式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>标签的过滤器,创建时候仅需要确定该组件能够处理过滤器定义的操作即可。

如果是根据<action><data><category>标签的组合来处理多个Intent,那么需要为这个组件声明多个过滤器。

系统会以这三个属性将隐式Intent与所有组件声明的过滤器进行对比,如果这三个属性全部能够匹配上,系统才有可能将这个隐式Intent传递给这个组件,因为如果多个应用的组件都能匹配上会弹出选择框,让用户选择一个应用去处理这个隐式Intent。

为了避免无意中启动了其他的Service,所以在应用内,建议一直使用显示的Intent去启动服务,这样就不必再AndroidManifest.xml文件中为Service声明过滤器了。

对于Activity的过滤器,必须在AndroidManifest.xml文件中声明,也可以不声明,直接使用显示Intent唤起Activity组件。

广播接收器的过滤器声明可以在AndroidManifest.xml文件中声明,也可以使用registerReceiver()方法动态注册,使用完毕后,使用unregisterReceiver()方法动态注销。

9. 过滤器声明示例

下面一些过滤器的声明能够帮助你更好的理解。

<activity android:name="MainActivity">
    <!-- 该Activity是该应用的启动入口页面,它会被储存在系统的launcher列表中 -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity android:name="ShareActivity">
    <!-- 该Activity能够处理ACTION_SEND行为且数据类型为text/plain的隐式Intent -->
    <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_SEND行为且数据类型是媒体内容的隐式Intent -->
    <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="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>

第一个名为MainActivity的组件,是应用的启动入口页面,当用户点击应用图标,该Activity会被启动。

  • android.intent.action.MAIN,表示该Activity是应用的启动入口,且不需要任何Intent携带的数据。
  • android.intent.category.LAUNCHER,表示将该Activity的图标设为手机主屏幕上的应用图标,如果它没有图标,就用Application的图标。

第二个名为ShareActivity的组件,能够处理两种隐式Intent,可以接受文本和媒体内容的分享操作,也就是说如果一个隐式Intent能够匹配到任意一个过滤器都可以唤起该Activity。当然,也可以直接通过显示Intent指定启动它。

9. PendingIntent

PendingIntent是对Intent的一种封装。它主要作用在于,让外部的应用执行内部的Intent时候,就好像是在你的应用中还行一样。

通常在以下场景中会使用PendingIntent。

  • 当用户点击通知栏时候,才执行的Intent(系统的NotificationManager执行的Intent),详情参考这里
  • 当用户操作悬浮在主屏幕中的小工具,才执行的Intent(主屏幕应用执行的Intent),详情参考这里
  • 在未来某一个特定时间执行的Intent(系统的AlarmManager执行的Intent)

因为每一个Intent对象都是针对具体的组件类别(Activity/Service/BroadcastReceiver)进行实例化,因此在创建PendingIntent的时候,也要基于相同的因素去实例化,使用以下方法实例化PendingIntent。

  • PendingIntent.getActivity(),返回一个适用于Activity组件的PendingIntent
  • PendingIntent.getService(),返回一个适用于Service组件的PendingIntent
  • PendingIntent.getBroadcast(),返回一个适用于BroadcastReceiver的PendingIntent

当然官方还有一些其他获取PendingIntent对象的方法,不过内部也是使用上面三个方法来获取实例化对象的。

这三个方法都需要当前应用的context,需要封装的Intent,以及一个或者多个该如何使用该Intent的标志(例如,是否可以多次使用该Intent)。

关于Pending的具体使用也不再这里展开,需要了解具体使用的可以查看Notification中PendingIntent的使用悬浮工具栏中PendingIntent的使用

10. Intent匹配规则

       上文中提到了,当发送一个隐式Intent后,系统会将它与设备中的每一个组件的过滤器进行匹配,匹配属性有ActionCategoryData三个,需要这三个属性都匹配成功才能唤起相应的组件。

10.1 Action匹配规则

一个过滤器可以不声明Action属性也可以声明多个Action属性。如下:

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

隐式Intent中的Action属性,与组件中的某一个过滤器的Action能够匹配(如果一个过滤器声明了多个Action属性,只需要匹配其中一个就行),那么就算是匹配成功。

如果过滤器没有声明Action属性,那么只有没有设置Action属性的隐式Intent才能匹配成功。

10.2 Category匹配规则

一个过滤器可以不声明Category属性也可以声明多个Category属性,如下:

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

       隐式Intent中声明的Category必须全部能够与某一个过滤器中的Category匹配才算匹配成功。比如说一个Category属性设为CATEGORY_BROWSABLE的隐式Intent也可以通过上面的过滤器,也就是说,过滤器的Category属性内容必须是大于或者等于隐式Intent的Category属性时候,隐式Intent才能匹配成功。

如果一个隐式Intent没有设置Category属性,那么它可以通过任何一个过滤器的Category匹配。

10.3 Data匹配规则

一个过滤器可以不声明Data属性也可以声明多个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四个部分,host和port合起来也成authority(host:port)部分。

<scheme>://<host>:<port>/<path>

例如:

content://192.168.0.1:8080/folder/subfolder/etc

在这个URI中,scheme是content,host是192.168.0.1,port是8080,path是folder/subfolder/etc。我们平时使用的网络url就是这种格式。

在URI中,每个组成部分都是可选的,但是有线性的依赖关系

  • 如果没有scheme部分,那么host部分会被忽略
  • 如果没有host部分,那么port部分会被忽略
  • 如果host部分和port部分都没有,那么path部分会被忽略

当进行URI匹配时候,并不是比较全部,而是局部对比,以下是URI匹配规则。

  • 如果一个URI仅声明了scheme部分,那么所有拥有与其相同的scheme的URI都会通过匹配,其他部分不做匹配
  • 如果一个URI声明了scheme部分和authority部分,那么拥有与其相同schemeauthority的URI才能匹配成功,path部分不做匹配
  • 如果一个URI所有的部分都声明了,那么只有所有部分都相同的URI才能匹配成功

Data匹配时候,MIME类型和URI两者都会进行匹配,匹配规则如下:

  • 如果过滤器未声明URI和MIME类型,则只有不含URI和MIME类型的隐形Intent才能匹配成功
  • 如果过滤器中声明URI但是未声明MIME类型(也不能从URI中分析出MIME类型),则只有URI与过滤器URI相同且不包含IME类型的隐式Intent才能匹配成功
  • 如果过滤器声明MIME类型但是未声明URI,只有包含相同MIME类型但是不包含URI的隐式Intent才能匹配成功
  • 如果过滤器声明了URI和MIME类型(既可以是直接设置,也可以是从URI分析出来),只有包含相同的URI和MIME类型的隐式Intent才能匹配成功

11. 其他

       系统通过过滤器去匹配Intent,启动相应组件,在PackageManager类中提供了一系列的查询(queryIntentActivities()/queryIntentServices()/queryBroadcastReceivers())方法去查询可以处理某个Intent的组件,也提供了一系列的解析(resolveActivity()/resolveService())方法来确定最佳启动组件。这些方法在某些场景下是非常有用的,也可以帮助我们降低程序crash风险。

12. 思考

       上文中对Intent以及IntentFilter进行了详细的讲解,大多都是系统级别的处理过程。但是Intent作为一个官方已经封装好的信息携带者,我们可以用它来做很多事情。比如可以写自己的一套匹配规则,Intent仅仅作为数据携带者,通过它去传递一些信息,实现Fragment/Activity的页面跳转逻辑。关于Intent的使用,我相信还有更多的用处,需要用户一步一步的去探索。

03-05 21:03