你們應該都有過這樣的體會,手機用着用着裏面就充斥着各類不懂的文件夾和文件。甚至是連已經刪除的軟件的文件夾還存在。
爲何會發生的這樣的問題呢?
由於Google的缺席,致使Android生態野蠻生長,致使不少開發規範沒有徹底被落實。
爲了解決這樣的問題,Google決定重拳出擊,提出了分區存儲(Scoped Storage)機制,也叫沙盒存儲機制。git
那麼什麼是沙盒存儲機制呢。 沙盒機制是一種安全機制,用於防止應用讀取其餘應用的數據。github
每一個應用程序都有本身的存儲空間。
應用程序不能翻過本身的目錄,去訪問公共目錄。
應用程序請求的數據都要經過權限檢測,不符合要求不會被放行。
以 Android 10(API 級別 29)及更高版本爲目標平臺的應用在默認狀況下被賦予了對外部存儲設備的分區訪問權限(即分區存儲), 對外部存儲文件訪問方式從新設計,便於用戶更好的管理外部存儲文件。若是不符合條件的會以兼容模式運行,兼容模式跟之前同樣,根據路徑能夠直接存儲文件。安全
應用只能看到本應用專有的目錄(經過 Context.getExternalFilesDir() 訪問)以及特定類型的媒體。除非您的應用須要訪問存放在應用的專有目錄以及 MediaStore 以外的文件,不然最好使用分區存儲。框架
在發佈Android10的時候官方明確表態: 2020年,主要平臺版本將要求全部應用都使用分區存儲,不管應用的目標 SDK 級別是多少。所以,您應該提早確保您的應用可以使用分區存儲。爲此,請確保針對搭載 Android 10(API 級別 29)及更高版本的設備啓用了該行爲。
翻譯成通俗語言,不論是使用requestLegacyExternalStorage=true
的方式以兼容模式運行仍是下降targetSDK
都沒法在接下來2020年的Android(API 29)10更新中被豁免。ide
因此爲了應用的穩定性,應該盡在進行適配。post
默認狀況下,對於targetSdkVersion大於等於29的應用,其訪問權限範圍限定爲分區存儲。此應用無需請求與存儲相關的用戶權限,便可以查看外部存儲中如下類型的文件:測試
getExternalFilesDir()
訪問)。MediaStore
訪問)。分區存儲將影響在Android10
系統首次安裝啓動、且targetSdkVersion >=29
的應用。須要訪問和共享外部存儲文件的應用會受到影響,須要進行兼容性適配。動畫
影響範圍: 在Android 10上運行的應用:
1.targetSdkVersion <= 28,不受影響
2.若是targetSdkVersion >= 29,默認狀況應用外部存儲可見性將被過濾,應用須要對分區存儲進行適配。ui
還有值得注意的是如下兩種狀況比較特殊,不會受到分區存儲的影響:spa
若是應用最早安裝在Android 10如下的系統,
1) 而後系統經過Fota升級到Android 10
2) 應用經過更新升級到targetSdkVersion >= 29
下面是關於分區存儲權限和其餘相關項目的表格。
類型 | 位置 | 訪問應用本身生成的文件 | 訪問其餘應用生成的的文件 | 訪問方法 | 卸載應用是否刪除文件 |
---|---|---|---|---|---|
外部存儲 | Photo/ Video/ Audio/ | 無需權限 | 須要權限READ_EXTERNAL_STORAGE | MediaStore Api | 否 |
外部存儲 | Downloads | 無需權限 | 無需權限 | 經過存儲訪問框架SAF,加載系統文件選擇器 | 否 |
外部存儲 | 應用特定的目錄 | 無需權限 | 沒法直接訪問 | getExternalFilesDir()獲取到屬於應用本身的文件路徑 | 是 |
應用讀取或寫入應有專有的目錄中的文件時,不須要獲取存儲權限。
在應用中想要獲取當前應用的專有存儲目錄路徑是能夠用Context.getExternalFilesDir()
的方式獲取。
val dirpath = context.getExternalFilesDir("")
val fileString = dirpath + File.separator
val file = File(fileString)
... // 剩下的步驟是用Java IO或者其餘IO庫來寫入數據
複製代碼
在共享媒體集合存儲中保存媒體文件時,須要根據文件的類型選擇MediaStore
。
把相關數據放入到ContentValues中,最後把ContentValues插入到ContentResolver中,並得到返回的Uri。 經過Uri過得OutputStream,而後用Okio
的IO庫,進行文件的存儲。 關於Okio
的只是之後有機會的話,咱們再好好講一講。 不要忘了這裏須要獲取權限。
// 把圖片下載到共有媒體集合中,並在相冊中顯示
// 建立ContentValues, 並加入信息
val values = ContentValues()
values.put(MediaStore.Images.Media.DESCRIPTION, downloadedFile.name)
values.put(MediaStore.Images.Media.DISPLAY_NAME, downloadedFile.name)
values.put(MediaStore.Images.Media.MIME_TYPE, mimeType)
values.put(MediaStore.Images.Media.TITLE, downloadedFile.name)
values.put(
MediaStore.Images.Media.RELATIVE_PATH,
"${Environment.DIRECTORY_PICTURES}/${downloadedFile.name}"
)
// 插入到ContentResolver,並返回Uri
val insertUri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
)
if (insertUri != null) {
// 獲取OutputStream
val outputStream = context.contentResolver.openOutputStream(insertUri)
if (outputStream != null) {
sink = outputStream.sink().buffer()
} else {
return@runCatching FileDownloadResult.OthersError
}
} else {
return@runCatching FileDownloadResult.OthersError
}
val responseBody = response.body ?: return@runCatching FileDownloadResult.OthersError
try {
val contentLength = responseBody.contentLength()
if (contentLength > FileUtil.getAvailableSize(dirPath)) {
continuation.resume(FileDownloadResult.StorageError)
}
var totalRead: Long = 0
var lastRead: Long
do {
lastRead = responseBody.source().read(sink.buffer(), BUFFER_SIZE)
if (lastRead == -1L) {
break
}
totalRead += lastRead
sink.emitCompleteSegments()
} while (true)
sink.writeAll(responseBody.source())
sink.close()
responseBody.close()
}
複製代碼
Github: github.com/HyejeanMOON…
其餘教程:
Android Jetpack Room的詳細教程: juejin.im/post/5ebac9…
Android的屬性動畫(Property Animation)詳細教程: juejin.im/post/5eb7a5…
Android ConstraintLayout的易懂教程: juejin.im/post/5ea50a…
在RecyclerView中能夠應對多個ViewType的庫--Groupie: juejin.im/post/5e9059…
Google的MergeAdapter的使用: juejin.im/post/5e903f…
Paging在Android中的應用: juejin.im/post/5e75db…
Android UI測試之Espresso: juejin.im/post/5e6caa…
Android WorkManager的使用: juejin.im/post/5e635e…