Intent是一个我们可以用来请求操作其它组件的消息体。虽然intent使用不同的方式在组件间通讯,但是主要是三个基本的用法:
1.启动activity
app中一个activity代表着一个单独的屏幕。我们可以传递intent给startActivity方法来启动一个activity的实例。intent事有要启动activity的信息和需要的数据。如果我们想要在activity finish的时候再接收一个结果,请使用startActivityForResult.我们的activity会在自己的onActivityResult回调方法中收到一个单独的intent。想了解更多,请参阅activity.
2.启动一个service
Service是一个没有用户界面的后台运行处理任务的组件。我们可以传递intent给startService来启动一个service来执行一次性的任务(比如下载文件)。Intent带有要启动service的信息和必要的数据。
如果Service有一个client-server的接口,我们可以传递intent给bindService来从其它组件中绑定服务。想了解更多,请参阅服务。
3.传递一个广播
广播是任何app都可以接收的消息。系统会为系统事件(比如,开机,充电)发送各种各样的广播。我们可以传递intent给sendBroadcast(), sendOrderedBroadcast(),或者sendStickyBroadcast()来发送广播。
Intent类型
两种intent类型:
1.显式intent是指定需要启动组件的name(包括包名的全名称)。通常启动自己app的组件都使用显式intent,因为我们知道要启动activity或者service的名字。比如,响应用户的操作去启动一个activity或者在后台下载一个文件。
2.隐式intent不需要指定组件的name,但是需要声明一些要执行的操作来让其它app的组件去执行。比如,想给用户展示一个地图上的位置,我们可以使用隐式intent来请求另外一个有这种功能的app去展示位置。
图片中展示了隐式intent是如何在系统中传递并启动另外一个activity.
1.Activity A 创建了一个带有action的intent传递给startActivity。
2.系统搜索所有的app的intent filter来匹配这个intent.
3.当找一个匹配的,系统会启动这个ActivityB并调用它的onCreate传递intent作为参数。
当我们创建一个隐式intent,系统会通过比较intent的内容和设备上其它app在清单文件中声明的intent filters来找到一个合适的组件去启动。如果找到了一个,系统会启动这个组件并传递这个intent.如果找到多个匹配的intent filters,系统会弹一个对话框来让用户选择。
intent filters是在清单文件中用于指定组件想要接收的intent类型的描述。比如,声明一个activity的intent filters, 这样其它app就可以直接使用一定的intent来启动我们的activity.同样的,如果我们没有为activity声明intent filters,它就只能通过显式intent启动。
注意:为了保证app的安全,使用显式intent来启动一个service,不要为service声明intent filters. 使用隐式intent来启动service是有安全漏洞的,因为我们不能确定哪个service会响应这个intent,同时用户也无法看到是哪个service启动了。API21开始,如果使用隐式intent来绑定服务系统会抛异常。
创建一个Intent
intent包含了两部分信息,一是系统用来决定启动哪个组件的信息(比如组件的名称或者接收intent的组件类别),另外就是接收组件执行操作需要使用到的信息(比如操作的类型和需要操作的数据)。
intent包含的主要信息是下边的:
1.Component name
要启动组件的名称。
这个是可选的,但是对于显式intent来说它是非常重要的,它表明了intent要传递给哪个组件。如果没有名称,intent就是隐式的,系统通过intent的其它信息(比如,action,data或者category)来决定哪个组件接收.所以,如果需要望到一个自己app的组件,一定要指定名称。
注意:当启动一个service时,我们应该总是指定名称,否则,我们不能确定哪个service会来响应intent,并且用户无法感知哪个service启动了。
intent的这个属性是一个ComponentName对象,我们可以使用目标组件的类名全称,包括app的包名。比如com.example.ExampleActivity. 我们可以通过setComponent(), setClass(), setClassName(),或者构造来设置component name。
2.Action
指定需要进行的操作(比如查看或者选择)的字符串。
在广播的intent中,Action是已经发生并且正在被报道的操作。Action主要决定了其它的部分(主要是数据和extra)的结构。
我们也可以在应用内自定义intent来让自己的app或者其它app来调用我们的组件,但是我们应该使用Intent类或者其它framework类的静态常量。下面是一些启动activity的常用Action:
a.ACTION_VIEW
当我们有信息需要展示给用户时可以使用这个action.比如图库app的查看相片,或者地图app查看地址。
b.ACTION_SEND
也可以叫做分享intent,当用户需要把一些数据通过其它app分享出去的时候,使用startActivity和带有这个action的intent.比如一个邮件app或者一个社交分享app.
查看Intent类可以看到更多的可以用来定义action的常量。其它的action在framework中定义,比如一些设置的action可以打开系统设置app的某个界面。
我们可以通过intent的setAction或者构造来设置action.
如果自定义acction,一定要使用包名作为前缀,比如:
static final String ACTION_TIMETRAVEL = "com.example.action.TIMETRAVEL";
3.数据
要操作的数据或者数据的类型的URI。数据类型通常是由action来决定的。比如,如果是ACTION_EDIT,那么数据就一定是一个需要编辑的文件的URI.
创建intent时,指定数据类型通常也是很重要的。比如,即使URI非常相似,一个可以展示图片的activity通常不能播放音频。所以指定数据类型可以帮助系统找到最合适的组件。但是数据类型有的时候是可以通过URI来推断的。特别是当数据的URI是content:,这表明了这个数据是在设备的一个contentProvider,所以它的数据类型对系统是可见的。
使用setData来设置数据,使用setType来设置数据类型。必要时,使用setDataAndType来明确它们两个。
注意:如果想要设置两个,不能分别调用setData和setType,因为它们会把对方重置,一定要使用setDataAndType。
4.类型
一个包含了有关哪种组件可以处理这个intent的字符串。一个intent可以包含任意数量的category,但是很我intent不需要category.下边是一些常用的category:
a.CATEGORY_BROWSABLE
目标activity允许自己被浏览器的链接启动,比如一个图片或者邮件的链接。
b.CATEGORY_LAUNCHER
一个任务栈的初始activity,同时也会被列入系统启动图标。
查看Intent类来了解更多。
可以使用addCategory来指定一个category。
上边列出的(component name, action, data, 和 category)是intent的主要组成部分。通过这些,系统可以解析出来启动哪个组件。
但是,intent还可以携带一些不影响解析的信息,比如:
Extras
键值对携带了一些完成操作需要的信息。和一些操作需要使用特定的数据URI一样,一些操作也需要使用特定的extras.
通过putExtra来添加extra数据,每次有两个参数,key和value。也可以把所有的extra data放到一个bundle对象中,然后使用putExtras来传递这个bundle.
比如,创建intent来发送邮件时,我们会使用ACTION_SEND,可以使用EXTRA_EMAIL来指定接收者,EXTRA_SUBJECT来指定主题。
Intent类有很多EXTRA_常量来给那些标准的数据类型服务。如果需要自定义key,请使用包名作为前缀。比如:
static final String EXTRA_GIGAWATTS = "com.example.EXTRA_GIGAWATTS";
Flags
在intent类中定义,充当了intent的元数据。falg可以指导系统如何启动一个activity(比如,activity应该在哪个任务栈),启动后如何把处理(比如,是否要把它放到最近启动的任务列表)
参考setFlags来了解更多。
显式intent示例
显式intent必须指定一个启动的组件。比如,一个应用中的activity或者service.定义显式intent时,必须定义组件名称,其它的属性都是可选的。
比如,应用中有一个叫做DownloadService的service,它可以从网络下载文件。我们可以使用下边的代码来启动它:
// Executed in an Activity, so 'this' is the Context
// The fileUrl is a string URL, such as "http://www.example.com/image.png"
Intent downloadIntent = new Intent(this, DownloadService.class);
downloadIntent.setData(Uri.parse(fileUrl));
startService(downloadIntent);
Intent(Context, Class)构造提供了app的context和组件的Class对象.这样就可以显式的启动应用的DownloadService.
参阅Services了解更多关于创建和启动service.
隐式intent示例
隐式intent可以启动设备上任何app,如果app可以执行intent指定的action.
当我们的app不能执行某个操作时,其它app很能可以执行,我们可能希望用户来选择哪个app去执行。这时使用隐式intent非常有用,
比如,我们想让用户分享一些东西给其它人,创建一个带有ACTION_SEND的action和那些需要分享的内容的intent。然后使用startActivity调用刚才的intent,用户可以选择一个app来分享内容。
注意:用户可能没有任何一个app可以处理我们发送给startActivity的intent。这时,调用失败,app会崩溃。为了校验是否有一个activity可以接收这个intent,可以使用resolveActivity来验证。如果返回值非空,那么至少有一个app可以处理intent,调用startActivity就是安全的。如果返回值为空,我们就不应该使用这个intent,可能的话也要禁用创建这个intent的功能。
// Create the text message with a string
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
sendIntent.putExtra(Intent.EXTRA_TEXT, textMessage);
sendIntent.setType("text/plain");
// Verify that the intent will resolve to an activity
if (sendIntent.resolveActivity(getPackageManager()) != null) {
startActivity(sendIntent);
}
备注:示例中,没有使用URI,但是使用了数据类型来限定extras的内容。
当startActivity调用时,系统会检查所有已安装的app来决定使用哪个来处理当前的intent(带有ACTION_SEND的action和数据类型为"text/plain").如果只有一个app可以处理,这个app就会启动,然后接收到这个intent。如果有多个activity可以接收这个intent,系统会弹出一个对话框来让用户选择一个。
强制使用一个app选择器
当有多个app可以响应我们的隐式intent时,用户可以选择一个app使用,并且设置它为默认处理这个action的app.这是很方便的,因为对于同一个action,用户很可能想使用同一个app来处理,比如打开一个网页(用户通常使用一个浏览器)。
但是,如果有多个app可以响应intent,用户也可能想每次都使用不同的app,这时我们需要明确的弹出一个选择对话框。对话框来引导用户每次选择一个应用(用户不能为这个action选择一个默认的app).比如,当我们的app使用ACTION_SEND的action来分享时,用户可能需要根据当前的情况来选一个不同的app,所以我们应该使用选择器。
使用createChooser创建intent然后传递给startActivity,就可以展示出选择器。比如:
Intent sendIntent = new Intent(Intent.ACTION_SEND);
...
// Always use string resources for UI text.
// This says something like "Share this photo with"
String title = getResources().getString(R.string.chooser_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);
}
这会弹出一个对话框,内容列出了可以响应传递给createChooser的intent,标题使用了提供的文本。
接收一个隐式intent
为了告知系统,我们的app可以接收哪些隐式intent,我们可以在清单文件中自己的组件节点里添加
备注:显式intent总是直接传递给目标,不论组件是否声明了intent filters。
app组件应该为每个它可以做的工作都单独声明一个filter. 比如,图库app的一个activity可能有两个filter:一个可以查看相片,一个可以编辑相片。当activity启动时,它会检查intent,然后根据intent的信息(比如是否显示编辑器)来决定如何执行。
每个intent filter都是在app的清单文件中对应的组件节点(比如
1.
在name属性下声明可以接收的intent action。name的值必须是字符串,不可以是类里的静态常量。
2.
使用一个或多个属性来指定数据的URI(scheme, host, port, path等)和类型,声明可以接收的数据类型。
3.
在name属性下声明可以接收的intent category。name的值必须是字符串,不可以是类里的静态常量。
备注:为了可以接收隐式intent,必须在intent filter中声明 CATEGORY_DEFAULT category。startActivity和startActivityForResult会认为所有的intent都有 CATEGORY_DEFAULT category。如果没有为我们的intent filter声明 CATEGORY_DEFAULT category,我们的activity就不会响应任何隐式intent.
比如,下边是一个activity声明intent filter来接收ACTION_SEND和数据类型是text的intent:
一个filter包含多个
当我们想要处理多种类型的intent时,但是只有特定的action,data和category组合才可以时,我们就需要创建多个intent filters.
限制访问组件
使用intent filter不是一个阻止其它应用启动我们的组件的安全方法。虽然intent filters可以限制我们组件只能响应一些固定类型的隐式intent,但是如果开发者知道我们的组件名称,那么他们就可以使用显式intent来启动。如果确实需要只让自己的app来启动组件,就需要把exported属性设置为false.
隐式intent是通过逐一比较intent和intent filter的三个节点来测试的。intent必须通过三个测试才能传递给组件。哪怕只有一个没有通过,系统也不会把Intent传递给组件。但是,因为组件可能有多个intent filters,所以intent没有通过一个intent filter不代表它不能通过另外一个。参阅下边的Intent Resolution来了解更多。
注意:为了避免无意中启动了另外一个app的service,总是使用显式intent来启动自己的service,不要给service设置intent filters.
备注:所有的activity都必须声明intent filters. 但是,广播接收者的intent filters可以通过registerReceiver来动态的添加。然后使用unregisterReceiver来取消订阅。这么做可以让我们的app在运行时的固定时间接收固定的广播。
filters示例
为了更好地理解intent filter,请看从一个社交app的清单文件中节选的一段代码:
第一个MainActivity是app的入口,用户点击桌面图标启动的就是它:
- ACTION_MAIN表明这是app的主入口,并且不要求intent数据。
- CATEGORY_LAUNCHER category表明这个activity的icon应该放到系统的启动器中,如果activity没有指定icon,系统会使用application的icon.
两个必须一起配置才能让activity出现在app启动器中。
第二个ShareActivity是来方便分享文本和多媒体内容的。虽然用户可以从MainActivity进入到这个activity,用户也可能从其它app中使用一个可以匹配这两个intent filter的intent来直接进入。
备注:类型application/vnd.google.panorama360+jpg,是一个特殊的数据类型,我们可以使用google全景的api来处理它。
使用Pending Intent
Pending Intent是intent的包装器。使用pendingIntent的主要目的是授权其它应用处理intent,就好像在我们自己的进程处理一样。
使用pending intent的主要场景:
1.当用户需要使用Notification时(系统的NotificationManager来执行这个intent).
2.当用户需要使用App Widget(系统的screen app来执行这个intent).
3.当用户需要执行一个定时任务时(系统的AlarmManager来执行这个intent).
因为每个intent都是被特定的组件处理的(activity,service或者broadcastreceiver),所以pendingIntent创建时也需要考虑这些。使用pending intent时,我们的app是不会通过像startActivity这样的方式来调用的。所以我们应该在创建pending intent时声明哪些组件来处理:
- PendingIntent.getActivity() 是启动activity。
- PendingIntent.getService() 是启动service。
- PendingIntent.getBroadcast()是启动BroadcastReceiver.
除非我们的app接收其它app的pending intent,否则上边的方法就是我们在创建pendingintent时,唯一需要使用的方法。
每个方法都会取得当前app的context,intent和一个或多个指定intent使用方式(比如一个intent是否可以重复使用)的flag.
参阅Notification和app widgets可以了解更多。
Intent解析
当系统接收到一个隐式intent启动activity时,它会通过根据下列三个方面来比较intent和intent filters来搜索最合适的activity.
The intent action
The intent data (both URI and data type)
The intent category
下边分析intent filter是如何在清单文件中声明及intent是如何匹配到合适的组件。
Action 测试
要指定接收的actions,intent filter可以声明0个或多个
...
为了通过这个filter测试,intent的action必须匹配filter中的一个action.
如果filter没有列出任何一个action, 就没有东西给intent来匹配,所有的intent就会测试失败。但是如果intent也没有指定action,就会通过测试(只要filter包含至少一个action)。
Category 测试
要指定接收的categories,intent filter可以声明0个或多个
...
intent想要通过category测试,intent中的每个category都需要匹配filter中的一个category。反方向不是必须的,intent filter声明的category可以多intent指定的多。当然, 不论filter声明的是什么category,没有category的intent总是可以通过测试.
备注:系统会为所有传递给startActivity和startActivityForResult的隐式intent添加一个CATEGORY_DEFAULT category,所以如果想让我们的activity来接收隐式intent,必须在intent filter中声明一个"android.intent.category.DEFAULT"。
Data 测试
要指定接收的data,intent filter可以声明0个或多个节点,比如:
...
每个data节点都可以声明一个URI和数据类型(MIME).每个URI可以有不同的属性:scheme, host, port, and path.
比如:
content://com.example.project:200/folder/subfolder/etc
这个URI中,content是scheme,com.example.project是host,200是port,folder/subfolder/etc是path.
每个属性都是可选的,但是却有线性依赖:
1.如没有指定scheme,host会被忽略。
2.如没有指定host,port会被忽略。
3.如没有指定scheme和host,path会被忽略。
intent和URI和filter的URI比较时,只比较filter中URI的一部分。比如:
1.如果filter只指定了scheme,所有带有这个scheme的URI都通过测试。
2.如果filter指定了scheme和authority,所有带有这个scheme和authority的URI都通过测试。
3.如果filter指定了scheme,authority和path,所有带有这个scheme,authority和path的URI都通过测试。
备注:path可以包含通配符(*)来匹配一部分的path名称。
数据测试会比较URI和MIME类型。规则如下:
1.只有当filter没有指定任何URI和MIME时,没有URI和MIME的intent才可以通过测试。
2.包含URI但是没有MIME(既没有显式声明,也不能根据URI推断出来)的intent,只能通过那些filter带有匹配URI并且也没有指定MIME的测试。
3.有MIME但没有URI的intent,只能通过那些filter列举相同MIME并且没有指定URI的测试。
4.包含URI和MIME(显式声明,或者根据URI推断出来)的intent,匹配filter的类型通过MIME测试,如果它的URI匹配filter的URI,或者filter没有URI,它有一个contet:或者file:的URI,这样它就通过了URI测试。也就是说,如果一个filter只有MIME类型,那么它默认支持content:和file:的data.
最后一条规则,说明了组件可以取得本地的文件和content provider数据。所以,它们的filter可以只列举data type,不需要显式声明content:和file:的scheme.下边是一个典型的示例,一个组件可以从content provider取得图片并展示:
...
因为大部分的数据都是由content provider提供的,所以filter只指定数据类型没有URI是非常常见的。
另外一个常见的filter配置,有scheme和data type,比如:一个组件可以从网络取得视频数据:
...
Intent 匹配
匹配intent不仅仅是为了找到一个可以启动的组件。也可以发现设备上组件的信息。比如,Home app是通过找到所有指定了ACTION_MAIN action 和CATEGORY_LAUNCHER category的filter的activity来填充界面的。
我们的app也可以使用类似的方式。PackageManager有一组query...()的方法可以返回所有接收固定intent的组件和类似的resolve...()方法来返回最合适的组件。比如queryIntentActivities返回可以执行intent参数的activity.queryIntentServices返回类似的一组service.这些方法都不会启动组件。他们只是列出可以响应的组件。对于broadcast receivers也有对应的queryBroadcastReceivers.