Android FileProvider使用

使用FileProvider出現(xiàn)如下錯誤

java.lang.IllegalArgumentException: Failed to find configured root that contains

原文鏈接:https://blog.csdn.net/u013553529/article/details/83900704#how

FileProvider 路徑配置策略的理解

★ FileProvider的使用

在AndroidManifest.xml中

<provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="set_your_package_name"
        android:exported="false"
        android:grantUriPermissions="true">
    <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepath_data" />
</provider>

通常設置android:exported="false"炕柔,以保證權限最小化彪见。
android:resource="@xml/filepath_data"中,filepath_data.xml文件是配置哪些路徑是可以通過FileProvider訪問的檩电。
meta-data是以鍵值對的方式保存(key-value pairs)蔑水。android.support.FILE_PROVIDER_PATHS作為meta-data的鍵(key)力细,@xml/filepath_data作為meta-data的值(value)。在FileProvider中會讀取meta-data中的android.support.FILE_PROVIDER_PATHS對應的值切平。

filepath_data.xml

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="my_files" path="tempfiles" />
    <external-path name="my_external" path="Download"/>
    <cache-path name="my_cache" />
</paths>

files-path對應app的/data/data/<package_name>/files/目錄埋凯,path="tempfiles"是指子目錄点楼,即完整的目錄為/data/data/<package_name>/files/tempfiles。

external-path對應的是內(nèi)置的sdcard目錄/sdcard/递鹉,path="Download"是子目錄盟步,完整目錄為 /sdcard/Download。

cache-path對應的是/data/data/<package_name>/cache/躏结,這個例子里沒有子目錄却盘。

name屬性相當于這些路徑的別名,通過name可以獲取到相對應的路徑媳拴。

★ 如何更好地理解這幾個路徑的用法黄橘?

通過學習Android中解析filepath_data.xml文件的源代碼,可以更容易理解和掌握這些路徑的具體含義屈溉。
代碼請參考FileProvider的parsePathStrategy()方法塞关。如果想了解如何執(zhí)行到此方法的,可以參考Android ContentProvider的加載過程

parsePathStrategy()方法的代碼如下(省略了一些代碼):

XML文件中的TAG和屬性:

private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";

private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";

XML中各個tag對應的路徑子巾,如下表:

  • Tag 對應的路徑
  • root-path 根目錄/
  • files-path /data/user/0/<package_name>/files 或者/data/data/<package_name>/files
  • 這兩個目錄指向相同的位置
  • cache-path /data/user/0/<package_name>/cache 或者 /data/data/<package_name>/cache
  • external-path /storage/emulated/0或者/sdcard/
  • external-files-path /storage/emulated/0/Android/data/<package_name>/files 或者 /sdcard/Android/data/<package_name>/files
  • external-cache-path /storage/emulated/0/Android/data/<package_name>/cache 或者 /sdcard/Android/data/<package_name>/cache
parsePathStrategy() @FileProvider

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);
    // META_DATA_FILE_PROVIDER_PATHS 為"android.support.FILE_PROVIDER_PATHS"帆赢, 這是在AndroidManifest.xml中所使用的小压。
    // 讀取filepath_data.xml文件(本文中的例子)
    final XmlResourceParser in = info.loadXmlMetaData(
            context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);

    int type;
    while ((type = in.next()) != END_DOCUMENT) {
        if (type == START_TAG) {
            final String tag = in.getName();

            // 獲取屬性"name"和"path"
            final String name = in.getAttributeValue(null, ATTR_NAME);
            String path = in.getAttributeValue(null, ATTR_PATH);

            File target = null;
            if (TAG_ROOT_PATH.equals(tag)) {
                // "root-path"標簽,DEVICE_ROOT = new File("/")椰于,系統(tǒng)的根目錄
                target = DEVICE_ROOT;
            } else if (TAG_FILES_PATH.equals(tag)) {
                // "files-path"標簽怠益,getFilesDir(),對應"/data/user/0/<package_name>/files"目錄
                target = context.getFilesDir();
            } else if (TAG_CACHE_PATH.equals(tag)) {
                // "cache-path"標簽瘾婿,對應"/data/user/0/<package_name>/cache"目錄
                target = context.getCacheDir();
            } else if (TAG_EXTERNAL.equals(tag)) {
                // "external-path"標簽蜻牢,對應內(nèi)置sdcard目錄,例如"/storage/emulated/0"偏陪, 或者"/sdcard/"
                target = Environment.getExternalStorageDirectory();
            } else if (TAG_EXTERNAL_FILES.equals(tag)) {
                // "external-files-path"標簽抢呆,對應 "/storage/emulated/0/Android/data/<package_name>/files"目錄
                File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
                if (externalFilesDirs.length > 0) {
                    target = externalFilesDirs[0];
                }
            } else if (TAG_EXTERNAL_CACHE.equals(tag)) {
                // "external-cache-path"標簽,對應"/storage/emulated/0/Android/data/<package_name>/cache"目錄
                File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
                if (externalCacheDirs.length > 0) {
                    target = externalCacheDirs[0];
                }
            }

            if (target != null) {
                // 將路徑拼起來笛谦,name作為key抱虐,完整路徑是value
                strat.addRoot(name, buildPath(target, path));
            }
        }
    }
    return strat;
}

