Android FileProvider 踩坑指北

前言

從 Android N(7.0) 開(kāi)始,將嚴(yán)格執(zhí)行 StrictMode 模式煤禽。而從 Android N 開(kāi)始漆撞,將不允許在 App 間限次,使用 file:// 的方式,傳遞一個(gè) File 摩梧,否者會(huì)拋出 FileUriExposedException 的異常引發(fā) Crash物延。解決方案就是通過(guò)FileProvider 用 content:// 代替 file://,需要開(kāi)發(fā)者主動(dòng)升級(jí) targetSdkVersion 到 24 才會(huì)執(zhí)行此策略仅父。

其實(shí)在 Android 7.0 出來(lái)后我們應(yīng)用就 就開(kāi)始適配了教届,應(yīng)用中加入了FileProvider,但偶爾還是會(huì)碰到 FileProvider 導(dǎo)致的問(wèn)題驾霜,所以這邊文章準(zhǔn)備徹徹底底的分析下FileProvider案训,揪出FileProvider的廬山真面目。

Android 應(yīng)用文件存儲(chǔ)目錄

內(nèi)部存儲(chǔ)空間中的應(yīng)用私有目錄

每安裝一個(gè) App 系統(tǒng)都會(huì)在內(nèi)部存儲(chǔ)空間的 data/data 目錄下以應(yīng)用包名為名字自動(dòng)創(chuàng)建與之對(duì)應(yīng)的文件夾粪糙,這個(gè)文件夾用于持久化 App 中的 WebView 緩存頁(yè)面信息强霎、SharedPreferences、SQLiteDatabase等應(yīng)用相關(guān)數(shù)據(jù)蓉冈。當(dāng)用戶卸載 App 時(shí)城舞,系統(tǒng)自動(dòng)刪除 data/data 目錄下對(duì)應(yīng)包名的文件夾及其內(nèi)容。

Android SDK 提供 獲取并操作內(nèi)部存儲(chǔ)空間中的應(yīng)用私有目錄的方法如下:

  • context.getFilesDir()
  • context.getCacheDir()
  • context.deleteFile()
  • context.fileList()
  • Environment.getDataDirectory()

外部存儲(chǔ)空間中的應(yīng)用私有目錄

考慮到普通用戶無(wú)法訪問(wèn)應(yīng)用的內(nèi)部存儲(chǔ)空間寞酿,比如用戶想從應(yīng)用里面保存一張圖片家夺,那么這張圖片應(yīng)該存儲(chǔ)在外部存儲(chǔ)空間,用戶才能訪問(wèn)的到伐弹,外部存儲(chǔ)空間路徑為:/storage/emulated/0/Android/data/<包名>

默認(rèn)情況下拉馋,系統(tǒng)并不會(huì)自動(dòng)創(chuàng)建外部存儲(chǔ)空間的應(yīng)用私有目錄。只有在應(yīng)用需要的時(shí)候惨好,開(kāi)發(fā)人員通過(guò) SDK 提供的 API 創(chuàng)建該目錄文件夾和操作文件夾內(nèi)容煌茴。

當(dāng)用戶卸載 App 時(shí),系統(tǒng)也會(huì)自動(dòng)刪除外部存儲(chǔ)空間下的對(duì)應(yīng) App 私有目錄文件夾及其內(nèi)容日川。

Android SDK 中提供給開(kāi)發(fā)人員直接操作外部存儲(chǔ)空間下的應(yīng)用私有目錄的方法如下:

  • context.getExternalFilesDir()
  • context.getExternalCacheDir()
  • Environment.getExternalStorageDirectory()

外部存儲(chǔ)空間中的公共目錄

外部存儲(chǔ)空間中的公共目錄用來(lái)存放當(dāng)應(yīng)用被卸載時(shí)蔓腐,仍然可以保存在設(shè)備中的信息,如:拍照類應(yīng)用的圖片文件龄句,用戶是使用瀏覽器手動(dòng)下載的文件等回论。

外部存儲(chǔ)空間已經(jīng)為用戶默認(rèn)分類出一些公共目錄散罕。開(kāi)發(fā)人員可以通過(guò) Environment 類提供的方法直接獲取相應(yīng)目錄的絕對(duì)路徑,傳遞不同的 type 參數(shù)類型即可:

  • Environment.getExternalStoragePublicDirectory(String type);

Envinonment 類提供諸多 type 參數(shù)的常量傀蓉,比如:

  • DIRECTORY_MUSIC:/storage/emulated/0/Music
  • DIRECTORY_MOVIES:/storage/emulated/0/Movies
  • DIRECTORY_PICTURES:/storage/emulated/0/Pictures
  • DIRECTORY_DOWNLOADS:/storage/emulated/0/Download

FileProvider

什么是 FileProvider

