關於Android Q分區存儲的一些適配心得

前言

  Android Q最大的變化莫過因而對用戶隱私權的進一步保護,其中有一個feature更是讓Android用戶(尤爲是國內用戶)拍手稱快,這就是分區存儲(Scoped Storage, 也有翻譯爲存儲沙盤化的)。截止目前,Google已經發布了Android Q的第4個beta版本(QPP4),想必許多開發者已經開始適配(踩坑)了。最近爲了避免在年末的時候手忙腳亂,本人也在開始準備Q的適配了。目前關於Scoped Storage適配的文章已經很多了,但我的以爲大多都講得太泛,缺少實際的操做指南,看完以後仍是有些雲裏霧裏。因而,筆者決定結合現有的文章,本身以實際行動踩坑,總結一些實際的適配技巧。html

  本文也不打算寫成一篇大而全的適配指南,只是爲了補充現有適配文章的一些不足,講一些我的經實踐驗證過的Scoped Storage適配技巧。java

  關於Scoped Storage在Android Q上的全部行爲都是在AndroidStudio上的模擬器上驗證的,模擬器系統版本爲QPP4。android

關於Scoped Storage

  關於Scoped Storage在開始以前,先簡單說說Scoped Storage的理解。要理解Google引入這個feature的緣由,你只須要隨便找一臺Android手機,打開文件管理器:segmentfault

  如今你們明白了吧?在Q之前,任何一個APP, 一旦拿到了外部權限(WRITE_EXTERNAL_STORAGE)後,就能夠在你的內部存儲的根目錄下肆意創建文件夾了,這致使幾乎每一個Android用戶的內部存儲活像一個垃圾桶,想必大多數人都體驗過在這一堆文件夾中定位本身的某一個文檔的痛苦吧。微信

  Google想必也是聽到了用戶們的抱怨,下決心要好好管一管這個事了,引入了Scoped Storage來防止App們處處建文件夾的行爲,並且態度還挺強硬,無論你targetSDK調不調到29,反正只要運行在Q上,Scoped Storage就會強制適用。因此在第二個beta版本發佈後,不少用戶發現很多APP包括微信的媒體選擇器都掛了。但這沒持續多久,Google就心軟了,在beta3時又放寬了適用策略,表示給你們一些適配的時間,可是明年Android R發佈時就不給機會了,一概強制適用。app

到目前爲止,Scoped Storage的適用策略以下:google

  • targetSDK = 29, 默認開啓Scoped Storage, 但可經過在manifest裏添加requestLegacyExternalStorage = true關閉;
  • targetSDK < 29, 默認不開啓Scoped Storage, 但可經過在manifest裏添加requestLegacyExternalStorage = false打開;

有兩點要注意:spa

  1. 當你的targetSDK < 29,而且想經過requestLegacyExternalStorage來打開Scoped Storage策略時,你須要把compileSdkVersion上調到29, 不然會編譯失敗。另外,可在運行時經過Environment.isExternalStorageLegacy()判斷Scoped Storage策略是否打開。
  2. 當修改了requestLegacyExternalStorage屬性的值,必需要卸載掉舊APK,從新安裝纔會生效。

接下來咱們經過實際的例子來對比Scoped Storage策略適用先後的一些行爲變化。翻譯

適配心得

1. getExternalStorageDirectory(), getExternalStoragePublicDirectory()讀寫權限變化

在以前,只要你有外部存儲權限,你能夠經過如下的操做,在內部儲存肆意構建本身的目錄結構:code

File dir = new File(Environment.getExternalStorageDirectory(), "my_dir");
    if(!dir.exists()){
        dir.mkdir();
    }
複製代碼

可是Scoped Storage引入後,你會發現以上代碼根本不起做用了,這樣APP就沒法再亂建文件夾啦。

2. Java File API, BitmapFactory.decodeFile()沒法讀寫app-specific目錄以外的地方

  • app-specific目錄:即經過context. getExternalFilesDir()返回的目錄,通常爲/storage/emulated/0/Android/data/<package name>/files/, 這是屬於APP的私有目錄,在該目錄下的讀寫是不須要申請權限的,當APP卸載時,系統會清理該目錄。值得一提的是,在Q以前,其餘擁有外部存儲權限的APP其實也是能夠讀寫該目錄的,但從Q開始,這個行爲被禁止了。

當你獲取到一個app-specific目錄以外的文件路徑時,你也許會這麼這麼作: 將文件路徑傳給FileOutputStream或者FileWriter,而後開始讀寫操做;又或者該文件是張圖片,你經過BitmapFactory.decodeFile()來獲取到Bitmap對象。

