解读Android之Intents和Intent Filters
概述
Intent本意为目的、意向、意图。在Android中,Intent是系统各组件(或应用程序)之间进行数据传递的数据附载者,Intent不仅可以用于应用程序之间的交互,也可以用于应用程序内部的activity、service和broadcast receiver之间的交互。
Android的三个基本组件——Activity,Service和Broadcast Receiver都是通过Intent机制启动的,不同类型的组件有不同的传递Intent机制:
- 启动Activity
使用Context的startActivity()
或Activity的startActivityForRestult()
去启动一个Activity或使一个已存在的Activity去做新的事情。
使用Activity的setRestult()
传入一intent来从activity中返回结果。
具体详见本人之前的翻译:解读Android之Activity(1)基础知识。 启动Service
使用Context的startService()
去初始化一个service或传递消息给正在运行的service。
使用Context的bindService()
去建立调用组件和目标服务之间的连接(即绑定service)。
具体详见本人之前的翻译:解读Android之Service(1) 和
解读Android之Service(2)Bound Service启动broadcast receiver
将intent对象传递给Context的sendBroadcast()
或sendOrderedBroadcast()
或sendStickyBroadcast()
,都将发送一个broadcast。
Intent类型
Intent有两种类型:
显式Intent,指定具体将要启动的组件
指定了component属性的Intent(调用setComponent(ComponentName)
或者setClass(Context, Class)
来指定)或者直接用构造器Intent(Context,Class)
。通过指定具体的组件类,通知应用启动对应的组件。通常,在启动我们自己程序中的组件需要使用显示Intent对象,这样能够减少Intent对象解析过程,提高性能。关于解析后续介绍。
- 隐式Intent,即通过Intent Filter过滤匹配
没有指定comonent属性的Intent。这些Intent需要包含其它足够的信息,这样系统才能根据这些信息,在所有的可用组件中,确定满足此Intent的组件。
当创建显示intent启动activity或service时,系统将立刻启动指定activity或service。
当创建隐式intent时,系统通过manifest文件中的intent filters寻找合适的组件来启动。若有多个组件满足intent的条件,则系统会显示一个对话框呈现这些满足条件的组件让用户自己选择。
Intent Filter描述了一个组件愿意接收什么样的Intent对象,Android将其抽象为android.content.IntentFilter
类。在Android的AndroidManifest.xml配置文件中的<activity>
标签中可以通过<intent-filter>
标签为一个Activity指定其Intent Filter,以便告诉系统该Activity 可以响应什么类型的Intent对象。若组件在manifest中没有设置<intent-filter>
,则该组件只能通过显示intent启动。
注意:为了确保我们的应用程序的安全性,不声明service的intent filters,当启动Service时,总是使用显示intent。因为若我们 使用隐式intent启动service的话会造成安全隐患,我们不能确定哪个service去响应intent,用户也无法看到。从android5.0(API21)开始,若要通过隐式intent绑定一个service的话,系统会抛出异常。
上图描述了隐式intent如何启动另一个activity:
- activity A创建一个带有action的intent,然后传递给
startActivity()
。 - 系统寻找所有程序的intent filters是否匹配该intent。
- 当发现一个匹配时,系统启动该匹配的activity B,并将intent传递给B。
创建Intent对象
Intent对象携带必要的信息,使得系统能够使用这些信息去启动相应的组件。
intent中主要的信息包括:
Component name
组件名字是可选的,指定Intent对象的目标组件的类名称(包括包名)。指定component的话,将直接使用并启动指定的组件,Intent的其它所有属性都是可选的。此为显式Intent,即直接启动指定的组件。如果设置了,intent对象传递到指定类的实例;如果没有设置,Android使用intent中的其它属性来定位合适的目标组件。
组件的名字通过setComponent()
,setClass()
或setClassName()
或Intent构造器设置,通过getComponent()读取。其中,setComponent()接收一个ComponentName类实例,ComponentName构造器源码如下:
/** * @param pkg 包名,不能为null * @param cls 类名,不能为null */ public ComponentName(String pkg, String cls) { if (pkg == null) throw new NullPointerException("package name is null"); if (cls == null) throw new NullPointerException("class name is null"); mPackage = pkg; mClass = cls; }/** * @param pkg Context对象 * @param cls 类名,不能为null */public ComponentName(Context pkg, String cls) { if (cls == null) throw new NullPointerException("class name is null"); mPackage = pkg.getPackageName(); mClass = cls;}/** * @param pkg Context对象 * @param cls 类 */ public ComponentName(Context pkg, Class<?> cls) { mPackage = pkg.getPackageName(); mClass = cls.getName(); }
其实,这三种构造器本质都一样。
行为Action
String字符串,表示intent的动作。例如日常生活中描述一个意愿时经常带有某种动作,比如我想看爱情动作片,这里看就是一个动作。在Intent中,Action就是描述intent的动作。当指明一个action时,目标组件就会依照这个动作的指示,表现对应的行为。在Intent中,定义了很多动作,如ACTION_VIEW等等,基本上涵盖了常用的动作。下面介绍两个:
ACTION_VIEW
当要通过activity(startActivity()
启动的)显示信息给用户时可以设置该动作,例如:查看图片,查看地图等。ACTION_SEND
这种也可以称为share
intent,当要通过activity(startActivity()
启动的)和其它应用程序共享数据时可以使用该动作,例如email程序和社交程序等。
一个intent Filter可以包含多个Action。但是每个Intent只能设置一个动作,通过setAction()
或Intent构造器设置动作。
除了使用系统提供的动作,我们还可以自定义动作。若要自定义动作的话,必须要保证该动作是以包名为前缀的,例如:
static final String ACTION_TIMETRAVEL = "com.example.action.TIMETRAVEL";
动作很大程度上决定了剩下的intent如何构建,特别是数据(data和type)和类别(category)属性,就像一个方法名决定了参数和返回值。正是这个原因,应该尽可能明确指定动作,并紧密关联到其它Intent属性。换句话说,应该定义你的组件能够处理的Intent对象的整个协议,而不仅仅是单独地定义一个动作。
数据Data
data表示动作要操作的数据,更精确地指定当前活动能够响应什么类型的数据。data属性的声明中要指定访问数据的Uri和MIME类型(如text/html,text/xml,image/jpg等)。
关于更多MIME类型的介绍请参考之前翻译文章:MIME类型。
当创建intent时,在URI中添加MIME类型是非常重要的。例如一个显示图片的activity无法播放视频文件,但是它们的URI格式却差不多。因此,指定数据的MIME类型能够帮助系统选择最佳的组件响应intent。在许多情况下,数据类型能够从URI中推测,特别是content:URIs,它表示位于设备上的数据且被内容提供者(content provider)控制。
只设置数据URI的话,可以使用setData()
(设置这个会将MIME类型设置为null),只数据MIME类型的话,可以调用setType()
(设置这个会将数据设置为null),若两个都设置的话,可以调用setDataAndType()
。
类别category
字符串,用于表现动作的类别,相当于把动作分类(如增删改(三个动作)一篇论文,这三个动作都属于编辑类别),Intent和Intent Filter中都可以添加多个。系统提供了很多类别,如CATEGORY_BROWSABLE
(允许使用浏览器打开activity),CATEGORY_LAUNCHER
(系统应用程序启动入口activity)等,我们也可以自定义类别,方法和自定义Action一样。在Intent中可以通过setCategory()
添加。
注:
- 必须和动作一块设置,否则报错。
- 若当前组件是Activity则,必须在intent-filter标签中再增加一个添加默认的类别:
<category android:name="android.intent.category.DEFAULT"/>
。PS:多说一句看准IDE自动补全的是不是android.intent.category.DEFAULT
,提示NO Activity。我在这纠结了半天。 - 在主Activity中设置
android.intent.action.MAIN
和android.intent.category.LAUNCHER
,它们分别标记活动开始新的任务和带到启动列表界面。该Activity可以包含android.intent.category.DEFAULT
,也可以不包含。
通过上面这四个属性,系统就能够知道应该由哪个组件响应intent。但是,除了上面的几个属性,intent还可以携带不会影响系统响应组件的其它信息。如下:
Extras
附加信息,一般用来携带一些数据信息。通过额外的键值对(key-value)把数据保存在Intent对象中。系统提供了一些额外信息的key如:EXTRA_EMAIL
,EXTRA_SUBJECT
等,当然我们也可以添加自己的额外信息。
Intent对象有一系列的putExtra(String,XXX)
方法用于插入各种附加数据和一系列的getXXXExtra()
用于读取数据。这些方法与Bundle对象的方法类似,实际上,附加信息可以作为一个Bundle使用putBundleExtras()和getBundleExtras()添加和读取。
intent指定许多EXTRA_*
常量用于标准化的数据类型,若我们需要声明自己的额外信息的key,需要确保该key要包含程序包名,如:
static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";
关于更多Extras,请参考: Activity之间通信。
Flags
期望意图的运行模式。Intent类定义了各种各样的标志,许多指示Android系统如何去启动一个活动(例如,活动应该属于那个任务)和启动之后如何对待它(例如,它是否属于最近的活动列表)。
这一部分的详细信息,请参考我之前的翻译:解读Android之任务和Back栈。
显式Intent例子
通过component属性设置intent对象为显示启动方法,其它intent属性为可选项。
代码如下:
/** * 使用component属性设置Intent对象,启动一个Activity * */Intent intent = new Intent();//构造器有多种,也可以为new ComponentName(this,MainActivity2.class);//同样也有启动其它包中activity的构造器ComponentName componentName = new ComponentName(this,"com.sywyg.intent_test.MainActivity2");intent.setComponent(componentName);startActivity(intent);
需要注意的是,如果我们在Intent中指定了component属性,系统将不会再对action、data/type、category进行匹配。
隐式Intent例子
不是通过component属性方式启动的intent都是隐式启动,因此需要指定Action/Category/Data/Type信息。
注意:隐式intent方式启动的acitivity可能不存在,因此为避免程序无法响应intent而造成崩溃,可以先通过resolveActivity()
判断是否有满足的组件,如下:
// Create the text message with a stringIntent sendIntent = new Intent();sendIntent.setAction(Intent.ACTION_SEND);sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);sendIntent.setType(HTTP.PLAIN_TEXT_TYPE); // "text/plain" MIME type// Verify that the intent will resolve to an activityif (sendIntent.resolveActivity(getPackageManager()) != null) { startActivity(sendIntent);}
注意:这个例子中,没有使用Data,但是声明了intent数据类型,用于指定extras携带的内容。
当调用startActivity()
时,系统检测所有安装的程序确定哪些程序能够处理这种intent对象(带有ACTION_SEND
和text/plain
)。若只有一个程序满足条件的话,则该程序立刻响应;若多个响应的话,由用户选择响应哪个。
强制使用程序选择器
当有多个程序能够响应隐式intent时,用户可以选择使用哪个,也能够使那个程序成为默认的选择。
然而,用户可能每次都有不同的选择,我们可以使用一个对话框的形式显示给用户,让用户自己选择。
为了显示选择器对话框,可以使用createChooser()
,然后传递给startActivity()
,如下:
Intent sendIntent = new Intent(Intent.ACTION_SEND);// 创建选择器对话框Intent chooser = Intent.createChooser(sendIntent, "选择器名称");// Verify the original intent will resolve to at least one activityif (sendIntent.resolveActivity(getPackageManager()) != null) { startActivity(chooser);}
上述方法将显示一个带有满足条件的程序列表对话框,并且使用”选择器名称”作为对话框标题。
接收隐式intent
为了声明我们的activity可以响应哪些隐式intent,我们应该在manifest文件中,在<activity>
标签或其它组件标签的子标签中设置<intent-filter>
标签,可以设置intent的action,data和category。只有满足条件的intent filters才能响应。而对于显示intent对象则不需要intent filters。
一个组件应该为每个独立的任务设置不同的intent filters。我们可以为每个intent filter设置如下标签:
<action>
使用name属性声明intent行为。该值必须是字面字符串(literal string),不能是类常量。<data>
使用一个或多个属性声明数据和MIME类型。<category>
使用name属性声明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>
我们可以在一个intent filter中创建多个<action>
,<data>
和<category>
。若设置了多个,则表明该组件能够处理这些标签的任何组合。
当我们想要处理多个intents时,但是只有特定的action/data/category组合,因此我们需要创建多个intent filters。
隐式intent需要通过intent filter检测这三个标签(action/data/category)是否匹配。为了能够将intent传递给该组件,intent必须能够同时通过这三种检测。若有一个检测失败的话,则系统不会将intent传递给该组件。然而,若一个组件有多个intent filters的话,则只要intent能通过这些filters中的一个检测,该intent就会传递给该组件。详细请参考下面的Intent解析部分。
注意:为了避免不经意间运行其它程序的Service,我们总是使用显示Intent启动我们自己的Service,而不应在service中声明intent filters。
对于所有的activities,我们必须声明intent filters。然而,对于broadcast receivers能够通过registerReceiver()
被动态地注册,然后通过unregisterReceiver()
解除。这样做的好处是,我们能够使程序只在指定的时间内响应broadcasts。
限制获得组件
使用intent filter不能够阻止其他程序启动我们的组件。另一个程序能够通过满足intent filter隐式启动intent,和当知道我们的组件类名时通过显示启动intent。若我们不想让其他应用程序启动我们的组件时,可以在<activity>
中设置exported属性为false阻止外界启动我们的组件。
filters例子
代码如下:
<activity android:name=".MainActivity"> <!-- This activity is the main entry, should appear in app launcher --> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter></activity><activity android:name=".ShareActivity"> <!-- This activity handles "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 handles "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>
第一个activity,MainActivity
,是入口activity——当用户使用启动图标初始化启动程序时该activity将被启动:
ACTION_MAIN
行为表名这是程序入口,并且不希望接收任何intent数据。CATEGORY_LAUNCHER
类别表名该activity图标将替换程序的启动图标,也就是首先启动该activity。若<activity
没有指定图标的话,则程序使用<application>
标签的图标。
这两个必须成对出现作为程序的入口。
使用延迟Intent
PendingIntent对象是一个Intent对象的包装类。PendingIntent主要目的是为了保障外部程序使用intent,好像是我们自己的程序进程中执行。
pending intent主要使用场景如下:
- 当用户执行Notification时(系统的NotificationManager执行intent),声明intent被执行。
- 当用户执行APP控件时(Home屏幕执行intent),声明intent被执行。
- 在指定的未来时间执行该intent(例如系统的AlarmManager执行该intent)。
因为每个intent对象被设计成被指定的组件(Activity,Service,BroadcastReceiver
)处理。同样,PendingIntent也需要这样的考虑。当使用pending intent时,我们不能再使用类似startActivity()
执行intent,而是使用下列方法:
PendingIntent.getActivity()
启动Activity。PendingIntent.getService()
启动Service。PendingIntent.getBroadcast()
启动BroadcastReceiver。
除非我们的应用程序需要pending intent,上面创建PendingIntent的方法是我们可能需要的唯一创建PendingIntent方法。
上面的每个方法参数为:当前程序的Context,Intent,flags。
intent解析
当系统接收到隐式intent要启动activity时,系统会基于下面三个方面比较接收到的intent和系统中的intent filters,然后选择最合适的activity响应:
- intent行为
- intent数据(URI和数据类型)
- intent类别
当我们使用startActivity()
或startActivityForRestult()
来启动另外一个Activity时,如果直接指定了Intent对象的Component属性(多种方式,下面有详细介绍),那么系统将启动其Component属性指定的Activity。否则系统将通过Intent的其它属性从安装在系统中的所有Activity中查找与之最匹配的一个启动(或当优先级相同时通过列表让用户自己选择,intent filter优先级设置:使用android:priority属性设置(-1000到1000,必须有个负值才生效)。),如果没有找到合适的Activity,应用程序会得到一个系统抛出的异常。这个匹配的过程如下:
注:
- URI数据即为属性data,此处意思表示必须匹配data或type,若同时出现则必须两者都匹配。
- 三种检测:动作检测(Action),种类检测(Category),数据检测(包括data和type)。
下面我们根据intent filter在manifest文件中的声明来描述intent如何匹配合适的组件。
行为检测
为了指定能接受的intent行为,intent filter可以声明0个或多个行为,如下:
<intent-filter> <action android:name="android.intent.action.EDIT" /> <action android:name="android.intent.action.VIEW" /> ...</intent-filter>
为了能够匹配该filter,指定的intent必须要匹配上述行为中的一个。若filter不包含任何行为,则没有intent能够匹配该filter。但是若intent不指定行为的话,该intent能够通过检测(只要filter至少包含一个行为)。
类别检测
为了指定能接受的intent类别,intent filter可以声明0个或多个类别,如下:
<intent-filter> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> ...</intent-filter>
为了能够匹配该filter,指定的intent必须要匹配上述类别中的一个。反过来则没必要,即filter中指定的类别可能多余intent中设置的类别,但是只要filter中包含所有intent设置的类别的话,该intent就能够通过类别检测。特殊情况,当intent不包含类别时,该intent肯定能通过类别检测。
注:
- 必须和动作一块设置,否则报错。
- 若当前组件是Activity则,必须在intent-filter标签中再增加一个添加默认的类别:
PS:多说一句看准IDE自动补全的是不是android.intent.category.DEFAULT
,提示NO Activity,我在这纠结了半天。 - 在主Activity中设置
android.intent.action.MAIN
和android.intent.category.LAUNCHER
,它们分别标记活动开始新的任务和带到启动列表界面。该Activity可以包含android.intent.category.DEFAULT
,也可以不包含。
数据检测
为了指定能接受的intent数据,intent filter可以声明0个或多个数据,如下:
<intent-filter> <data android:mimeType="video/mpeg" android:scheme="http" ... /> <data android:mimeType="audio/mpeg" android:scheme="http" ... /> ...</intent-filter>
每个数据能够指定URI和数据类型(MIME类型)。URI由多个属性构成,格式如下:
<scheme>://<host>:<port>/<path>
例如:
content://com.example.project:200/folder/subfolder/etc
而在标签中的中有如下主要属性:
android:scheme
指定数据的协议部分,如http、https、tel…,还可以定义自己的前缀android:host
用于指定数据的主机名部分,如www.google.com,如果定义为*
则表示任意主机名android:port
用于指定数据的端口部分,一般紧随在主机名之后。android:path
用于指定主机名和端口之后的部分,如一段网址中跟在域名之后的内容。android:mimeType
用于指定可以处理的数据类型,允许使用通配符(模糊说明,如audio/*
)的方式指定。
host和port一起构成URI的权限(authority),如果host没有指定,port也被忽略。这四个属性都是可选的,但它们之间并不都是完全独立的。要让authority有意义,scheme必须也要指定。要让path有意义,scheme和authority也都必须要指定。即:
- 若scheme没有指定,则host可以省略;
- 若host没有指定,则port可以省略;
- 若host和port都没有指定,则path可以省略。
当比较intent的URI和filter的URI时,仅仅比较filter中出现的URI属性。例如:
- 如果一个filter仅指定了scheme,所有与此scheme的URIs都匹配过滤器;
- 如果一个filter指定了scheme和authority(host和port),但没有指定path,所有匹配scheme和authority的URIs都通过检测,而不管paths;
- 如果四个属性都指定了,必须都匹配才能通过检测。
注意:filter的path指定可以包含通配符(*)来要求匹配path中的一部分。
数据检测既要检测URI,也要检测MIME类型。规则如下:
- 若intent既不包含URI,也不包含MIME类型,则仅当filter也不指定任何URIs和MIME类型时,才能通过检测。
- 若intent包含URI,但不包含MIME类型(既不是显示指定也不能通过URI获得),则仅当它们的URI匹配,同时filter也不指定MIME类型,才能通过检测。
- 若intent包含MIME类型,但不包含URI,则仅当filter也包含相同的MIME类型,同时不包含URI数据,才通过检测。
- 若intent既包含URI,也包含MIME类型(显示指定或从URI推断),则只有与filter中MIME类型的一个匹配才能通过MIME类型匹配。URI检测若想通过则需满足下列情况之一:
- URI要匹配filter中的URI;
- URI包含
content:
或file:
,同时filter没有指定URI。换句话说,若filter仅包含MIME类型,则对应的组件假定支持content:
和file:
。
若不指定URI的话,则组件假定支持content:
和file:
。这说明该组件期望从文件或content provider中获取本地数据,例如:
<intent-filter> <data android:mimeType="image/*" /> ...</intent-filter>
上面的内容表示:组件能够从content provider中获取图片信息。
同时,当我们设置scheme和数据类别时,系统就能够知道如何获取数据,而不用再指定其它的数据属性,例如:
<intent-filter> <data android:scheme="http" android:type="video/*" /> ...</intent-filter>
上面内容表示:组件从网络中获取视频数据。
intent匹配
intent和intent filters匹配不仅寻找处于激活状态的组件,也会寻找设备上的其它组件。PackageManager提供了一系列的query...()
能够返回可以接受指定intent的所有组件(但是不激活组件),resolve...()
方法能够确定最佳的组件。
更多相关文章
- android入门教程(十六)之-- 使用Intent传递数据
- Android中的数据绑定框架DataBinding(对比AngularJS双向数据绑定
- Android数据存储方式
- Android简单数据存储类SharedPreferences详解及实例(通过“记住密