FileProvider 是 ContentProvider的子類 目前 support v4 包 和 androidx的core包里面都有提供笨使。FileProvider 本質(zhì)上就是一個(gè) ContentProvider ,它其實(shí)也繼承了 ContentProvider 的特性僚害。其實(shí)ContentProvider 就是在可控的范圍內(nèi)硫椰,向外部其他的 App 分享數(shù)據(jù)。而 FileProvider 將這樣的數(shù)據(jù)變成了一個(gè) File 文件而已萨蚕。

使用 FileProvider 的場(chǎng)景

在 App 內(nèi)靶草,通過(guò)一個(gè) Intent 傳遞了一個(gè) file:// 的 Uri 的場(chǎng)景都需要使用 FileProvider ,如:

  • 調(diào)用相機(jī)拍照
  • 剪裁圖片
  • 調(diào)用系統(tǒng)安裝器去安裝 Apk

如何使用 FileProvider

1 在AndroidManifest.xml 中聲明

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileProvider"
            android:exported="false"
            android:grantUriPermissions="true">
            
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

可以看到岳遥,provider 標(biāo)簽下奕翔,配置了幾個(gè)屬性:

  • name :配置當(dāng)前 FileProvider 的實(shí)現(xiàn)類。
  • authorities:配置一個(gè) FileProvider 的名字浩蓉,它在當(dāng)前系統(tǒng)內(nèi)需要是唯一值派继。
  • exported:表示該 FileProvider 是否需要公開(kāi)出去,傳 false 表示不公開(kāi)捻艳。
  • granUriPermissions:是否允許授權(quán)文件的臨時(shí)訪問(wèn)權(quán)限驾窟。傳 true 表示需要 。

name 屬性就是標(biāo)記當(dāng)前 FileProvider 的實(shí)現(xiàn)類认轨,對(duì)于一個(gè) App Module 而言绅络,如果只是自己使用,可以直接使用 FileProvider 嘁字,但是如果是作為一個(gè) Lib Module 來(lái)供其他項(xiàng)目使用恩急,最好還是重新創(chuàng)建一個(gè)Provider繼承 FileProvider。

2 指定可分享的文件路徑

在配置 Provider 的時(shí)候纪蜒,還需要額外配置一個(gè) <meta-data/> 標(biāo)簽衷恭,它用于配置 FileProvider 支持分享出去的目錄。這個(gè) <meta-data/> 標(biāo)簽的 name 值是固定的纯续,resource 需要指向一個(gè) xml 資源文件随珠。


file_paths.xml 中內(nèi)容如下:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path
        name="files-path"
        path="." />
    <cache-path
        name="cache-path"
        path="." />
    <external-path
        name="external_storage_root"
        path="." />
    <external-files-path
        name="external_file_path"
        path="." />
    <external-cache-path
        name="external_cache_path"
        path="." />
    <!--配置root-path。這樣子可以讀取到sd卡和一些應(yīng)用分身的目錄杆烁,否則微信分身保存的圖片牙丽,就會(huì)導(dǎo)致 java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/999/tencent/MicroMsg/WeiXin/export1544062754693.jpg-->
    <root-path
        name="root-path"
        path="" />
</paths>

通過(guò)上面的內(nèi)容可以看到,在paths根目錄下定義了各種 xx-path 標(biāo)簽兔魂,這些標(biāo)簽,我們可以通過(guò)查看 FileProvider 的源碼查到

這些 xx-path 標(biāo)簽所代表的目錄可以通過(guò)源碼了解

標(biāo)簽對(duì)應(yīng)的目錄匯總?cè)缦拢?/p>

  • root-path:表示根目錄举娩,“/”析校。
  • files-path:表示 content.getFileDir() 獲取到的目錄
  • cache-path:表示 content.getCacheDir() 獲取到的目錄
  • external-path:表示Environment.getExternalStorageDirectory() 指向的目錄
  • external-files-path:表示 ContextCompat.getExternalFilesDirs() 獲取到的目錄
  • external-cache-path:表示 ContextCompat.getExternalCacheDirs() 獲取到的目錄
TAG Value Path
TAG_ROOT_PATH root-path /
TAG_FILES_PATH files-path /data/data/<包名>/files
TAG_CACHE_PATH cache-path /data/data/<包名>/cache
TAG_EXTERNAL external-path /storage/emulate/0
TAG_EXTERNAL_FILES external-files-path /storage/emulate/0/Android/data/<包名>/files
TAG_EXTERNAL_CACHE external-cache-path /storage/emulate/0/Android/data/<包名>/cache

注意:
如果App有選擇和剪裁圖片的需求构罗,最好配置下root-path,這樣子可以讀取到sd卡和一些應(yīng)用分身的目錄智玻,否則微信等應(yīng)用分身保存的圖片遂唧,在App里面讀取時(shí)就發(fā)生下面異常:

