我想把 FileProvider 聊的更透徹一些

承香墨影發表於2017-07-23

版權宣告:

本賬號釋出文章均來自公眾號,承香墨影(cxmyDev),版權歸承香墨影所有。

未經允許,不得轉載。

一、前言

從 Android N(7.0) 開始,將嚴格執行 StrictMode 模式,也就是說,將對安全做更嚴格的校驗。而從 Android N 開始,將不允許在 App 間,使用 file:// 的方式,傳遞一個 File ,否者會丟擲 FileUriExposedException 的錯誤,會直接引發 Crash。

但是,既然官方對檔案的分享做了一個這麼強硬的修改(直接丟擲異常),實際上也提供瞭解決方案,那就是 FileProvider,通過 comtent:// 的模式替換掉 file:// ,同時,需要開發者主動升級 targetSdkVersion 到 24 才會執行此策略,也留給了開發者升級的時間。

有關 Android 為了相容而配置的 targetSdkVersion,不瞭解的可以看看之前的文章《》。

本文就 FileProvider 需要了解的所有細節,進行一個詳盡的說明。

二、如何使用 FileProvider

1、什麼是 FileProvider

FileProvider 是 Android support v4 包下,提供的一個 ContentProvider 的子類,用於向其他 App 分享檔案,並且是在 v4 包下,所以只要引入了 v4 包,就可以做到全版本相容。

既然 FileProvider 本質上就是一個 ContentProvider ,它其實也繼承了 ContentProvider 的特性。ContentProvider 其實就是在可控的範圍內,向外部其他的 App 分享資料。而 FileProvider 將這樣的資料變成了一個 File 檔案而已。

2、在什麼場景下需要使用 FileProvider

在 App 間對 file:// 的分享做了嚴格的校驗之後,其實也是出於安全考慮,這就導致了,所有包含 file:// 的URI 的 Intent 離開你的 App ,都受此限制。所以說,只要你的 App 內,通過一個 Intent 傳遞了一個 file:// 的 Uri ,就需要小心使用了。

在實際開發過程中,使用最多的場景有一下幾個:

  • 呼叫相機拍照。
  • 剪裁圖片。
  • 呼叫系統安裝器去安裝 Apk。

3、如何使用 FileProvider

1)在 AndroidManifest 中配置

前面提到,FileProvider 實際上是一個 ContentProvider ,所以如果需要使用它,就需要在 AndroidManifest.xml 中宣告它。


可以看到,provider 標籤下,配置了幾個屬性:

  • name :配置當前 FileProvider 的實現類。
  • authorities:配置一個 FileProvider 的名字,它在當前系統內需要是唯一值。
  • exported:表示該 FileProvider 是否需要公開出去,這裡不需要,所以是 false。
  • granUriPermissions:是否允許授權檔案的臨時訪問許可權。這裡需要,所以是 true。

可以看到 name 屬性就是標記當前 FileProvider 的實現類,對於一個 App Module 而言,如果只是自己使用,可以直接使用 v4 包下的 FileProvider ,但是如果是作為一個 Lib Module 來供其他專案使用,最好還是重新空繼承一個 FileProvider ,這裡填寫我們的繼承類即可。

2) 指定可分享的檔案路徑

在配置 Provider 的時候,還需要額外配置一個 <meta-data/> 標籤,它用於配置 FileProvider 支援分享出去的目錄。這個 <meta-data/> 標籤的 name 值是固定的,resource 需要指向一個 根節點為 paths 的 xml 資原始檔。


然後就可以對 provider_paths.xml 進行配置。


paths 標籤內,必須配置最少一個 xxx-path 標籤,上圖給出的例子,配置的是 files-path 這些配置的資訊,都是可以在官方文件中找到答案的,這裡直接以查閱原始碼的方式來檢視他們分別代表的意思。

這些配置,在 FileProvider 的原始碼內,都是以一個個 TAG_Xxx 標記的。


而他們分別代表的目錄,也可以在原始碼內找到答案。


可以看到,不同的標籤,代表不同的目錄。

  • 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 只是對 Context 做了一個相容處理,其實就是對 Api level 19 做了一個分解,分別代表不同的獲取方式,以 getExternalFilesDirs() 為例。

3) 使用 content://

配置工作已經全部完成,後面就需要將之前傳遞的 file:// 替換成 FileProvider 需要的 content:// ,這就需要用到 FileProvider.getUriForFile() 方法了,以下是它的完整簽名。


getUriForFile() 方法,需要一個 authority 的引數,這正是前面在 AndroidManifest.xml 中 配置的 android:authorities

呼叫此方法,會自動得到一個 file:// 轉換成 content:// 的 一個 Uri 物件,可以供我們直接使用。

4) 授予臨時的讀寫許可權

在配置 provider 標籤的時候,有一個屬性 android:grantUriPermissions="true" ,它表示允許它授予 Uri 臨時的許可權。

