Android 文件系統(tǒng)總結(jié)

整個(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)行

  1. 申請(qǐng)WRITE_EXTERNAL_STORAGE 權(quán)限。
  2. 使用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)用和提供程序中以一致的方式瀏覽文件和訪問近期文件。

image.png
  • 客戶端應(yīng)用:一般指的是第三方應(yīng)用。
  • 選取器(Picker):選取的系統(tǒng)UIDownload模塊里特指DocumentsUI
  • 文檔提供程序:內(nèi)容提供者,也就是常說的ContentProvider, 文檔提供程序作為DocumentsProvider類的子類實(shí)現(xiàn)。

工作流程如下

  1. 客戶端向 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)建新文檔。
  1. Picker 收到請(qǐng)求之后,將所有已注冊(cè)的provider 中尋找符合要求的數(shù)據(jù)源,并向用戶展示
  2. 用戶對(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è)方法

  1. 把用戶選擇文件拷貝到應(yīng)用專屬目錄(實(shí)際工作中自身采用)
  2. uri 轉(zhuǎn)path 可以使用 Utils 類
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容