Android 下 FileProvider 的 authorities 重名會怎么樣?
先說結論:如果有兩個或多個 FileProvider 的 authorities 重名,那么只有合并后的 AndroidManifest.xml 文件里,排在最前面的那個配置會生效。
一、場景
應用里有個自升級的功能,下載完 apk 后,通過 FileProvider 提供 Uri 進行安裝。我修改了文件下載路徑后,功能失效了,報錯如下:
java.lang.IllegalArgumentException: Failed to find configured root that contains /data/user/0/org.mazhuang.test/cache/download/xxx.apk
at android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:738)
at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:417)
對應的 provider 的聲明是:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
provider_paths 內容:
<?xml version="1.0" encoding="utf-8"?>
<paths >
<cache-path name="internal_cache_download" path="download/" />
</paths>
二、分析
對照 FileProvider 官方文檔:https://developer.android.com/reference/android/support/v4/content/FileProvider.html ,我再三確認了配置本身沒有問題。
然后在報錯堆棧的 android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile 方法處下斷點調試:
@Override
public Uri getUriForFile(File file) {
// some code here
// Find the most-specific root path
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null
|| rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
}
}
if (mostSpecific == null) {
throw new IllegalArgumentException(
"Failed to find configured root that contains " + path);
}
// some code here
}
發現 SimplePathStrategy 的 mRoots 里確實沒有我配置的路徑。而 SimplePathStrategy 唯一的構造方法的參數是 authority,該實例的 authority 確實是 ${applicationId}.provider 無誤……那么,合理猜測,是有同名的 FileProvider,這里用到的是另一個 FileProvider 的 mRoots。
為了驗證該猜測,我從兩方面做確認:
- 查看合并后的 AndroidManifest.xml 文件,是否有其它 FileProvider 的 authorities 也是 ${applicationId}.provider?
- 閱讀 Android Frameworks 里的相關源碼,確認解析 provider 配置、取 FileProvider 實例的邏輯。
1.查看合并后的 AndroidManifest.xml
現在 Android Studio 已經提供了非常方便的查看合并后的 AndroidManifest.xml 的功能,打開 app 項目的 AndroidMenifest.xml 文件,在編輯器底部有個 Merged Manifest 選項卡,點擊即可查看。
可以看到,確實有兩個 FileProvider 的 authorities 都是 ${applicationId}.provider,另一個是從一個第三方庫里來的,并且,它排在前面。
2.源碼確認
首先是在 Android Studio 里進行,找到調用 SimplePathStrategy 構造方法的地方,是在 android.support.v4.content.FileProvider#parsePathStrategy:
/**
* Parse and return {@link PathStrategy} for given authority as defined in
* {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}.
*
* @see #getPathStrategy(Context, String)
*/
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
// some code here
}
這里的 context.getPackageManager().resolveContentProvider 的實現,一路通過以下路徑找到:
// android.app.ContextImpl#getPackageManager
// -->
// android.app.ActivityThread#getPackageManager
public static IPackageManager getPackageManager() {
if (sPackageManager != null) {
return sPackageManager;
}
IBinder b = ServiceManager.getService("package");
sPackageManager = IPackageManager.Stub.asInterface(b);
return sPackageManager;
}
到這里動用一點歷史經驗,可知實際實現類是 PackageManagerService,來看看 PackageManagerService#resolveContentProvider 的實現:
@Override
public ProviderInfo resolveContentProvider(String name, int flags, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, name);
final String instantAppPkgName = getInstantAppPackageName(Binder.getCallingUid());
// reader
synchronized (mPackages) {
final PackageParser.Provider provider = mProvidersByAuthority.get(name);
// some code here
}
// some code here
}
在 PackageManagerService 里繼續查找寫入 mProvidersByAuthority 的地方,在 PackageManagerService#commitPackageSettings:
/**
* Adds a scanned package to the system. When this method is finished, the package will
* be available for query, resolution, etc...
*/
private void commitPackageSettings(PackageParser.Package pkg, PackageSetting pkgSetting,
UserHandle user, int scanFlags, boolean chatty) throws PackageManagerException {
// some code here
synchronized (mPackages) {
// some code here
for (i=0; i<N; i++) {
PackageParser.Provider p = pkg.providers.get(i);
p.info.processName = fixProcessName(pkg.applicationInfo.processName,
p.info.processName);
mProviders.addProvider(p);
p.syncable = p.info.isSyncable;
if (p.info.authority != null) {
String names[] = p.info.authority.split(";");
p.info.authority = null;
for (int j = 0; j < names.length; j++) {
// some code here
// 【我們要找的地方】
if (!mProvidersByAuthority.containsKey(names[j])) {
mProvidersByAuthority.put(names[j], p);
if (p.info.authority == null) {
p.info.authority = names[j];
} else {
p.info.authority = p.info.authority + ";" + names[j];
}
// some code here
從上面這段中我們可以得到兩個知識點:
- 如果已經有同名的 authority,那么后面的 Provider 配置會被忽略掉;
- authority 可以配置多個,用分號分隔。(這一點在官方文檔之類的都沒有找到說明,也許官方覺得配置項的名稱 autorities 就說明了一切?實測可正常使用。)
接下來還有一點需要確認的,就是 pkg.providers 是否是按 AndroidManifexs.xml 里的順序排列的。
根據上面代碼里的線索,可以留意到 PackageParser 類,按如下順序遞進:
// android.content.pm.PackageParser#parseBaseApk(java.io.File, android.content.res.AssetManager, int)
private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
throws PackageParserException {
// some code here
// 下面這行里的 ANDROID_MANIFEST_FILENAME = AndroidManifest.xml
parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
final String[] outError = new String[1];
final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError);
// some code here
}
// -->
// android.content.pm.PackageParser#parseBaseApk(java.lang.String, android.content.res.Resources, android.content.res.XmlResourceParser, int, java.lang.String[])
// -->
// android.content.pm.PackageParser#parseBaseApkCommon
// -->
// android.content.pm.PackageParser#parseBaseApplication
// -->
private boolean parseBaseApplication(Package owner, Resources res,
XmlResourceParser parser, int flags, String[] outError)
// some code here
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {
if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if (tagName.equals("activity")) {
// some code here
} else if (tagName.equals("provider")) {
Provider p = parseProvider(owner, res, parser, flags, outError);
if (p == null) {
mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
return false;
}
owner.providers.add(p);
// some code here
至此,我們已經可以確定 pkg.providers 是按 AndroidManifest.xml 里的順序解析出來的了。
三、解決方案
既然已經知道了問題的原因,那么解決方案也就呼之欲出了:
修改自己的 FileProvider 的 authorities,不會和其它庫的 authorities 重名即可。
四、小結
源碼面前,了無秘密。——侯捷
如果遇到疑難問題,而恰好又有源碼可查,那么就不要猶豫,直接去看源碼吧!花一些時間和耐心,最終會找到你想要的。