整個(gè)Android系統(tǒng)文件分為下面幾個(gè)模塊
模塊名稱 | 內(nèi)容類型 | 訪問方法 | 所需權(quán)限 | 其他應(yīng)用是否可以訪問 | 卸載應(yīng)用時(shí)候是否移除 |
---|---|---|---|---|---|
應(yīng)用專屬文件 | 僅供您的應(yīng)用使用文件 | 內(nèi)部存儲(chǔ)空間 getFilesDir或getCacheDir 外部存儲(chǔ)空間 getExternalFilesDir或者getExternalCacheDir | 內(nèi)部存儲(chǔ)空間不需要任何權(quán)限 如果應(yīng)用在搭載Android4.4(API 19) 或更高版本設(shè)備運(yùn)行,從外部存儲(chǔ)空間訪問不需要任何權(quán)限 | 不支持 | 是 |
媒體 | 可共享的媒體文件: 圖片音頻文件、視頻 | MediaStore API | 在Android 11(API 30) 或者更高版本中訪問其他應(yīng)用文件需要 READ_EXTERNAL_STORAGE 在Android10(API 29) 中,訪問其他應(yīng)用的文件需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 在Android 9 或更低版本,訪問所有文件均需要相關(guān)權(quán)限 | 是,共享目錄可以訪問 | 不支持 |
文檔和其他文件 | 其他類型的可共享內(nèi)容,包括已下載的文件 | 存儲(chǔ)訪問框架 | 無 | 是,可以通過系統(tǒng)文件選擇器訪問 | 不支持 |
應(yīng)用偏好設(shè)置 | 鍵值對(duì) | Jetpack Preferences庫(kù) | 無 | 不支持 | 是 |
數(shù)據(jù)庫(kù) | 結(jié)構(gòu)化數(shù)據(jù) | Room 持久性庫(kù) | 無 | 不支持 | 是 |
應(yīng)用專屬文件
不需要任何權(quán)限。直接使用文件路徑,進(jìn)行讀寫操作就行。這個(gè)比較簡(jiǎn)單 注意目錄不要使用全路徑方式,而是使用getFilesDir等API 就可以。
Android10的時(shí)候引入了作用域存儲(chǔ)的概念。 在Android10 以前,外部存儲(chǔ)屬于公共空間,不計(jì)入應(yīng)用程序占用的空間,所有應(yīng)用可以申請(qǐng)權(quán)限進(jìn)行隨意訪問。并且卸載了,創(chuàng)建的文件也保留下來。
Android 10 開始,對(duì)SD卡進(jìn)行了限制,每個(gè)應(yīng)用只有權(quán)限讀取自己外置存儲(chǔ)空間關(guān)聯(lián)的目錄,如下面的地址。該目錄下文件記錄到應(yīng)用程序的空間,并且隨應(yīng)用卸載而刪除。這些目錄就是應(yīng)用專屬空間。
/storage/emulated/0/Android/data/<包名>/files
/storage/emulated/0/Android/data/<包名>/caches
媒體文件訪問讀寫
權(quán)限需要:READ_EXTRERNAL_STORGE
讀取媒體庫(kù)文件
在在作用域存儲(chǔ)中,我們只能使用MediaStore API獲取,無法像之前使用絕對(duì)路徑了 這里舉例圖片
fun queryMedia() {
val cursor = ContentResolverCompat.query(
this.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null,
null, null, null
)
cursor.use {
while (it.moveToNext()) {
//獲取文件地址, 現(xiàn)在還能用,但是被標(biāo)注廢棄
val filePath =
cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA))
//文件名稱
val fileName =
cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME))
//文件uri
val id =
cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media._ID))
val uri =
ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
//獲取文件流
val inputStream = contentResolver.openInputStream(uri)
}
}
}
新增媒體庫(kù)文件
fun insertMedia(bitmap: Bitmap, displayName: String) {
val contentValues = ContentValues()
//圖片名稱
contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, displayName)
//圖片mime類型
contentValues.put(MediaStore.Images.Media.MIME_TYPE, "image/*")
val contentUri: Uri =
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
} else {
MediaStore.Images.Media.INTERNAL_CONTENT_URI
}
contentValues.put(
MediaStore.Images.Media.RELATIVE_PATH,
Environment.DIRECTORY_DCIM + "/" + displayName
)
contentValues.put(MediaStore.Images.Media.IS_PENDING,1)
contentValues.put
val uri = contentResolver.insert(contentUri, contentValues)
uri?.let {
val outputStream = contentResolver.openOutputStream(uri)
outputStream?.use {
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
}
}
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
uri?.let {
contentResolver.update(it, contentValues, null, null)
}
}
修改或者刪除媒體庫(kù)中文件
平時(shí)開發(fā)場(chǎng)景比較少見,但是也需要注意,在 targetSDK 在30的時(shí)候 在Android 11上運(yùn)行 需要進(jìn)行權(quán)限申請(qǐng)
1. MediaStore.createWriteRequest()
2. MediaStore.createDeleteRequest()
在Android 10上運(yùn)行 如果targetSDK 是 29 的話 停用分區(qū)存儲(chǔ),并繼續(xù)使用Android 9 執(zhí)行這類操作 在Android 9或者更低版本運(yùn)行
- 申請(qǐng)WRITE_EXTERNAL_STORAGE 權(quán)限。
- 使用MediaStore API 進(jìn)行修改或者刪除媒體文件
// 修改圖片
// 構(gòu)造 ContentValues
var contentValues: ContentValues = ContentValues();
// 將 display_name 修改成 image_update
contentValues.put(MediaStore.Images.ImageColumns.DISPLAY_NAME, "image_update.jpg")
// 修改文件名稱
var row = contentResolver.update(uri!!, contentValues, null, null)
Log.i(TAG, "修改 uri = $uri 結(jié)果 row = $row")
//刪除圖片
var row = contentResolver.delete(uri!!, null, null)
文檔以及其他文件
權(quán)限: 無需權(quán)限
SAF(存儲(chǔ)訪問框架)
Android 4.4 (API級(jí)別19) 引入了存儲(chǔ)訪問框架. SAFA 允許用戶在其所有首選文檔存儲(chǔ)提供程序中瀏覽和打開文檔、圖片和其他文件. 通過易用的標(biāo)準(zhǔn)節(jié)目,用戶在各個(gè)應(yīng)用和提供程序中以一致的方式瀏覽文件和訪問近期文件。
- 客戶端應(yīng)用:一般指的是第三方應(yīng)用。
- 選取器(
Picker
):選取的系統(tǒng)UI
,Download
模塊里特指DocumentsUI
。 - 文檔提供程序:內(nèi)容提供者,也就是常說的
ContentProvider
, 文檔提供程序作為DocumentsProvider
類的子類實(shí)現(xiàn)。
工作流程如下
- 客戶端向
Picker
請(qǐng)求直接與文件交互的權(quán)限
-
Intent.ACTION_OPEN_DOCUMENT
: 對(duì)提供內(nèi)容有長(zhǎng)期、持久性訪問權(quán)限。 -
Intent.ACTION_CREATE_DOCUEMNT
: 只對(duì)提供內(nèi)容進(jìn)行數(shù)據(jù)讀取導(dǎo)入。 -
Intent.ACTION_GET_CONTENT
: 創(chuàng)建新文檔。
-
Picker
收到請(qǐng)求之后,將所有已注冊(cè)的provider 中尋找符合要求的數(shù)據(jù)源,并向用戶展示 - 用戶對(duì)內(nèi)容選取之后,返回給客戶端,然后進(jìn)行增刪改查
讀取文檔以及其他文件
var fileSelectedLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.data?.data != null) {
//TODO 根據(jù)uri拿到文件流
var fileInputStream = contentResolver.openInputStream()it.data!!.data!!)
}
}
fun openFileSelected(){
fileSelectedLauncher?.launch(
createDocumentChooseIntent(
arrayOf(
"application/msword",
"application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
)
)
)
}
private fun createDocumentChooseIntent(mimeTypes: Array<String?>?): Intent {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
intent.setType("*/*")
if (!mimeTypes.isNullOrEmpty()) {
intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
}
intent.addCategory(Intent.CATEGORY_OPENABLE)
return intent
}
新增文件
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/pdf"
putExtra(Intent.EXTRA_TITLE, "invoice.pdf")
// 可選參數(shù)為要打開的目錄指定一個(gè)URI 應(yīng)用程序創(chuàng)建文檔之前的系統(tǒng)文件選擇器。
//putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
activityResultLauncher.launch(intent)
result 返回一個(gè)uri, 然后我們使用contentResolver.openOutputStream, 進(jìn)行文件寫入
刪除文件
val cursor = contentResolver.query(documentUri, null, null, null, null)
cursor?.use {
val flags: Long =
it.getLong(it.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS))
if ((flags and DocumentsContract.Document.FLAG_SUPPORTS_DELETE.toLong()) > 0) {
DocumentsContract.deleteDocument(contentResolver, documentUri)
}
}
開發(fā)過程中注意問題
文件選擇器返回之后為uri , 只能拿到輸入流,但是接口需要filePath怎么辦? 首先區(qū)分該文件是媒體文件,還是文檔類型, 媒體文件直接讀取MediaStore.MediaColumns.DATA
就是文件路徑。 文檔類型兩個(gè)方法
- 把用戶選擇文件拷貝到應(yīng)用專屬目錄(實(shí)際工作中自身采用)
- uri 轉(zhuǎn)path 可以使用 Utils 類