注意:/data/user/0是指向/data/data目錄,所以/data/user/0/<package_name>/files也就是/data/data/<package_name>/files揪罕。
執(zhí)行下面的命令可以看到:

ls -ld /data/user/0
lrwxrwxrwx 1 root root 10 2017-04-15 00:25 /data/user/0 -> /data/data

以filepath_data.xml這個文件為例梯码,再看一下都配置了哪些路徑:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="my_files" path="tempfiles" />
    <external-path name="my_external" path="Download"/>
    <cache-path name="my_cache" />
</paths>

files-path對應app的/data/data/<package_name>/files/目錄讶踪,path="tempfiles"是指子目錄亲桥,拼起來的完整路徑為/data/data/<package_name>/files/tempfiles涡驮。

external-path對應的是內(nèi)置的sdcard目錄/sdcard/,path="Download"是子目錄框往,完整目錄為 /sdcard/Download。

cache-path對應的是/data/data/<package_name>/cache/闯捎,這個例子里沒有子目錄椰弊。

★ 如何使用filepath_data.xml中配置的路徑?

◇ 通過uri來訪問文件

以<files-path name="my_files" path="tempfiles" />為例瓤鼻。

通過Uri content://<authority>/my_files/<file_name>來訪問my_files標簽對應的目錄中的文件<file_name>秉版。

例如,content://my_authority/my_files/path/to/file001.txt對應的就是/data/data/<package_name>/files/path/to/file001.txt茬祷。

代碼可以參考FileProvider的getFileForUri()清焕,下面是部分主要代碼。

public File getFileForUri(Uri uri) {
    String path = uri.getEncodedPath();

    final int splitIndex = path.indexOf('/', 1);
    // 解析出tag祭犯,在此例中tag是my_files
    final String tag = Uri.decode(path.substring(1, splitIndex));
    // path是uri中的<file_name>秸妥,path可以只是文件名,也可以是帶路徑的文件名
    path = Uri.decode(path.substring(splitIndex + 1));
    // 這個tag就是`<files-path name="my_files" path="tempfiles" />`中的屬性name
    final File root = mRoots.get(tag);
    // 將路徑拼起來沃粗,構成實際的文件路徑
    File file = new File(root, path);
    // 略
    return file;
}

◇ 獲取文件對應的Uri

參考FileProvider中的getUriForFile()
注:所有出錯處理的代碼都忽略了粥惧。

public Uri getUriForFile(File file) {
    String path;
    try {
        path = file.getCanonicalPath();
    } catch...

    // 這段代碼是為了找到文件file最匹配的路徑,即取匹配最長的那個root
    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;
        }
    }

    final String rootPath = mostSpecific.getValue().getPath();
    // path是以/開頭的
    if (rootPath.endsWith("/")) {
        // 如果rootPath以/開頭最盅,則將rootPath長度的內(nèi)容去掉后突雪,剩下的就是uri中使用的路徑
        path = path.substring(rootPath.length());
    } else {
        // 如果rootPath不是以/開頭起惕,則需要去掉path的第一個/后,再去掉rootPath.length()的內(nèi)容后咏删,剩下的就是uri中使用的路徑
        path = path.substring(rootPath.length() + 1);
    }

    // mostSpecific.getKey()對應的是路徑配置文件中的屬性name
    // 最終拼起來像這樣:content://<authority>/<name>/<file_path>
    path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
    return new Uri.Builder().scheme("content")
            .authority(mAuthority).encodedPath(path).build();
}

以<external-path name="my_external" path="Download"/>為例疤祭。

對于內(nèi)置sdcard中Download目錄下的文件file002.txt,其路徑為/sdcard/Download/file002.txt饵婆。對應的uri為content://<authority>/my_external/file002.txt勺馆。

★ Android ContentProvider的加載過程