當我們生成出一個 content:// 的 Uri 物件之後,其實也無法對其直接使用,還需要對這個 Uri 接收的 App 賦予對應的許可權才可以。

授權型別的常量,被定義在 Intent 類中。


可以看到,直接就是讀和寫的許可權授予。

而這個授權的動作,提供了兩種方式來授權:

1、使用 Context.grantUriPermission() 為其他 App 授予 Uri 物件的訪問許可權。

它的完整簽名如下:


grantUriPermission() 方法包含三個引數,這三個引數都非常的好理解。

  • toPackage :表示授予許可權的 App 的包名。
  • uri:授予許可權的 content:// 的 Uri。
  • modeFlags:前面提到的讀寫許可權。

這種情況下,授權的有效期限,從授權一刻開始,截止於裝置重啟或者手動呼叫 Context.revokeUriPermission() 方法,才會收回對此 Uri 的授權。

2、配合 Intent.addFlags() 授權。

既然這是一個 Intent 的 Flag,Intent 也提供了另外一種比較方便的授權方式,那就是使用 Intent.setFlags() 或者 Intent.addFlag 的方式。

這種方式相信大家都比較熟悉,就不細說了。而使用這種形式的授權,許可權截止於該 App 所處的堆疊被銷燬。也就是說,一旦授權,直到該 App 被完全退出,這段時間內,該 App 享有對此 Uri 指向的檔案的對應許可權,我們無法再主動收回此許可權了。

雖然使用 Intent.addFlags() 的方式,一旦授權將無法主動回收,但是大多數情況下,也是會使用此種方式進行授權,除了操作起來方便之外,既然授權了也無需太擔心對方會有破壞的行為。有點切合 用人不疑,疑人不用 的道理。

擁有了授權許可權的 content:// 的 Uri 之後,就可以通過 startXxx 或者 setResult() 的方式,將 Uri 傳遞給其他的 App。

5)舉個例子

到這裡,基本上關於 FileProvider 的使用,都做了一個詳盡的說明,接下來舉個簡單的例子來看看如何使用它。

調起系統安裝器來安裝一個 Apk 。

三、FileProvider 的注意事項

1、authorities 的唯一性

在 AndroidManifest.xml 中配置 provider 的時候,需要保證 android:authorities 的值,在整個系統中的唯一性。其實這也很好理解,看了 FileProvider.getUriForFile() 之後,發現它是通過 android:authorities 屬性配置的值,來唯一確定由誰來響應這個 provider 的,所以它需要保證在系統內唯一,否者安裝的時候會丟擲異常。


而在常規開發過程中,如果是一個 App Module 在使用 FileProvider 的話,那麼只需要我們自己規範不要寫同一個 authorities 即可。但是如果是作為一個 Lib Module 釋出出去的話,是需要考慮使用者的如何使用的,所以為了友好起見,最好使用 applicationId 來配置 provider 標籤。


這樣配置之後,就會使用 Gradle 中配置的 applicationId 的值替換這裡,而使用 FileProvider.getUriForFile() 的時候,只需要根據 applicationId 拼接一個 authorities 值即可,簡單修改一下上面呼叫系統去安裝 APK 的例子。

2、Lib 下的 targetSdkVersion

前面提到,如果不將 targatSdkVersion 升級到 24 的話,之前的方式依然是可用的,不會有 FileUriExposedException 的隱患。但是如果你的專案是作為一個 Lib Module 這種 SDK 的形式釋出出去,供其他人使用的話,這裡的 targetSdkVersion 就不受 Lib 的 targetSdkVersion 控制,而是主專案的 targetSdkVersion。

所以如果是以 SDK 的形式整合到別的 App 內使用的話,如果需要用傳送一個 File 給其他 App,一定要適配 FileProvider 。

3、不使用 v4 包

FIleProvider 是存在於的 Support v4 包下,所以想要使用 FileProvider 就必須整合 v4 包。但是對於一個本身無需使用 v4 包的專案來說,為了 FileProvider 來整合 v4 包,無形中就增加了安裝包的體積。

但是仔細看 FileProvider ,其實並沒有引用到什麼更多的 package ,而 FileProvider 本質上也只是一個 ContentProvider ,所以我們只需要將它的程式碼複製出來,簡單修改一下保證可以正確執行,就可以使用,而不是必須繼承 v4 包。

四、小結

FileProvider 的核心就是提高安全性,讓開發者來限制自己本 App 的檔案對外的訪問許可權,以提高安全性。

所以在開發過程中,只需要配合 FileProvider 將我們可能需要第三方 App 用到的檔案目錄加入到可授權的範圍,然後在傳送 Intent 的時候,對其進行授權即可,其他的操作和之前並無變化,這裡就不一一列舉了。

公眾號二維碼.jpg
公眾號二維碼.jpg

相關文章