MediaStore Provider:访问媒体文件、查询图片/视频/音频、文件操作
说到 Android 里的媒体文件访问,MediaStore 绝对是绕不开的核心组件。我记得刚做 Android 开发那会儿,很多人还在用 File 路径直接操作媒体文件,结果 Android 10 一出,全崩了。嗯,那时候我正好在做一个相册 App,差点没被兼容性问题搞疯。
MediaStore Provider,说白了就是系统提供的一个统一媒体数据库。它管理着设备上所有的图片、视频、音频文件。你不需要关心文件存在哪个目录,只需要通过 ContentResolver 去查它就行。
MediaStore 的核心表结构
MediaStore 内部其实分了几张表,每张表对应一类媒体文件。我习惯把它们理解成三个大仓库:
| 表名 | 对应媒体类型 | 常用 URI |
|---|---|---|
| MediaStore.Images | 图片文件 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI |
| MediaStore.Video | 视频文件 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI |
| MediaStore.Audio | 音频文件 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI |
| MediaStore.Files | 所有文件(包括非媒体) | MediaStore.Files.getContentUri("external") |
你想想看,这三张表的结构其实非常相似。它们都包含 _ID、DATA、TITLE、MIME_TYPE、SIZE、DATE_ADDED 这些通用字段。但每张表也有自己特有的字段,比如 Images 有 ORIENTATION,Video 有 DURATION,Audio 有 ARTIST 和 ALBUM。
重要提醒:从 Android 10(API 29)开始,DATA 字段已经被废弃。你不能直接通过它拿到文件路径了。这是 Google 强制推行分区存储的手段。
查询媒体文件:基础操作
查询 MediaStore 其实跟查普通数据库没什么两样。你只需要构建一个 Cursor,然后遍历结果就行。我个人习惯把查询封装成一个工具方法,这样到处复用。
// 查询所有图片
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.SIZE,
MediaStore.Images.Media.MIME_TYPE
)
val selection = "${MediaStore.Images.Media.MIME_TYPE} = ?"
val selectionArgs = arrayOf("image/jpeg")
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"
contentResolver.query(
uri,
projection,
selection,
selectionArgs,
sortOrder
)?.use { cursor ->
while (cursor.moveToNext()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
val name = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
// 处理数据...
}
}
这里有个坑,我曾经踩过。如果你不传 selection 参数,系统会返回所有图片。但如果你传了 null 的 selectionArgs,在某些厂商的 ROM 上会直接崩溃。所以我的习惯是:要么不传 selection,要么就老老实实把 selectionArgs 也传对。
获取缩略图:性能优化关键
做图片列表的时候,千万别直接加载原图。你想想看,一张 4000 万像素的照片直接加载到内存,App 不崩才怪。MediaStore 提供了缩略图机制,这个一定要用。
// 获取图片缩略图
val thumbnail = contentResolver.loadThumbnail(
uri,
Size(512, 512),
null
)
// 或者用旧版 API(兼容老设备)
val bitmap = MediaStore.Images.Thumbnails.getThumbnail(
contentResolver,
imageId,
MediaStore.Images.Thumbnails.MINI_KIND,
null
)
我个人更推荐用 loadThumbnail() 方法,它是 Android 10 引入的,性能更好。但如果你要兼容 Android 9 以下的设备,还是得用旧版 API。嗯,这就是做 Android 开发的常态——永远在跟碎片化斗争。
文件操作:增删改
MediaStore 不只是能查,还能写。但写操作比查要复杂一些,因为涉及到权限和分区存储的限制。
插入新文件
// 插入一张新图片到 MediaStore
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "my_photo.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val uri = contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
)
// 然后通过 uri 打开 OutputStream 写入数据
uri?.let {
contentResolver.openOutputStream(it)?.use { outputStream ->
// 将 bitmap 或字节数据写入
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
}
}
这里要注意 RELATIVE_PATH 字段。从 Android 10 开始,你不能指定绝对路径了,只能指定相对路径。系统会自动把文件放到对应的公共目录下。
更新文件信息
// 修改文件名称
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "renamed_photo.jpg")
}
contentResolver.update(
uri,
values,
null,
null
)
删除文件
// 删除文件
contentResolver.delete(
uri,
"${MediaStore.Images.Media._ID} = ?",
arrayOf(imageId.toString())
)
警告:删除操作需要 DELETE 权限。而且从 Android 10 开始,即使用户授予了权限,你也不能删除其他应用创建的文件。这是分区存储的硬性限制。
分区存储适配:避坑指南
我曾经在一个项目中,因为没处理好分区存储的适配,导致用户升级到 Android 11 后,App 完全读不到媒体文件。那段时间真是焦头烂额。
这里我总结几个关键点:
- Android 10(API 29):分区存储默认开启,但可以通过
android:requestLegacyExternalStorage="true"临时关闭 - Android 11(API 30):分区存储强制开启,
requestLegacyExternalStorage失效 - Android 13(API 33):新增细粒度媒体权限,不再需要
READ_EXTERNAL_STORAGE
说白了,如果你现在还在用 File 路径直接操作媒体文件,赶紧改吧。Android 11 以上的设备已经走不通了。
SVG 流程图:MediaStore 访问流程
实战建议
最后,我分享几个在实际项目中的经验:
- 批量查询用 Loader 或 Paging 3:不要在主线程查大量数据,Cursor 操作很重
- 监听媒体变化:注册 ContentObserver 可以实时感知文件变化
- 权限请求要优雅:Android 13 以上用细粒度权限,以下用 READ_EXTERNAL_STORAGE
- 测试要覆盖多厂商:华为、小米、三星的 MediaStore 实现有差异,我吃过这个亏
小技巧:如果你只需要读取自己应用创建的文件,可以用 MediaStore.setIncludePending() 来包含那些还没完全写入的文件。这在做断点续传时特别有用。
MediaStore Provider 其实没那么复杂,核心就是理解它是一张数据库表,然后用 ContentResolver 去操作。但适配不同 Android 版本确实是个体力活。嗯,慢慢来,踩过的坑多了,自然就熟了。