當某個app的進程要啟動時,Dalvik虛擬機先fork出一個新的進程侨核,然后將此進程的名字命名為這個app的包名草穆,然后通過反射的方式,執(zhí)行 ActivityThread 的靜態(tài)的main()方法搓译,在main()中創(chuàng)建主線程 ActivityThread悲柱,并將app中的各種組件信息附加到該進程中,即調(diào)用attach()方法些己。

從這個attach()方法開始豌鸡,來描述ContentProvider的加載過程。

說明:@ActivityThread表示代碼在ActivityThread類中段标。

-> main() @ActivityThread
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
-> attach() @ActivityThread
-> mgr.attachApplication(mAppThread); @ActivityThread
    mgr是IActivityManager類型的接口涯冠,是 ActivityManagerProxy 的實例,最終調(diào)用 ActivityManagerService的對應的方法逼庞。
    這里會切換進程到system_server進程中(ActivityManagerService所在的進程)

-> attachApplication() @ActivityManagerService
-> attachApplicationLocked() @ActivityManagerService
    List<ProviderInfo> providers = normalMode ? generateApplicationProvidersLocked(app) : null;
    thread.bindApplication(processName, appInfo, providers, ...);
    thread 是 IApplicationThread 類型的接口蛇更,用來向app所在進程發(fā)送消息,即調(diào)用app進程中的方法赛糟。切換進程到app進程派任。

-> bindApplication() @ActivityThread
    AppBindData data = new AppBindData();
    data.providers = providers;
    sendMessage(H.BIND_APPLICATION, data);
-> handleMessage() @ActivityThread
    case BIND_APPLICATION:
        handleBindApplication(data);
-> handleBindApplication() @ActivityThread
        if (!data.restrictedBackupMode) {
            if (!ArrayUtils.isEmpty(data.providers)) {
                installContentProviders(app, data.providers);
            }
        }
-> installContentProviders() @ActivityThread
-> installProvider() @ActivityThread
        final java.lang.ClassLoader cl = c.getClassLoader();
        localProvider = (ContentProvider)cl.loadClass(info.name).newInstance();
        localProvider.attachInfo(c, info);
    c 是context,info是ProviderInfo對象璧南。info.name是provider的名字掌逛。
    由于FileProvider中重寫了attachInfo(),所以司倚,這里的localProvider.attachInfo()將執(zhí)行FileProvider的attachInfo()豆混。
-> attachInfo() @FileProvider
        super.attachInfo(context, info); // 調(diào)用父類ContentProvider的attachInfo(),設置 ContentProvider 的各種屬性对湃,并調(diào)用Provider 的onCreate()
        mStrategy = getPathStrategy(context, info.authority);
        getPathStrategy()解析filepath_data.xml文件(在本文中的例子)崖叫。

-> getPathStrategy() @FileProvider
-> parsePathStrategy() @FileProvider
    parsePathStrategy()用來解析filepath_data.xml文件,這對理解filepath_data.xml文件很有幫助拍柒。
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末心傀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拆讯,更是在濱河造成了極大的恐慌脂男,老刑警劉巖养叛,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異宰翅,居然都是意外死亡弃甥,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門汁讼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淆攻,“玉大人,你說我怎么就攤上這事嘿架∑可海” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵耸彪,是天一觀的道長伞芹。 經(jīng)常有香客問我,道長蝉娜,這世上最難降的妖魔是什么唱较? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮召川,結果婚禮上南缓,老公的妹妹穿的比我還像新娘。我一直安慰自己扮宠,他們只是感情好西乖,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著坛增,像睡著了一般。 火紅的嫁衣襯著肌膚如雪薄腻。 梳的紋絲不亂的頭發(fā)上收捣,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音庵楷,去河邊找鬼罢艾。 笑死,一個胖子當著我的面吹牛尽纽,可吹牛的內(nèi)容都是我干的咐蚯。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弄贿,長吁一口氣:“原來是場噩夢啊……” “哼春锋!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起差凹,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤期奔,失蹤者是張志新(化名)和其女友劉穎侧馅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體呐萌,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡馁痴,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了肺孤。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罗晕。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赠堵,靈堂內(nèi)的尸體忽然破棺而出小渊,到底是詐尸還是另有隱情,我是刑警寧澤顾腊,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布粤铭,位于F島的核電站,受9級特大地震影響杂靶,放射性物質(zhì)發(fā)生泄漏梆惯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一吗垮、第九天 我趴在偏房一處隱蔽的房頂上張望垛吗。 院中可真熱鬧,春花似錦烁登、人聲如沸怯屉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锨络。三九已至,卻和暖如春狼牺,著一層夾襖步出監(jiān)牢的瞬間羡儿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工是钥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留掠归,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓悄泥,卻偏偏與公主長得像虏冻,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子弹囚,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容