当 Intent 碰上 Uri

源自https://unsplash.com/

问题描述

最近的工作中,涉及到在AndroidBroadcastReceiver的使用,这本是Android开发中常见的场景。但是当我用来隐式启动BroadcastReceiverIntent中通过setData方法携带了一个Uri的时候,BroadcastReceiver却无法被Intent唤醒了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Set the data this intent is operating on. This method automatically
* clears any type that was previously set by {@link #setType} or
* {@link #setTypeAndNormalize}.
*
* <p><em>Note: scheme matching in the Android framework is
* case-sensitive, unlike the formal RFC. As a result,
* you should always write your Uri with a lower case scheme,
* or use {@link Uri#normalizeScheme} or
* {@link #setDataAndNormalize}
* to ensure that the scheme is converted to lower case.</em>
*
* @param data The Uri of the data this intent is now targeting.
*
* @return Returns the same Intent object, for chaining multiple calls
* into a single statement.
*
* @see #getData
* @see #setDataAndNormalize
* @see android.net.Uri#normalizeScheme()
*/
public @NonNull Intent setData(@Nullable Uri data) {
mData = data;
mType = null;
return this;
}

经过测试之后,发现如果Intent中同时设置了ActionUri的时候,Action相当于是失效状态,这个不光是涉及到

BroadcastReceiver,连Activity的表现也是如此。

发现了问题,当然要解决呀,下面是经过复盘后精简出的两个可以验证失效的最小场景:

BroadcastReceiver 的隐式启动

首先,在AndroidManifest中注册我们的BroadcastReceiver及其intent-filter:

1
2
3
4
5
6
7
<receiver
android:exported="true"
android:name=".EmptyReceiver">
<intent-filter>
<action android:name="com.test.ACTION_RECEIVER"/>
</intent-filter>
</receiver>

然后,在Kotlin中尝试用如下的代码来启动EmptyReceiver:

1
2
3
sendBroadcast(Intent("com.test.ACTION_RECEIVER").apply {
data = Uri.parse("https://blog.lstec.org")
})

之后就会发现这个BroadcastReceiver并没有被启动。

Activity 的隐式启动

首先,在AndroidManifest中注册我们的Activity及其intent-filter:

1
2
3
4
5
6
7
<activity
android:exported="true"
android:name=".EmptyActivity">
<intent-filter>
<action android:name="com.test.ACTION_ACTIVITY"/>
</intent-filter>
</activity>

然后,在Kotlin中尝试用如下的代码来启动EmptyActivity:

1
2
3
startActivity(Intent("com.test.ACTION_ACTIVITY").apply {
data = Uri.parse("https://blog.lstec.org")
})

之后就会发现这个EmptyActivity并没有被启动。

原理探究

既然问题已经暴露,我们就要尽量查出问题产生的根本原因,而不是仅仅找个规避方案了事。经过一番代码跟踪之后,我发现这个问题在BroadcastReceiverActivity中的表现是一致的,连原因也是同一个,那么我下面就用BroadcastReceiver举例,说一下这个问题的根本原因。

1. BroadcastReceiver 的分发

众所周知,BroadcastReceiver的分发也是由framework中的AMS来处理的,就是它:frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java。经过一番调查之后,发现AMS收到一个BroadcastReceiver请求的时候,会通过它的成员变量mReceiverResolver来查找哪些BroadcastReceiver可以处理这个Intent

1
2
3
4
5
6
7
8
9
10
/**
* Resolver for broadcast intents to registered receivers.
* Holds BroadcastFilter (subclass of IntentFilter).
*/
final IntentResolver<BroadcastFilter, BroadcastFilter> mReceiverResolver
= new IntentResolver<BroadcastFilter, BroadcastFilter>() {
......
被忽略的其他代码
......
}

下面我们就要分析这个mReceiverResolver是如何工作的。

2. IntentResolver 查找对应的 BroadcastReceiver

AMS通过IntentResolverqueryIntent来查找 Intent 对应的那些BroadcastReceiver。这个方法的定义大致如下:

1
2
public List<R> queryIntent(Intent intent, String resolvedType, boolean defaultOnly,
int userId)

经过我的梳理,这个方法的工作流程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public List<R> queryIntent(Intent intent, String resolvedType, boolean defaultOnly,
int userId) {
// Uri中的scheme,如Uri是https://google.com,那么scheme就是https
String scheme = intent.getScheme();
ArrayList<R> finalList = new ArrayList<R>();

F[] firstTypeCut = null;

// If the intent includes a MIME type, then we want to collect all of
// the filters that match that MIME type.
if (resolvedType != null) {
// 由于 resolvedType 是空的,所以这里不执行
}

// If the intent includes a data URI, then we want to collect all of
// the filters that match its scheme (we will further refine matches
// on the authority and path by directly matching each resulting filter).
if (scheme != null) {
schemeCut = mSchemeToFilter.get(scheme);
}

// If the intent does not specify any data -- either a MIME type or
// a URI -- then we will only be looking for matches against empty
// data.
if (resolvedType == null && scheme == null && intent.getAction() != null) {
// 由于 scheme 不为空,所以这里不执行
firstTypeCut = mActionToFilter.get(intent.getAction());
}

FastImmutableArraySet<String> categories = getFastIntentCategories(intent);
if (firstTypeCut != null) {
// 由于 firstTypeCut 为null,所以这里不执行
buildResolveList(intent, categories, debug, defaultOnly, resolvedType,
scheme, firstTypeCut, finalList, userId);
}

return finalList;
}

由此可见,当Intent中既有Action,又有Uri的时候,Action就会被忽略。

总结

当我们使用的Intent去隐式启动BroadcastReceiver或者 Activity,如果Intent里既有Action,又有Uri。我们需要在组件的intent-filter显式声明我们能够捕获该Urischeme。类似https://m.lstec.org的 uri,需要在AndroidManifest.xml 中用如下的方式声明:

1
2
3
4
5
<intent-filter>
<action android:name="com.test.ACTION_RECEIVER" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="wmpfos" />
</intent-filter>