分區存儲如何影響文件訪問:android
文件位置 | 所需權限 | 訪問方法 (*) | 卸載應用時是否移除文件? |
---|---|---|---|
特定於應用的目錄 | 無 | getExternalFilesDir() |
是 |
媒體集合 (照片、視頻、音頻) |
READ_EXTERNAL_STORAGE (僅當訪問其餘應用的文件時) |
MediaStore |
否 |
下載內容 (文檔和 電子書籍) |
無 | SAF存儲訪問框架 (加載系統的文件選擇器) |
否 |
對應於
MediaStore
類中僅包含五種文件類型Image/Video/Audio
以及Files
和Download
, 其中Image/Video/Audio
直接使用MediaStore
+ContentResolver
API便可訪問 , 而Files
和Download
則是使用SAF
存儲訪問框架訪問。緩存
⭐ 注意:使用分區存儲的應用對於 /sdcard/DCIM/IMG1024.JPG 這類路徑不具備直接內核訪問權限。要訪問此類文件,應用必須使用
MediaStore
,並調用ContentResolver.openFile()
等方法。bash
FileProvider
而且配置了external-files-path
和external-cache-path
,應用會在啓動時自動建立 cache
和files
目錄:<!--context.getExternalFilesDirs()-->
<external-files-path
name="ando_file_external_files"
path="." />
<!-- getExternalCacheDirs() 此標籤須要 support 25.0.0以上纔可使用-->
<external-cache-path
name="ando_file_external_cache"
path="." />
複製代碼
FileProvider
:app
<provider
android:name=".common.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>
複製代碼
分區存儲會施加如下媒體數據限制:框架
若您的應用未得到 ACCESS_MEDIA_LOCATION 權限,照片文件中的 Exif 元數據會被修改。要了解詳情,請參閱介紹如何訪問照片中的位置信息的部分。ide
MediaStore.Files 表格自己會通過過濾,僅顯示照片、視頻和音頻文件。例如,表格中不顯示 PDF 文件。 必須使用 MediaStore 在 Java 或 Kotlin 代碼中訪問媒體文件。請參閱有關如何從原生代碼訪問媒體文件
的指南。 該指南介紹瞭如何處理媒體文件,並提供了有關訪問 MediaStore 內的單個文檔和文檔樹的最佳作法。若是您的應用使用分區存儲,則須要使用這些方法來訪問媒體。post
系統會自動掃描外部存儲,並將媒體文件添加到如下定義好的集合中:ui
DCIM/
and Pictures/
directories. The system adds these files to the MediaStore.Images
table.DCIM/
, Movies/
, and Pictures/
directories. The system adds these files to the MediaStore.Video
table.Alarms/
, Audiobooks/
, Music/
, Notifications/
, Podcasts/
, and Ringtones/
directories, as well as audio playlists that are in the Music/
or Movies/
directories. The system adds these files to the MediaStore.Audio
table.Download/
directory. On devices that run Android 10 (API level 29) and higher, these files are stored in the MediaStore.Downloads
table. This table isn't available on Android 9 (API level 28) and lower.若是您的應用使用範圍存儲,則它僅應針對運行Android 9(API級別28)或更低版本的設備請求與存儲相關的權限。 您能夠經過在應用清單文件中的權限聲明中添加android:maxSdkVersion屬性來應用此條件:this
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
複製代碼
不要爲運行Android 10或更高版本的設備沒必要要地請求與存儲相關的權限。 您的應用程序能夠參與定義明確的媒體集合,包括MediaStore.Downloads集合,而無需請求任何與存儲相關的權限。 例如,若是您正在開發相機應用程序,則無需請求與存儲相關的權限,由於您的應用程序擁有您要寫入媒體存儲區的圖像。
照片:存儲在 MediaStore.Images 中。
視頻:存儲在 MediaStore.Video 中。
音頻文件:存儲在 MediaStore.Audio 中。
MediaStore 還包含一個名爲 MediaStore.Files 的集合,該集合提供訪問全部類型的媒體文件的接口。其餘文件,例如 PDF 文件,沒法訪問到。
複製代碼
注意:若是您的應用使用分區存儲,MediaStore.Files 集合將僅顯示照片、視頻和音頻文件。
若要加載媒體文件,請從 ContentResolver 調用如下方法之一:
openFileDescriptor()
。loadThumbnail()
,並傳入要加載的縮略圖的大小。ContentResolver.query()
。🌰查詢一個媒體文件集合
// Need the READ_EXTERNAL_STORAGE permission if accessing video files that your
// app didnt create.
// Container for information about each video.
data class Video(val uri: Uri,
val name: String,
val duration: Int,
val size: Int
)
val videoList = mutableListOf<Video>()
val projection = arrayOf(
MediaStore.Video.Media._ID,
MediaStore.Video.Media.DISPLAY_NAME,
MediaStore.Video.Media.DURATION,
MediaStore.Video.Media.SIZE
)
// Show only videos that are at least 5 minutes in duration.
val selection = "${MediaStore.Video.Media.DURATION} >= ?"
val selectionArgs = arrayOf(
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)
// Display videos in alphabetical order based on their display name.
val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"
val query = ContentResolver.query(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)
query?.use { cursor ->
// Cache column indices.
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
val nameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
val durationColumn =
cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
while (cursor.moveToNext()) {
// Get values of columns for a given video.
val id = cursor.getLong(idColumn)
val name = cursor.getString(nameColumn)
val duration = cursor.getInt(durationColumn)
val size = cursor.getInt(sizeColumn)
val contentUri: Uri = ContentUris.withAppendedId(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
id
)
// Stores column values and the contentUri in a local object
// that represents the media file.
videoList += Video(contentUri, name, duration, size)
}
}
複製代碼
若是您的應用程序執行一些可能很是耗時的操做,好比寫入媒體文件,那麼在文件被處理時對其進行獨佔訪問是很是有用的。在運行Android 10或更高版本的設備上,您的應用程序能夠經過將IS_PENDING
標誌的值設置爲1
來得到這種獨佔訪問。只有您的應用程序能夠查看該文件,直到您的應用程序將IS_PENDING
的值更改回0。
爲正在存儲的媒體文件提供待處理狀態
在搭載 Android 10(API 級別 29)及更高版本的設備上,您的應用能夠經過使用 IS_PENDING 標記在媒體文件寫入磁盤時得到對文件的獨佔訪問權限。
如下代碼段展現了在將圖片存儲到 MediaStore.Images 集合所對應的目錄時如何使用 IS_PENDING 標記:
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "IMG1024.JPG")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val resolver = context.getContentResolver()
val collection = MediaStore.Images.Media
.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = resolver.insert(collection, values)
resolver.openFileDescriptor(item, "w", null).use { pfd ->
// Write data into the pending image.
}
// Now that we're finished, release the "pending" status, and allow other apps // to view the image. values.clear() values.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(item, values, null, null) 複製代碼
Android Q 上,MediaStore 中添加了一個 IS_PENDING Flag,用於標記當前文件是 Pending 狀態。
其餘 APP 經過 MediaStore 查詢文件,若是沒有設置 setIncludePending 接口,就查詢不到設置爲 Pending 狀態的文件,這就能使 APP 專享此文件。
這個 flag 在一些應用場景下可使用,例如在下載文件的時候:下載中,文件設置爲 Pending 狀態;下載完成,把文件 Pending 狀態置爲 0。
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "myImage.PNG");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");
values.put(MediaStore.Images.Media.IS_PENDING, 1);
ContentResolver resolver = context.getContentResolver();
Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri item = resolver.insert(uri, values);
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(item, "w", null);
// write data into the pending image.
} catch (IOException e) {
LogUtil.log("write image fail");
}
// clear IS_PENDING flag after writing finished.
values.clear();
values.put(MediaStore.Images.Media.IS_PENDING, 0);
resolver.update(item, values, null, null);
複製代碼
Note: You can move files on disk during a call to update() by changing MediaColumns.RELATIVE_PATH or MediaColumns.DISPLAY_NAME.
注意:您能夠在調用update 的過程當中經過更改 MediaColumns.RELATIVE_PATH 或MediaColumns.DISPLAY_NAME 在磁盤上移動文件。
複製代碼
相關視頻 (Youtube):
Android 4.4(API 級別 19)引入了存儲訪問框架 (SAF)。藉助 SAF,用戶可輕鬆在其全部首選文檔存儲提供程序中瀏覽並打開文檔、圖像及其餘文件。用戶可經過易用的標準界面,以統一方式在全部應用和提供程序中瀏覽文件,以及訪問最近使用的文件。
⭐ Note: The DocumentFile class's
canWrite()
method doesn't necessarily indicate that your app can edit a document. That's because this method returns true ifDocument.COLUMN_FLAGS
contains eitherFLAG_SUPPORTS_DELETE
orFLAG_SUPPORTS_WRITE
. To determine whether your app can edit a given document, query the value ofFLAG_SUPPORTS_WRITE
directly.
Android 7.0 在存儲訪問框架中加入了虛擬文件的概念。即便虛擬文件沒有二進制表示形式,客戶端應用也可將其強制轉換爲其餘文件類型,或使用 ACTION_VIEW Intent 查看這些文件,從而打開文件中的內容。
如要打開虛擬文件,您的客戶端應用需包含可處理此類文件的特殊邏輯。若想獲取文件的字節表示形式(例如爲了預覽文件),則需從文檔提供程序請求另外一種 MIME 類型。
爲得到應用中虛擬文件的 URI,您首先需建立 Intent 來打開文件選擇器界面(如先前搜索文檔中的代碼所示)。
⭐ 重要說明:因爲應用不能使用 openInputStream() 方法直接打開虛擬文件,所以若是您在 ACTION_OPEN_DOCUMENT Intent 中加入 CATEGORY_OPENABLE 類別,則您的應用不會收到任何虛擬文件。
經過上面的分析能夠看出, MediaStore 僅能夠處理公共目錄中的 圖片/視頻/音頻
文件, 當涉及到分組文件和其它類型文件的時候顯得捉襟見肘。
- [操做一組文件](https://developer.android.google.cn/training/data-storage/shared/media#manage-groups-files)
- [操做文檔和其餘文件](https://developer.android.google.cn/training/data-storage/shared/media#other-file-types)
- [把數據分享給其它應用](https://developer.android.google.cn/training/data-storage/shared/media#companion-apps)
複製代碼
若是您提早知道要存儲多少數據,則能夠經過調用getAllocatableBytes()找出設備能夠爲應用程序提供多少空間。 getAllocatableBytes()的返回值可能大於設備上當前的可用空間量。 這是由於系統已識別出能夠從其餘應用程序的緩存目錄中刪除的文件。
// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;
val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
storageManager.allocateBytes(
appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
val storageIntent = Intent().apply {
action = ACTION_MANAGE_STORAGE
}
// Display prompt to user, requesting that they choose files to remove.
}
複製代碼
⭐ 保存文件以前,不須要檢查可用空間量。 相反,您能夠嘗試當即寫入文件,而後在發生異常時捕獲IOException。
AndroidManifest.xml中聲明:android:hasFragileUserData="true",卸載應用會有提示是否保留 APP數據。默認應用卸載時App-specific目錄下的數據被刪除,但用戶能夠選擇保留。
Youtube 👉 www.youtube.com/watch?v=UnJ…
Bilibili 👉 www.bilibili.com/video/BV1NE…