好比我在項目中曾見過這種作法:經過MediaStore API中的DATA字段獲取到圖片的路徑,接着就經過BitmapFactory.decodeFile()獲取Bitmap對象。

只要你得到了外部存儲權限, 這麼作沒問題。但Scoped Storage適用以後, 這些行爲也被禁止了。谷歌推薦採用FileDescriptor的方式,以下:

ContentResolver cr = context.getContentResolver();
    ParcelFileDescriptor fd = cr.openFileDescriptor(captureUri, "r");
    
    //接下來就能夠讀寫了
    FileInputStream istream = new FileInputStream(fd.getFileDescriptor());//讀
    FileOutputStream ostream = new FileOutputStream(fd.getFileDescriptor());//寫
    
    //對於圖片的狀況,能夠這麼作
    Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd.getFileDescriptor());
複製代碼

順便提一下,關於Media.DATA, 在Scoped Storage的官方介紹頁面裏也有這麼一句話:

Don't load media files using the deprecated DATA columns.

想必你們也注意到了,以上操做都必須是在獲取了文件Uri的前提下才能進行,文件Uri的獲取方式不少,這裏不展開討論。你只須要知道,你沒法再經過文件路徑跟app-specific目錄外的文件打交道了。

3. APP產生的文件只能經過MediaStore API寫入磁盤

前面也提到了,你沒法直接經過文件路徑來讀寫app-specific目錄外的位置了。你也許會說那我往app-specific裏存不就完事了嗎,更不用申請存儲權限, 還不怕被其餘應用窺探到文件內容。是的,谷歌確實推薦這麼作,但並非全部的數據都適合放在這裏。假如你的APP是圖像或視頻類應用,使用過程當中產生的圖片視頻就不適合放在app-specific裏,首先是這個目錄路徑太深,用戶很差查找,其次是這一類數據用戶不但願隨應用卸載而被刪掉。因此必需要尋求放在app-specific目錄以外的地方。但正如前面所說,你必需要有Uri才能讀寫,這個時候你就得用到MediaStore API了,下面以建立圖片爲例:

ContentValues contentValues = new ContentValues();
    contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, fileName);
    contentValues.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
    contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
    Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);
複製代碼

那麼這個時候你就有了一個Uri了,接着就能夠按照上述所提到的使用FileDescriptor的方式去寫文件了。不過這也有個問題,你往MediaStore裏插入一條記錄後,對應Uri就可能被其餘應用檢索到,但又可能找不到這條記錄對應的那個文件(由於此時你的文件可能還沒真正寫入),這個問題Google也給了一個解決方案

再看另一個更爲常見的例子—調用相機拍攝並存儲照片,這個操做在Android Developer上的training中提供了最佳實踐,這個例子中將照片存在了app-specific目錄,但在實際業務中咱們更多是放在app-specific目錄以外,只要你有外部存儲權限,這是能夠作到的,可是在Scoped Storage策略下,你必須得經過MediaStore API來產生照片的Uri了,而後經過如下語句傳給Intent takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);

那麼接下來你可能會有兩個問題:

  • 問題1

上面經過MediaStore建立Uri的時候,咱們沒有指定文件路徑(MediaStore.Images.Media.DATA),那文件最終會存到哪?

系統會按分類自動幫你存入到相應的文件夾下,默認在Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_XXXX)返回的路徑下,好比圖片就是Environment.DIRECTORY_PICTURES, 音頻文件就是Environment. DIRECTORY_MUSIC……

  • 問題2

這樣的話那個人APP產生的圖片豈不是跟其餘APP的圖片放在經過文件夾下,這樣不是也很混亂嗎? 不用擔憂,你能夠經過Media.RELATIVE_PATH創建本身的二級目錄,假如上面的圖片我想放到Pictures/MY_PIC/目錄下,只須要這麼作:

contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MY_PIC");
複製代碼

圖片也不必定只能存到Pictures中,也能夠放到DCIM目錄中,也經過上述字段來實現,但若是你這麼作的話:

contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, Environment. DIRECTORY_MOVIES);
複製代碼

你會收到以下提示:

Primary directory Movies not allowed for content://media/external/images/media; allowed directories are [DCIM, Pictures]
複製代碼

後言

以上即是本人對Scoped Storage的一些適配心得,但願可以對你們有所幫助。若有錯誤,歡迎指正。另外,在Android Q的正式版發佈時以上的行爲可能還會發生變化。 關於Scoped Storage更全面的信息,建議你們閱讀參考連接。

參考連接

  1. segmentfault.com/a/119000001…
  2. developer.android.com/preview/pri…
相關文章
相關標籤/搜索