前言
從 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)的兩種方式:
- 使用 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
拿剪裁圖片舉例
大功告成腥寇!