- Android开发权威指南(第二版)
- 李宁编著
- 211字
- 2024-12-21 14:45:35
6.5 Android系统的过滤机制大揭秘
前面的章节已多次提到过Android系统通过一些列过滤机制(Action、Category和Data)调用指定的应用程序组件。到现在为止,我们一直关注窗口(Activity),不过另外两种应用程序组件(服务和广播接收器)也同样可以使用这些过滤机制,这一点在后面的章节会详细介绍。本节将介绍Android系统是如何同时利用这些过滤机制定位应用程序组件的。以及在被调用者没有明确说明的情况下,如何按图索骥获取被调用者的相关信息,例如,哪些窗口可以被调用,数据如何传递等。
6.5.1 过滤条件的设置
尽管现在已经知道了Android通过Action、Category和Data的组合来定位窗口,但这3种过滤机制涉及的规则还是很多的,如果不能充分理解它们,就可能设置了错误的Intent Filter,那么也就无法使用我们想要的方式访问这些窗口了。
窗口过滤条件首先需要从调用者和被调用者两方面考虑。
1.调用者
调用者就是通过Action调用其他程序中窗口的一端。在这一端需要使用Intent的相应方法设置Action、Category和Data,也就是设置调用的窗口要满足的过滤条件。Action和Data只能指定一个(分别通过setAction和setData方法设置),而Category可以指定多个(通过addCategory方法设置)。设置完过滤条件后,通常会使用Activity.startActivity方法显示满足条件的窗口。例如,下面的代码同时指定了Action、Category和Data,并显示窗口。
// 通过Intent类的构造方法指定了Action,也可以通过Intent.setAction方法指定Action
Intent intent = new Intent("android.intent.action.MYACTION");
// 添加了一个Category
intent.addCategory("android.intent.category.MYCATEGORY5");
// 指定了Data。Data通常是指Uri,也可以只MIMEType
intent.setData(Uri.parse("http://192.168.17.100/work/test.jsp"));
// 显示窗口
startActivity(intent);
2.被调用者
被调用者就是如何声明被调用的窗口。在声明一个窗口类时可以不指定过滤器,也可以指定多个过滤器,每一个过滤器用<intent-filter>标签设置。例如,下面的代码就为一个窗口指定了3个过滤器(过滤器的具体代码已省略)。
<activity android:name="MyActivity">
<intent-filter>……</intent-filter>
<intent-filter>……</intent-filter>
<intent-filter>……</intent-filter>
</activity>
在<intent-filter>标签中可以使用<action>、<category>和<data>标签分别设置Action、Category和Data。但要记住<action>标签是必须的,如果不指定<action>,尽管程序可以成功编译和安装,但其他程序将无法通过隐式的方式显示该窗口(不能只指定Category和Data,而不指定Action)。
从常理推测<category>标签应该是可选的,不过实际上<category>也是必须加的,原因就是一个特殊的Category:android.intent.category.DEFAULT。如果调用者不添加Category,按常理会认为过滤条件中不包含Category,系统自然也就不考虑Category了。不过Android系统并不是这样认为的。不管调用者是否添加Category,系统都会认为有一个默认的Category已经被添加。相当于调用者执行如下的代码。
intent.addCategory(Intent.CATEGORY_DEFAULT);
既然调用者默认加入了一个Category,那么被调用这自然也需要在过滤器(<intent-filter>标签)中加入如下的<category>标签了。
<category android:name="android.intent.category.DEFAULT" />
那么有的读者会发现,程序的主窗口的过滤器如下:
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
在这个过滤器中并没有指定android.intent.category.DEFAULT,那么这是怎么回事呢?实际上,这是一个例外,如果android.intent.action.MAIN和android.intent.category.LAUNCHER成对出现,并不需要指定android.intent.category.DEFAULT,当然,也可以指定该Category。其他的Action都必须要指定android.intent.category.DEFAULT。
除了<action>和<category>外,过滤器还可以使用<data>指定更细微的过滤信息。<data>标签是可选的,用于指定Uri或MIMEType。关于<action>、<category>和<data>的具体设置方法会在下一节详细讨论。
6.5.2 过滤器的筛选规则
窗口类的每一个过滤器(<intent-filter>标签)都会在系统中注册,然后系统再根据Intent对象设置的过滤条件筛选符合条件的窗口时会逐一扫描这些过滤器,并会分别根据Action、Category和Data筛选出所有符合条件的过滤器,如果这些被筛选出的过滤器属于多个窗口,系统就会显示一个选择列表,如果只属于一个窗口,就直接显示该窗口。
筛选规则主要应从两个方面考虑。首先应考虑Action、Category和Data在默认情况下代表什么含义。也就是在不设置这3个过滤条件的情况下,系统会如何处理。
不设置Action。由于Action是必选项,所以必须要设置Action,否则该窗口无法被其他的Android应用调用(同一个Android应用还是可以通过显式方式调用的)。
不设置Category。系统会添加一个默认的Category(android.intent.category.DEFAULT)。
不设置Data。系统会认为目标窗口不含有任何Data,也就是说如果不为Intent对象指定Data,在声明目标窗口时也不能在过滤器中使用<data>标签。否则即使Action和Category都匹配,该过滤器也不符合条件。
要考虑的第二个方面是Action、Category和Data如何进行匹配。系统会分别进行3次验证(分别验证这3个过滤条件),只有这3次验证都通过的过滤器才符合要求(该过滤器对应的窗口会成为候选窗口之一)。下面是这3个过滤条件的验证规则。
1.Action验证
如果某个过滤器指定了一个或多个<action>标签(如果没有<action>标签或Intent对象未设置Action,Action验证肯定失败)。系统会扫描所有的<action>标签,如果找到与指定Action相同的<action>标签,就认为Action验证通过。例如,调用者使用下面的代码设置了一个Action(MYACTION1)。
Intent intent = new Intent();
intent.setAction("MYACTION1");
系统中有一个过滤器的代码如下:
<intent-filter>
<action android:name="MYACTION1"/>
<action android:name="MYACTION2"/>
<action android:name="MYACTION3"/>
……
</intent-filter>
在该过滤器中恰好有一个<action>标签的android:name属性值是MYACTION1,所以该过滤器通过了Action验证。
2.Category验证
Category验证采用了子集的方法,也就是说为Intent对象指定了N个Category,而在某一个过滤器中设置了M个Category,并且M >= N。那么如果N个Category是M个Category的子集,那么该过滤器通过Category验证。否则不管是M < N,还是其他任何情况,Category验证都失败。例如,下面的代码为Intent对象设置了3个Category。
Intent intent = new Intent();
intent.addCategory("MYCATEGORY1");
intent.addCategory("MYCATEGORY2");
intent.addCategory("MYCATEGORY3");
下面有两个过滤器,代码如下:
①号过滤器。
<intent-filter>
<category android:name="MYCATEGORY1" />
<category android:name="MYCATEGORY2" />
<category android:name="MYCATEGORY3" />
<category android:name="MYCATEGORY4" />
……
</intent-filter>
②号过滤器。
<intent-filter>
<category android:name="MYCATEGORY1" />
<category android:name="MYCATEGORY2" />
<category android:name="MYCATEGORY4" />
<category android:name="MYCATEGORY5" />
……
</intent-filter>
由于①号过滤器包含了Intent对象设置的3个Category,所以第一个过滤器通过了Category验证。但②号过滤器由于缺少MYCATEGORY3,所以没有通过Category验证。
3.Data验证
Data验证用于验证Uri和Mime Type。Uri比Url范围更广,除了可表示Web地址外,还可以表示如下格式的字符串。
scheme://host:port/path
从这个字符串看,将Uri分成了如下4部分。
scheme:协议标识,例如,http、https、content、ftp等。
host:域名、IP等。例如www.google.com、192.168.17.104等。
port:端口号,例如,80、8080等。
path:资源的具体路径。
下面是一个标准的Uri。
http://www.microsoft.com:8080/index.html
Uri的4个组成部分如下。
scheme:http。
host:www.microsoft.com。
port:8080。
path:/index.html。
这里的path指的是完整的路径,但除了可以设置完整的路径外,还可以使用如下两种路径匹配方式。
pathPrefix:路径前缀。所有以pathPrefix开头的path都满足条件。例如“/in”可以匹配“/index.html”,也可以匹配“/in.jsp”。
pathPattern:路径匹配模式。可以使用“*”或“.*”(星号前面加一个点)匹配相应的路径。“*”表示在“*”前面的字符会出现0个或任意多个。“.*”表示0个或任意多个字符。例如,“abcd*e”可以匹配“abce”、“abcde”、“abcdde”、“abcddde”,也就是说在“abc”和“e”之间可以有0或任意多个“d”。而“ab.*cd”可以匹配“abcd”、“abxyzcd”、“ab123cd”,也就是说在“ab”和“cd”之间可以有0或多个任意字符。
注意
不仅pathPattern属性可以使用通配符,host属性也可以使用“*”表示匹配所有的host(host属性值只能包含“*”,不能包含其他的字符)。
现在我们已经知道一个Uri实际上由6个属性确定,其中前3个是scheme、host和port,还有3个路径(path、pathPrefix和pathPattern)。在一个过滤器(<intent-filter>标签)中可以有多个Data(用<data>标签定义)。系统在匹配每一个Data时会同时考虑这6个属性,但需要遵循如下规则。
(1)scheme是必须的,所以无论是调用者,还是被调用者,如果设置了Uri,必须指定scheme,否则Data验证一定失败。
(2)属性不能跳跃设置,否则跳跃设置的属性将被忽略。例如,设置scheme后直接设置了port,而host并未设置,这被称为跳跃设置。这种情况下port将被忽略,相当于从未设置port属性。当然,如果未设置host,后面再设置path、pathPrefix、pathPattern属性也同样会被忽略。还有就是不管中间跳跃几个属性,跳跃设置的属性同样会被忽略。例如,设置scheme后,host和port都未设置,而直接设置了path,那么path将被忽略。
(3)系统在检测每一个<data>标签时会根据依次检测每一个属性。例如,Intent对象指定的Uri包含scheme、host、port和path。那么系统会在当前的<data>标签中依次检测这4个属性(scheme、host、port和path)是否符合要求(path会检测3个属性:path、pathPrefix和pathPattern,这3个属性只要有一个通过即可)。只有同时符合要求,才会通过Data验证,也就是说这4个属性是“与”的关系。
(4)系统会检索过滤器中的所有Data来查找相应的属性。例如,在一个过滤器中定义了3个Data,其中一个只指定了scheme和host,而另两个Data中只指定了port属性(两个Data指定了不同的属性)。而Uri是http://www.microsoft.com:8080。在Uri中包含了scheme、host和port。系统在验证第一个Data时自然要考虑这3个属性,但scheme和host都通过验证后,发现第一个Data中并没有设置port,这时系统并不认为验证失败,而是看看当前过滤器的其他Data中是否设置了port属性。恰好发现另外两个Data都设置了port属性,系统会分别拿这两个port属性值逐个与Uri中的端口号(8080)比较,只要有一个port是8080,就会通过Data验证。
(5)如果同时指定了path、pathPrefix和pathPattern属性,系统会依次处理这3个属性,直到一个属性的验证通过为止。也就是说这3个属性直接的关系是“或”,而不是“与”。
(6)设置path和pathPrefix属性时必须以“/”开头,例如“/index.html”、“/abc”等。pathPattern属性值如果以“.*”开头,则不需要加“/”,因为“.*”已经包含“/”了。但如果不以“.*”开头,则pathPattern属性值必须以“/”开头。
(7)如果Uri没有指定所有的过滤条件,例如,只指定了scheme和host,那么Data也不能设置Uri未指定的属性。但由于跳跃设置被忽略的属性除外(反正被忽略了,指不指定无所谓)。从这一点可以看出,在定义过滤器时,Data的过滤条件不能超过Uri中包含的信息,否则Data验证必然失败。
(8)如果Uri指定的过滤条件比Data多,并且Uri与Data都指定的过滤条件都通过了验证,则通过Data验证。例如,Uri指定了scheme、host和port,而Data只指定了scheme。如果两个scheme相同,系统则认为通过Data验证。
(9)如果同时设置了Uri与Mime Type(稍后会介绍Mime Type)。系统会分别验证Uri和Mime Type。如果这两个过滤条件都设置在了一个Data上,那么必须都满足条件才通过Data验证。如果Uri与Mime Type分属不同的两个Data,并且这两个Data分别只设置了Uri和Mime Type。系统会分别处理Uri和Mime Type。只要这两个Data的Uri和Mime Type分别满足条件就认为通过了Data认证。
(10)Uri是大小写敏感的,这一点与平常使用的Uri不同。
注意
要牢记上面的几条筛选规则,在下一节会给出一个筛选过滤器的例子,到时读者可以对照这几条以及前面介绍的Action和Category验证规则来研究这个例子。
特别要注意的是Data不仅指Uri,也指Mime Type。Uri和Mime Type可以同时使用,也可以单独使用一个。如果同时使用Uri和Mime Type,要使用Intent.setDataAndType方法进行设置,否则Uri和Mime Type会互相覆盖,这一点在6.2.5小节已经详细介绍过了。
Mime Type就是指可以处理的资源类型,例如“audio/ *”用于处理所有的音频资源,“audio/mp3”只用于处理MP3音频格式,“video/ *”用于处理所有的视频资源。在Data中使用mimeType属性设置Mime Type。下面所示一个标准的Data,在该Data中同时指定了Uri和Mime Type。这种情况下只有Uri和Mime Type同时通过验证,Data验证才算通过。
<data
android:host="*"
android:port="8080"
android:path="/work/upload.jsp"
android:scheme="http"
android:mimeType="audio/ *"
/>
综上所述,Data验证要想通过,需要同时通过如下3种验证。
scheme、host、port、path(包括pathPrefix和pathPattern)。这4个属性的关系是“与”,只有同时满足这4个属性,才算通过验证。
path、pathPrefix和pathPattern。这3个属性的关系是“或”,只要通过一个属性就算通过验证。
Uri(scheme、host、port、path、pathPrefix和pathPattern)和Mime Type。关系是“与”,如果同时设置了Uri和Mime Type,必须同时满足条件才算通过验证。当然,如果Intent未设置Uri或Mime Type,而Data设置了Uri或Mime Type,验证同样会失败,反之亦然。
6.5.3 示例:过滤机制应用演示
源代码目录:src/ch06/FilterProducer、src/ch06/FilterConsumer
有可能很多读者看了前两节关于过滤条件和筛选规则的介绍会感到比较复杂,不过有些筛选规则并不经常使用。例如,path、pathPrefix和pathPattern中使用path会多一些,甚至大多数时候path都很少用。通常Uri只指定scheme,顶多与Mime Type一起使用。
为了使读者能更好地理解Android系统的过滤机制,本节会给出一个例子来看看系统是如何来匹配不同过滤器的。本例由两个Android应用组成,其中FilterProducer中的主窗口类(FilterProducerActivity)设置了多个过滤器,FilterConsumer会通过不同的过滤条件调用FilterProducer的主窗口。
在FilterProducer中为主窗口类设置了7个过滤器,其中第1个过滤器是用于启动窗口的,不需要考虑。后面6个过滤器将被FilterConsumer程序中的6个按钮调用。过滤器的定义代码如下:
源代码文件:src/ch06/FilterProducer/AndroidManifest.xml
<activity
android:name="mobile.android.filter.producer.FilterProducerActivity"
android:label="@string/app_name" >
<!-- ①号过滤器:用于启动窗口,在这里不需要考虑 -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- ②号过滤器-->
<intent-filter>
<action android:name="android.intent.action.MYACTION1" />
<action android:name="android.intent.action.MYACTION2" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MYCATEGORY1" />
<category android:name="android.intent.category.MYCATEGORY2" />
</intent-filter>
<!-- ③号过滤器-->
<intent-filter>
<action android:name="android.intent.action.MYACTION3" />
<action android:name="android.intent.action.MYACTION4" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MYCATEGORY3" />
<category android:name="android.intent.category.MYCATEGORY4" />
</intent-filter>
<!-- ④号过滤器-->
<intent-filter>
<action android:name="android.intent.action.MYACTION5" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MYCATEGORY5" />
<!--
下面给出一个符合条件的Uri,只匹配scheme,Uri其他部分随意指定
http://www.microsoft.com:8888/index.html
-->
<data android:scheme="http" />
<data android:mimeType="audio/ *" />
<data android:mimeType="video/ *" />
</intent-filter>
<!-- ⑤号过滤器-->
<intent-filter>
<action android:name="android.intent.action.MYACTION6" />
<category android:name="android.intent.category.DEFAULT" />
<!--
下面是一个符合条件的Uri
http://www.google.com:8080/work/upload.jsp
-->
<data
android:host="*"
android:mimeType="audio/ *"
android:path="/work/upload.jsp"
android:port="8080"
android:scheme="http" />
</intent-filter>
<!-- ⑥号过滤器-->
<intent-filter>
<action android:name="android.intent.action.MYACTION7" />
<category android:name="android.intent.category.DEFAULT" />
<!--
下面是一个符合条件的Uri
ftp://192.168.17.100:8080/work/upload.html
-->
<data
android:host="192.168.17.100"
android:mimeType="audio/ *"
android:port="8080"
android:scheme="ftp" />
<data android:pathPattern=".*.html" />
</intent-filter>
<!-- ⑦号过滤器-->
<intent-filter>
<action android:name="android.intent.action.MYACTION7" />
<category android:name="android.intent.category.DEFAULT" />
<!--
下面是3个符合条件的Uri
path属性验证通过
https://192.168.17.111:8888/work/test.up
pathPattern属性验证通过
https://192.168.17.111:8888/p/m/abc.html
pathPrefix属性验证通过
https://192.168.17.111:8888/test/up.aspx
-->
<data
android:host="192.168.17.111"
android:mimeType="audio/ *"
android:path="/work/test.up"
android:pathPattern=".*.html"
android:pathPrefix="/test"
android:port="8888"
android:scheme="https" />
</intent-filter>
</activity>
下面看一下FilterConsumer类是如何为这6个过滤器的设置筛选条件的。
源代码文件:src/ch06/FilterConsumer/src/mobile/android/filter/consumer/FilterConsumerActivity.java
public class FilterConsumerActivity extends Activity
{
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_filter_consumer);
}
// 第②号过滤器通过验证,利用了Action和Category验证规则,未使用Data
public void onClick_IntentFilter2(View view)
{
Intent intent = new Intent("android.intent.action.MYACTION1");
// 在这里可以不指定android.intent.category.DEFAULT
intent.addCategory("android.intent.category.MYCATEGORY1");
intent.addCategory("android.intent.category.MYCATEGORY2");
startActivity(intent);
}
// ③号过滤器通过验证,未指定Category
// 因为默认已经为Intent对象添加了android.intent.category.DEFAULT
public void onClick_IntentFilter3(View view)
{
Intent intent = new Intent("android.intent.action.MYACTION4");
startActivity(intent);
}
// ④号过滤器通过验证,Uri匹配了scheme(http),并且匹配了第1个Data(“audio/ *”)
// 在进行Data验证时利用了Data验证规则的第8条
public void onClick_IntentFilter4(View view)
{
Intent intent = new Intent("android.intent.action.MYACTION5");
intent.setDataAndType(Uri.parse("http://www.microsoft.com:8888/index.html"), "audio/mp3");
startActivity(intent);
}
// ⑤号过滤器通过验证
// 在进行Data验证时利用了Data验证规则的第3、9条
public void onClick_IntentFilter5(View view)
{
Intent intent = new Intent("android.intent.action.MYACTION6");
intent.setDataAndType(Uri.parse("http://www.google.com:8080/work/upload.jsp"), "audio/ *");
startActivity(intent);
}
// ⑥号过滤器通过验证
//在进行Data验证时利用了Data验证规则的第3、4、9条
public void onClick_IntentFilter6(View view)
{
Intent intent = new Intent("android.intent.action.MYACTION7");
intent.setDataAndType(Uri.parse("ftp://192.168.17.100:8080/work/upload.html"), "audio/ *");
startActivity(intent);
}
// ⑦号过滤器通过验证
// 在进行Data验证时利用了Data验证规则的第3、5、9条
// 下面的代码调用了3次setDataAndType方法,分别设置了3个Uri,其中前两次加了注释,
// 这3个Uri都符合要求,Uri的路径分别匹配path、pathPrefix和pahtPattern
public void onClick_IntentFilter7(View view)
{
Intent intent = new Intent("android.intent.action.MYACTION7");
//intent.setDataAndType(Uri.parse("https://192.168.17.111:8888/work/test.up"), "audio/wav");
//intent.setDataAndType(Uri.parse("https://192.168.17.111:8888/test/up.aspx"), "audio/wav");
intent.setDataAndType(Uri.parse("https://192.168.17.111:8888/p/m/abc.html"), "audio/wav");
startActivity(intent);
}
}
测试本例首先应安装FilterProducer,然后运行FilterConsumer,并单击主界面上的6个按钮来测试系统的过滤器筛选功能。