我想把 FileProvider 聊的更透徹一些
一、前言
從 Android N(7.0) 開始,將嚴(yán)格執(zhí)行 StrictMode 模式,也就是說,將對(duì)安全做更嚴(yán)格的校驗(yàn)。而從 Android N 開始,將不允許在 App 間,使用 file:// 的方式,傳遞一個(gè) File ,否者會(huì)拋出 FileUriExposedException 的錯(cuò)誤,會(huì)直接引發(fā) Crash。
但是,既然官方對(duì)文件的分享做了一個(gè)這么強(qiáng)硬的修改(直接拋出異常),實(shí)際上也提供了解決方案,那就是 FileProvider,通過 comtent:// 的模式替換掉 file:// ,同時(shí),需要開發(fā)者主動(dòng)升級(jí) targetSdkVersion 到 24 才會(huì)執(zhí)行此策略,也留給了開發(fā)者升級(jí)的時(shí)間。
本文就 FileProvider 需要了解的所有細(xì)節(jié),進(jìn)行一個(gè)詳盡的說明。
二、如何使用 FileProvider
1、什么是 FileProvider
FileProvider 是 Android support v4 包下,提供的一個(gè) ContentProvider 的子類,用于向其他 App 分享文件,并且是在 v4 包下,所以只要引入了 v4 包,就可以做到全版本兼容。
既然 FileProvider 本質(zhì)上就是一個(gè) ContentProvider ,它其實(shí)也繼承了 ContentProvider 的特性。ContentProvider 其實(shí)就是在可控的范圍內(nèi),向外部其他的 App 分享數(shù)據(jù)。而 FileProvider 將這樣的數(shù)據(jù)變成了一個(gè) File 文件而已。
2、在什么場景下需要使用 FileProvider
在 App 間對(duì) file:// 的分享做了嚴(yán)格的校驗(yàn)之后,其實(shí)也是出于安全考慮,這就導(dǎo)致了,所有包含 file:// 的URI 的 Intent 離開你的 App ,都受此限制。所以說,只要你的 App 內(nèi),通過一個(gè) Intent 傳遞了一個(gè) file:// 的 Uri ,就需要小心使用了。
在實(shí)際開發(fā)過程中,使用最多的場景有一下幾個(gè):
- 調(diào)用相機(jī)拍照。
- 剪裁圖片。
- 調(diào)用系統(tǒng)安裝器去安裝 Apk。
3、如何使用 FileProvider
1)在 AndroidManifest 中配置
前面提到,F(xiàn)ileProvider 實(shí)際上是一個(gè) ContentProvider ,所以如果需要使用它,就需要在 AndroidManifest.xml 中聲明它。
可以看到,provider 標(biāo)簽下,配置了幾個(gè)屬性:
- name :配置當(dāng)前 FileProvider 的實(shí)現(xiàn)類。
- authorities:配置一個(gè) FileProvider 的名字,它在當(dāng)前系統(tǒng)內(nèi)需要是唯一值。
- exported:表示該 FileProvider 是否需要公開出去,這里不需要,所以是 false。
- granUriPermissions:是否允許授權(quán)文件的臨時(shí)訪問權(quán)限。這里需要,所以是 true。
可以看到 name 屬性就是標(biāo)記當(dāng)前 FileProvider 的實(shí)現(xiàn)類,對(duì)于一個(gè) App Module 而言,如果只是自己使用,可以直接使用 v4 包下的 FileProvider ,但是如果是作為一個(gè) Lib Module 來供其他項(xiàng)目使用,最好還是重新空繼承一個(gè) FileProvider ,這里填寫我們的繼承類即可。
2) 指定可分享的文件路徑
在配置 Provider 的時(shí)候,還需要額外配置一個(gè) <meta-data/> 標(biāo)簽,它用于配置 FileProvider 支持分享出去的目錄。這個(gè) <meta-data/> 標(biāo)簽的 name 值是固定的,resource 需要指向一個(gè) 根節(jié)點(diǎn)為 paths 的 xml 資源文件。
然后就可以對(duì) provider_paths.xml 進(jìn)行配置。
paths 標(biāo)簽內(nèi),必須配置最少一個(gè) xxx-path 標(biāo)簽,上圖給出的例子,配置的是 files-path 這些配置的信息,都是可以在官方文檔中找到答案的,這里直接以查閱源碼的方式來查看他們分別代表的意思。
這些配置,在 FileProvider 的源碼內(nèi),都是以一個(gè)個(gè) TAG_Xxx 標(biāo)記的。
而他們分別代表的目錄,也可以在源碼內(nèi)找到答案。
可以看到,不同的標(biāo)簽,代表不同的目錄。
- root-path:表示根目錄,『/』。
- files-path:表示 content.getFileDir() 獲取到的目錄。
- cache-path:表示 content.getCacheDir() 獲取到的目錄
- external-path:表示Environment.getExternalStorageDirectory() 指向的目錄。
- external-files-path:表示 ContextCompat.getExternalFilesDirs() 獲取到的目錄。
- external-cache-path:表示 ContextCompat.getExternalCacheDirs() 獲取到的目錄。
注意,這里 ContextCompat 只是對(duì) Context 做了一個(gè)兼容處理,其實(shí)就是對(duì) Api level 19 做了一個(gè)分解,分別代表不同的獲取方式,以 getExternalFilesDirs() 為例。
3) 使用 content://
配置工作已經(jīng)全部完成,后面就需要將之前傳遞的 file:// 替換成 FileProvider 需要的 content:// ,這就需要用到 FileProvider.getUriForFile() 方法了,以下是它的完整簽名。
getUriForFile() 方法,需要一個(gè) authority 的參數(shù),這正是前面在 AndroidManifest.xml 中 配置的 android:authorities 。
調(diào)用此方法,會(huì)自動(dòng)得到一個(gè) file:// 轉(zhuǎn)換成 content:// 的 一個(gè) Uri 對(duì)象,可以供我們直接使用。
4) 授予臨時(shí)的讀寫權(quán)限
在配置 provider 標(biāo)簽的時(shí)候,有一個(gè)屬性 android:grantUriPermissions="true" ,它表示允許它授予 Uri 臨時(shí)的權(quán)限。
當(dāng)我們生成出一個(gè) content:// 的 Uri 對(duì)象之后,其實(shí)也無法對(duì)其直接使用,還需要對(duì)這個(gè) Uri 接收的 App 賦予對(duì)應(yīng)的權(quán)限才可以。
授權(quán)類型的常量,被定義在 Intent 類中。
可以看到,直接就是讀和寫的權(quán)限授予。
而這個(gè)授權(quán)的動(dòng)作,提供了兩種方式來授權(quán):
1、使用 Context.grantUriPermission() 為其他 App 授予 Uri 對(duì)象的訪問權(quán)限。
它的完整簽名如下:
grantUriPermission() 方法包含三個(gè)參數(shù),這三個(gè)參數(shù)都非常的好理解。
- toPackage :表示授予權(quán)限的 App 的包名。
- uri:授予權(quán)限的 content:// 的 Uri。
- modeFlags:前面提到的讀寫權(quán)限。
這種情況下,授權(quán)的有效期限,從授權(quán)一刻開始,截止于設(shè)備重啟或者手動(dòng)調(diào)用 Context.revokeUriPermission() 方法,才會(huì)收回對(duì)此 Uri 的授權(quán)。
2、配合 Intent.addFlags() 授權(quán)。
既然這是一個(gè) Intent 的 Flag,Intent 也提供了另外一種比較方便的授權(quán)方式,那就是使用 Intent.setFlags() 或者 Intent.addFlag 的方式。
這種方式相信大家都比較熟悉,就不細(xì)說了。而使用這種形式的授權(quán),權(quán)限截止于該 App 所處的堆棧被銷毀。也就是說,一旦授權(quán),直到該 App 被完全退出,這段時(shí)間內(nèi),該 App 享有對(duì)此 Uri 指向的文件的對(duì)應(yīng)權(quán)限,我們無法再主動(dòng)收回此權(quán)限了。
雖然使用 Intent.addFlags() 的方式,一旦授權(quán)將無法主動(dòng)回收,但是大多數(shù)情況下,也是會(huì)使用此種方式進(jìn)行授權(quán),除了操作起來方便之外,既然授權(quán)了也無需太擔(dān)心對(duì)方會(huì)有破壞的行為。有點(diǎn)切合 用人不疑,疑人不用 的道理。
擁有了授權(quán)權(quán)限的 content:// 的 Uri 之后,就可以通過 startXxx 或者 setResult() 的方式,將 Uri 傳遞給其他的 App。
5)舉個(gè)例子
到這里,基本上關(guān)于 FileProvider 的使用,都做了一個(gè)詳盡的說明,接下來舉個(gè)簡單的例子來看看如何使用它。
調(diào)起系統(tǒng)安裝器來安裝一個(gè) Apk 。
三、FileProvider 的注意事項(xiàng)
1、authorities 的唯一性
在 AndroidManifest.xml 中配置 provider 的時(shí)候,需要保證 android:authorities 的值,在整個(gè)系統(tǒng)中的唯一性。其實(shí)這也很好理解,看了 FileProvider.getUriForFile() 之后,發(fā)現(xiàn)它是通過 android:authorities 屬性配置的值,來唯一確定由誰來響應(yīng)這個(gè) provider 的,所以它需要保證在系統(tǒng)內(nèi)唯一,否者安裝的時(shí)候會(huì)拋出異常。
而在常規(guī)開發(fā)過程中,如果是一個(gè) App Module 在使用 FileProvider 的話,那么只需要我們自己規(guī)范不要寫同一個(gè) authorities 即可。但是如果是作為一個(gè) Lib Module 發(fā)布出去的話,是需要考慮使用者的如何使用的,所以為了友好起見,最好使用 applicationId 來配置 provider 標(biāo)簽。
這樣配置之后,就會(huì)使用 Gradle 中配置的 applicationId 的值替換這里,而使用 FileProvider.getUriForFile() 的時(shí)候,只需要根據(jù) applicationId 拼接一個(gè) authorities 值即可,簡單修改一下上面調(diào)用系統(tǒng)去安裝 APK 的例子。
2、Lib 下的 targetSdkVersion
前面提到,如果不將 targatSdkVersion 升級(jí)到 24 的話,之前的方式依然是可用的,不會(huì)有 FileUriExposedException 的隱患。但是如果你的項(xiàng)目是作為一個(gè) Lib Module 這種 SDK 的形式發(fā)布出去,供其他人使用的話,這里的 targetSdkVersion 就不受 Lib 的 targetSdkVersion 控制,而是主項(xiàng)目的 targetSdkVersion。
所以如果是以 SDK 的形式集成到別的 App 內(nèi)使用的話,如果需要用發(fā)送一個(gè) File 給其他 App,一定要適配 FileProvider 。
3、不使用 v4 包
FIleProvider 是存在于的 Support v4 包下,所以想要使用 FileProvider 就必須集成 v4 包。但是對(duì)于一個(gè)本身無需使用 v4 包的項(xiàng)目來說,為了 FileProvider 來集成 v4 包,無形中就增加了安裝包的體積。
但是仔細(xì)看 FileProvider ,其實(shí)并沒有引用到什么更多的 package ,而 FileProvider 本質(zhì)上也只是一個(gè) ContentProvider ,所以我們只需要將它的代碼復(fù)制出來,簡單修改一下保證可以正確運(yùn)行,就可以使用,而不是必須繼承 v4 包。
四、小結(jié)
FileProvider 的核心就是提高安全性,讓開發(fā)者來限制自己本 App 的文件對(duì)外的訪問權(quán)限,以提高安全性。
所以在開發(fā)過程中,只需要配合 FileProvider 將我們可能需要第三方 App 用到的文件目錄加入到可授權(quán)的范圍,然后在發(fā)送 Intent 的時(shí)候,對(duì)其進(jìn)行授權(quán)即可,其他的操作和之前并無變化,這里就不一一列舉了。
【本文為51CTO專欄作者“張旸”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過微信公眾號(hào)聯(lián)系作者獲取授權(quán)】