java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/10/tencent/MicroMsg/WeiXin/mmexport1592487275473.jpg

3 將 file:// 轉(zhuǎn)為 content://

使用FileProvider.getUriForFile()方法將file:// 轉(zhuǎn)為 content://

getUriForFile() 方法,需要一個(gè) authority 的參數(shù)吊奢,這里需要與前面在 AndroidManifest.xml 中 配置的 android:authorities保持一致盖彭,因?yàn)槭峭ㄟ^(guò) android:authorities 屬性配置的值,來(lái)唯一確定由誰(shuí)來(lái)響應(yīng)這個(gè) provider 的 页滚。在 AndroidManifest.xml 中配置 provider 的時(shí)候召边,需要保證 android:authorities 的值,在整個(gè)系統(tǒng)中的唯一性裹驰,否者安裝的時(shí)候會(huì)拋出如下異常:


4 授予臨時(shí)的讀寫(xiě)權(quán)限

在配置 provider 標(biāo)簽的時(shí)候隧熙,有一個(gè)屬性 android:grantUriPermissions="true" ,它表示允許它授予 Uri 臨時(shí)的權(quán)限幻林。

當(dāng)我們生成出一個(gè) content:// 的 Uri 對(duì)象之后贞盯,其實(shí)也無(wú)法對(duì)其直接使用,還需要對(duì)這個(gè) Uri 接收的 App 賦予對(duì)應(yīng)的權(quán)限才可以沪饺。

授權(quán)類型的常量躏敢,被定義在 Intent 類中。


授權(quán)的兩種方式:

  1. 使用 Context.grantUriPermission() 為其他 App 授予 Uri 對(duì)象的訪問(wèn)權(quán)限整葡。
public abstract void grantUriPermission(String toPackage, Uri uri,@Intent.GrantUriMode int modeFlags);

grantUriPermission() 方法包含三個(gè)參數(shù):

  • toPackage :表示授予權(quán)限的 App 的包名父丰。
  • uri:授予權(quán)限的 content:// 的 Uri
  • modeFlags:前面提到的定義在 Intent 中的讀寫(xiě)權(quán)限。

授權(quán)有效期:從授權(quán)一刻開(kāi)始掘宪,截止于設(shè)備重啟或者手動(dòng)調(diào)用 Context.revokeUriPermission() 方法蛾扇,才會(huì)收回對(duì)此 Uri 的授權(quán)

2、配合 Intent.addFlags() 授權(quán)魏滚。

既然授權(quán)類型的常量是一個(gè) Intent 的 Flag镀首,Intent 也提供了另外一種比較方便的授權(quán)方式,那就是使用 Intent.setFlags() 或者 Intent.addFlag 的方式鼠次。

授權(quán)有效期:從授權(quán)一刻開(kāi)始更哄,截止于App完全退出應(yīng)用

5 通過(guò) startXxx 或者 setResult() 的方式,將 Uri 傳遞給其他的 App

拿剪裁圖片舉例

大功告成腥寇!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末成翩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子赦役,更是在濱河造成了極大的恐慌麻敌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件掂摔,死亡現(xiàn)場(chǎng)離奇詭異术羔,居然都是意外死亡赢赊,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門级历,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)释移,“玉大人,你說(shuō)我怎么就攤上這事寥殖⊥婊洌” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵嚼贡,是天一觀的道長(zhǎng)熏纯。 經(jīng)常有香客問(wèn)我,道長(zhǎng)编曼,這世上最難降的妖魔是什么豆巨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮掐场,結(jié)果婚禮上往扔,老公的妹妹穿的比我還像新娘。我一直安慰自己熊户,他們只是感情好萍膛,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著嚷堡,像睡著了一般蝗罗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蝌戒,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天串塑,我揣著相機(jī)與錄音,去河邊找鬼北苟。 笑死桩匪,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的友鼻。 我是一名探鬼主播傻昙,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彩扔!你這毒婦竟也來(lái)了妆档?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤虫碉,失蹤者是張志新(化名)和其女友劉穎贾惦,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡纤虽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年乳绕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了绞惦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逼纸。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖济蝉,靈堂內(nèi)的尸體忽然破棺而出杰刽,到底是詐尸還是另有隱情,我是刑警寧澤王滤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布贺嫂,位于F島的核電站,受9級(jí)特大地震影響雁乡,放射性物質(zhì)發(fā)生泄漏第喳。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一踱稍、第九天 我趴在偏房一處隱蔽的房頂上張望曲饱。 院中可真熱鬧,春花似錦珠月、人聲如沸扩淀。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)驻谆。三九已至,卻和暖如春庆聘,著一層夾襖步出監(jiān)牢的瞬間胜臊,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工伙判, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留象对,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓澳腹,卻偏偏與公主長(zhǎng)得像织盼,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子酱塔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345