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")

你想想看,这三张表的结构其实非常相似。它们都包含 _IDDATATITLEMIME_TYPESIZEDATE_ADDED 这些通用字段。但每张表也有自己特有的字段,比如 Images 有 ORIENTATION,Video 有 DURATION,Audio 有 ARTISTALBUM

重要提醒:从 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 访问流程

MediaStore 媒体文件访问流程 你的 App ContentResolver MediaStore ContentProvider Images 表 Video 表 Audio 表 存储设备 存储设备 存储设备 应用层 API 层 Provider 层 数据表 物理存储

实战建议

最后,我分享几个在实际项目中的经验:

  • 批量查询用 Loader 或 Paging 3:不要在主线程查大量数据,Cursor 操作很重
  • 监听媒体变化:注册 ContentObserver 可以实时感知文件变化
  • 权限请求要优雅:Android 13 以上用细粒度权限,以下用 READ_EXTERNAL_STORAGE
  • 测试要覆盖多厂商:华为、小米、三星的 MediaStore 实现有差异,我吃过这个亏

小技巧:如果你只需要读取自己应用创建的文件,可以用 MediaStore.setIncludePending() 来包含那些还没完全写入的文件。这在做断点续传时特别有用。

MediaStore Provider 其实没那么复杂,核心就是理解它是一张数据库表,然后用 ContentResolver 去操作。但适配不同 Android 版本确实是个体力活。嗯,慢慢来,踩过的坑多了,自然就熟了。

公众号:蓝海资料掘金营,微信deep3321