admin管理员组

文章数量:1122855

Intent

一、问题复现

Intent隐式启动Activity,我们在manifest文件中定义了如下intent-filter:

// 出现问题的写法
<intent-filter><action android:name="android.intent.action.VIEW"/><category android:name="android.intent.category.DEFAULT"/><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/><dataandroid:scheme="xu"android:host="www.landon"/>
</intent-filter>

这样,我们可以直接用xu://www.landon启动这个activity,但问题出现了,启动的时候,会报错:

Error: Activity not started, unable to resolve Intent { act=android.intent.action.VIEW dat=xu://www.landon flg=0x10000000 }

但在manifest文件中,我们已经定义了scheme和host,只不过有一个是多path,另外一个没有path,我们都知道data是可以存在多个的,这样看不应该找不到activity。

难道data是不允许多个同时在intent-filter中吗?抱着试试看的心态,我把两个data拆分出来:

// 正常work的写法<intent-filter><action android:name="android.intent.action.VIEW"/><category android:name="android.intent.category.DEFAULT"/><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/></intent-filter><intent-filter><action android:name="android.intent.action.VIEW"/><category android:name="android.intent.category.DEFAULT"/><dataandroid:scheme="xu"android:host="www.landon"/></intent-filter>

拆分出来,还真是就可以识别了:

Starting: Intent { act=android.intent.action.VIEW dat=xu://www.landon }

于是我又去翻了一下官方网站:您可以在 内放置任意数量的 元素,为其提供多个数据选项。它的属性都没有默认值。()的确是可以放多个data元素的,那为啥第一种就是不行呢

二、源码追溯

为了探究这个问题,不得不查看Android源码。
从错误提示unable to resolve Intent可以找到入手点:

// Intent.javapublic ComponentName resolveActivity(@NonNull PackageManager pm) {if (mComponent != null) {return mComponent;}ResolveInfo info = pm.resolveActivity(this, PackageManager.MATCH_DEFAULT_ONLY);if (info != null) {return new ComponentName(info.activityInfo.applicationInfo.packageName,info.activityInfo.name);}return null;}

从Intent.resolveActivity可以看到,最终调用的是PackageManager的resolveActivity,而PackageManager是通过ContextImpl的getPackageManager获取到的:

// ContextImpl@Overridepublic PackageManager getPackageManager() {if (mPackageManager != null) {return mPackageManager;}IPackageManager pm = ActivityThread.getPackageManager();if (pm != null) {// Doesn't matter if we make more than one instance.return (mPackageManager = new ApplicationPackageManager(this, pm));}return null;}

ActivityThread.getPackageManager()就是获取了PackageManagerService,在ContextImpl中用ApplicationPackageManager代理PMS:

ApplicationPackageManager.java@Overridepublic ResolveInfo resolveActivity(Intent intent, int flags) {return resolveActivityAsUser(intent, flags, getUserId());}@Overridepublic ResolveInfo resolveActivityAsUser(Intent intent, int flags, int userId) {try {return mPM.resolveIntent(intent,intent.resolveTypeIfNeeded(mContext.getContentResolver()),updateFlagsForComponent(flags, userId, intent),userId);} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}

调用了PMS的resolveIntent方法:

// PackageManagerService.java@Overridepublic ResolveInfo resolveIntent(Intent intent, String resolvedType,int flags, int userId) {return resolveIntentInternal(intent, resolvedType, flags, userId, false,Binder.getCallingUid());}/*** Normally instant apps can only be resolved when they're visible to the caller.* However, if {@code resolveForStart} is {@code true}, all instant apps are visible* since we need to allow the system to start any installed application.*/private ResolveInfo resolveIntentInternal(Intent intent, String resolvedType,int flags, int userId, boolean resolveForStart, int filterCallingUid) {.....final List<ResolveInfo> query = queryIntentActivitiesInternal(intent, resolvedType,flags, filterCallingUid, userId, resolveForStart, true /*allowDynamicSplits*/);.......}private @NonNull List<ResolveInfo> queryIntentActivitiesInternal(Intent intent,String resolvedType, int flags, int filterCallingUid, int userId,boolean resolveForStart, boolean allowDynamicSplits) {....List<CrossProfileIntentFilter> matchingFilters =getMatchingCrossProfileIntentFilters(intent, resolvedType, userId);.....}private List<CrossProfileIntentFilter> getMatchingCrossProfileIntentFilters(Intent intent,String resolvedType, int userId) {CrossProfileIntentResolver resolver = mSettings.mCrossProfileIntentResolvers.get(userId);if (resolver != null) {return resolver.queryIntent(intent, resolvedType, false /*defaultOnly*/, userId);}return null;}

然后调用到IntentResolver:

// IntentResolver<F extends IntentFilter, R extends Object> public List<R> queryIntent(Intent intent, String resolvedType, boolean defaultOnly,int userId) {........if (firstTypeCut != null) {buildResolveList(intent, categories, debug, defaultOnly, resolvedType,scheme, firstTypeCut, finalList, userId);}if (secondTypeCut != null) {buildResolveList(intent, categories, debug, defaultOnly, resolvedType,scheme, secondTypeCut, finalList, userId);}if (thirdTypeCut != null) {buildResolveList(intent, categories, debug, defaultOnly, resolvedType,scheme, thirdTypeCut, finalList, userId);}if (schemeCut != null) {buildResolveList(intent, categories, debug, defaultOnly, resolvedType,scheme, schemeCut, finalList, userId);}......}private void buildResolveList(Intent intent, FastImmutableArraySet<String> categories,boolean debug, boolean defaultOnly, String resolvedType, String scheme,F[] src, List<R> dest, int userId) {.....match = filter.match(action, resolvedType, scheme, data, categories, TAG);......}

最终调用到IntentFilter的match和matchData方法:

public final int match(String action, String type, String scheme,Uri data, Set<String> categories, String logTag) {......int dataMatch = matchData(type, scheme, data);.....}/**
* 核心方法
*
*/
public final int matchData(String type, String scheme, Uri data) {final ArrayList<String> types = mDataTypes;final ArrayList<String> schemes = mDataSchemes;int match = MATCH_CATEGORY_EMPTY;if (types == null && schemes == null) {return ((type == null && data == null)? (MATCH_CATEGORY_EMPTY+MATCH_ADJUSTMENT_NORMAL) : NO_MATCH_DATA);}if (schemes != null) {if (schemes.contains(scheme != null ? scheme : "")) {match = MATCH_CATEGORY_SCHEME;} else {return NO_MATCH_DATA;}// mDataSchemeSpecificParts这个是ssp字段,目前这里没有用到,此处为nullfinal ArrayList<PatternMatcher> schemeSpecificParts = mDataSchemeSpecificParts;// 这里if不走,因为schemeSpecificParts为空if (schemeSpecificParts != null && data != null) {match = hasDataSchemeSpecificPart(data.getSchemeSpecificPart())? MATCH_CATEGORY_SCHEME_SPECIFIC_PART : NO_MATCH_DATA;}// 进入到此if中if (match != MATCH_CATEGORY_SCHEME_SPECIFIC_PART) {// If there isn't any matching ssp, we need to match an authority.final ArrayList<AuthorityEntry> authorities = mDataAuthorities;if (authorities != null) {int authMatch = matchDataAuthority(data);if (authMatch >= 0) {final ArrayList<PatternMatcher> paths = mDataPaths;// 如果定义的manifest文件中的intent-filter中没有定义path,就直接匹配成功if (paths == null) {match = authMatch;} else if (hasDataPath(data.getPath())) {如果定义在manifest文件中的intent-filter中定义了path,就需要再到hasDataPath中对比match = MATCH_CATEGORY_PATH;} else {return NO_MATCH_DATA;}} else {return NO_MATCH_DATA;}}}// If neither an ssp nor an authority matched, we're done.if (match == NO_MATCH_DATA) {return NO_MATCH_DATA;}} else {// Special case: match either an Intent with no data URI,// or with a scheme: URI.  This is to give a convenience for// the common case where you want to deal with data in a// content provider, which is done by type, and we don't want// to force everyone to say they handle content: or file: URIs.if (scheme != null && !"".equals(scheme)&& !"content".equals(scheme)&& !"file".equals(scheme)) {return NO_MATCH_DATA;}}if (types != null) {if (findMimeType(type)) {match = MATCH_CATEGORY_TYPE;} else {return NO_MATCH_TYPE;}} else {// If no MIME types are specified, then we will only match against// an Intent that does not have a MIME type.if (type != null) {return NO_MATCH_TYPE;}}return match + MATCH_ADJUSTMENT_NORMAL;}/*** Is the given data path included in the filter?  Note that if the* filter does not include any paths, false will <em>always</em> be* returned.** @param data The data path to look for.  This is without the scheme*             prefix.** @return True if the data string matches a path listed in the*         filter.*/public final boolean hasDataPath(String data) {if (mDataPaths == null) {return false;}// 当manifest文件中intent-filter定义了path,需要和目标对比final int numDataPaths = mDataPaths.size();for (int i = 0; i < numDataPaths; i++) {final PatternMatcher pe = mDataPaths.get(i);if (pe.match(data)) {return true;}}return false;}

三、原因分析

根据上述源码分析,可以找到原因:

  1. manifest文件中的intent-filter中如果没有定义path,那么当scheme和host都匹配成功,就不需要再去看path字段了:比如在intent-filter中定义了:
<!--这样定义想相当于要匹配所有xu://www.landon,而不管后续path是啥,都匹配上-->
<intent-filter><dataandroid:scheme="xu"android:host="www.landon"/>
</intent-filter>
  1. manifest文件中的intent-filter中如果定义了path,那么只会按照path来匹配,因为intent-filter中只要存在path,就会走到hasDataPath
<!--这样定义想相当于要匹配xu://www.landon/path,必须要有path,xu://www.landon这种就无法匹配>
<intent-filter><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/>
</intent-filter>
  1. manifest文件中的一个intent-filter可以存在多个data,如果这些data中存在有path,也存在没有path的,依旧会按照path来匹配,因为IntentFilter在解析xml文件中时,如果一旦发现path,mDataPaths就不为空,就会走到path匹配里
<intent-filter><dataandroid:scheme="xu"android:host="www.landon"/><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/>
</intent-filter>

所以,当按照两个data一个有path一个没有path的都在同一个intent-filter的时候,没有path的是无法匹配的,例如上面的xu://www.landon就没有办法匹配上。
如果将其中没有path的添加一个path=“”,这样就能匹配成功了

<intent-filter><dataandroid:scheme="xu"android:path=""android:host="www.landon"/><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/>
</intent-filter>

四、问题解决

既然在同一个intent-filter可以定义多个data,但要注意有path和没有path的不要混合在一起用

  1. 有path的定义在同一个intent-filter中
  2. 没有path只有scheme和host的,需要单独定义在一个intent-filter中,不与其他有path的混合在一起
<intent-filter><dataandroid:scheme="xu"android:host="www.landon"/>
</intent-filter>
<intent-filter><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/>
</intent-filter>

五、反思

对于intent-filter的写法

<intent-filter><dataandroid:scheme="xu"android:host="www.landon"/><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/>
</intent-filter>

这种写法本身就存在歧义:
是只匹配xu://www.landon和xu://www.landon/test吗,添加其他的path不匹配?
还是本意想要匹配所有xu://www.landon,不论path是啥?
如果是前者,那么可以使用:

<intent-filter><dataandroid:scheme="xu"android:path=""android:host="www.landon"/><dataandroid:scheme="xu"android:host="www.landon"android:path="/test"/>
</intent-filter>

如果是后者,那么直接使用一个就可以:

<intent-filter><dataandroid:scheme="xu"android:host="www.landon"/>
</intent-filter>

所以,问题的根本是在于对于intent-filter要过滤的协议标准,标准确认了,写法上也不会这样存在模糊有问题。

本文标签: Intent