Android 文件系统总结
整个Android系统文件分为下面几个模块
模块名称 | 内容类型 | 访问方法 | 所需权限 | 其他应用是否可以访问 | 卸载应用时候是否移除 |
---|---|---|---|---|---|
应用专属文件 | 仅供您的应用使用文件 | 内部存储空间 getFilesDir或getCacheDir 外部存储空间 getExternalFilesDir或者getExternalCacheDir | 内部存储空间不需要任何权限 如果应用在搭载Android4.4(API 19) 或更高版本设备运行,从外部存储空间访问不需要任何权限 | 不支持 | 是 |
媒体 | 可共享的媒体文件: 图片音频文件、视频 | MediaStore API | 在Android 11(API 30) 或者更高版本中访问其他应用文件需要 READ_EXTERNAL_STORAGE 在Android10(API 29) 中,访问其他应用的文件需要 READ_EXTERNAL_STORAGE 或 WRITE_EXTERNAL_STORAGE 在Android 9 或更低版本,访问所有文件均需要相关权限 | 是,共享目录可以访问 | 不支持 |
文档和其他文件 | 其他类型的可共享内容,包括已下载的文件 | 存储访问框架 | 无 | 是,可以通过系统文件选择器访问 | 不支持 |
应用偏好设置 | 键值对 | Jetpack Preferences库 | 无 | 不支持 | 是 |
数据库 | 结构化数据 | Room 持久性库 | 无 | 不支持 | 是 |
应用专属文件
不需要任何权限。直接使用文件路径,进行读写操作就行。这个比较简单 注意目录不要使用全路径方式,而是使用getFilesDir等API 就可以。
Android10的时候引入了作用域存储的概念。 在Android10 以前,外部存储属于公共空间,不计入应用程序占用的空间,所有应用可以申请权限进行随意访问。并且卸载了,创建的文件也保留下来。
Android 10 开始,对SD卡进行了限制,每个应用只有权限读取自己外置存储空间关联的目录,如下面的地址。该目录下文件记录到应用程序的空间,并且随应用卸载而删除。这些目录就是应用专属空间。
/storage/emulated/0/Android/data/<包名>/files
/storage/emulated/0/Android/data/<包名>/caches
媒体文件访问读写
权限需要:READ_EXTRERNAL_STORGE
读取媒体库文件
在在作用域存储中,我们只能使用MediaStore API获取,无法像之前使用绝对路径了 这里举例图片
fun queryMedia() {
val cursor = ContentResolverCompat.query(
this.contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, null,
null, null, null
)
cursor.use {
while (it.moveToNext()) {
//获取文件地址, 现在还能用,但是被标注废弃
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)
}
}
}
新增媒体库文件
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)
}
}
修改或者删除媒体库中文件
平时开发场景比较少见,但是也需要注意,在 targetSDK 在30的时候 在Android 11上运行 需要进行权限申请
1. MediaStore.createWriteRequest()
2. MediaStore.createDeleteRequest()
在Android 10上运行 如果targetSDK 是 29 的话 停用分区存储,并继续使用Android 9 执行这类操作 在Android 9或者更低版本运行
- 申请WRITE_EXTERNAL_STORAGE 权限。
- 使用MediaStore API 进行修改或者删除媒体文件
// 修改图片
// 构造 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 结果 row = $row")
//删除图片
var row = contentResolver.delete(uri!!, null, null)
文档以及其他文件
权限: 无需权限
SAF(存储访问框架)
Android 4.4 (API级别19) 引入了存储访问框架. SAFA 允许用户在其所有首选文档存储提供程序中浏览和打开文档、图片和其他文件. 通过易用的标准节目,用户在各个应用和提供程序中以一致的方式浏览文件和访问近期文件。
- 客户端应用:一般指的是第三方应用。
- 选取器(
Picker
):选取的系统UI
,Download
模块里特指DocumentsUI
。 - 文档提供程序:内容提供者,也就是常说的
ContentProvider
, 文档提供程序作为DocumentsProvider
类的子类实现。
工作流程如下
- 客户端向
Picker
请求直接与文件交互的权限
Intent.ACTION_OPEN_DOCUMENT
: 对提供内容有长期、持久性访问权限。Intent.ACTION_CREATE_DOCUEMNT
: 只对提供内容进行数据读取导入。Intent.ACTION_GET_CONTENT
: 创建新文档。
Picker
收到请求之后,将所有已注册的provider 中寻找符合要求的数据源,并向用户展示- 用户对内容选取之后,返回给客户端,然后进行增删改查
读取文档以及其他文件
var fileSelectedLauncher: ActivityResultLauncher<Intent>? = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
if (it.data?.data != null) {
//TODO 根据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")
// 可选参数为要打开的目录指定一个URI 应用程序创建文档之前的系统文件选择器。
//putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
}
activityResultLauncher.launch(intent)
result 返回一个uri, 然后我们使用contentResolver.openOutputStream, 进行文件写入
删除文件
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)
}
}
开发过程中注意问题
文件选择器返回之后为uri , 只能拿到输入流,但是接口需要filePath怎么办?
首先区分该文件是媒体文件,还是文档类型,
媒体文件直接读取MediaStore.MediaColumns.DATA
就是文件路径。
文档类型两个方法
- 把用户选择文件拷贝到应用专属目录(实际工作中自身采用)
- uri 转path 可以使用 Utils 类
转载自:https://juejin.cn/post/7360595729486544930