MediaStore 使用完全解析
MediaStore 是 Android 系统中用于管理多媒体内容的核心组件,它通过 ContentProvider 的方式让开发者可以安全地访问共享媒体库中的音频、视频和图像等资源。本篇会详细介绍其使用方法和工作原理。
1、MediaStore 基础概念
MediaStore 的几个核心概念:媒体库索引 (Index)、主要集合 (Collections)、内容 URI (Content URIs)。
1.1、主要组成部分
在 Android 10 (API 29) 及之后,系统推行分区存储 (Scoped Storage)。MediaStore 将文件分为几个主要的逻辑表格:
①Images:照片和截图。
②Video:视频文件。
③Audio:音乐、录音和播客。
④Downloads:下载的文件(Android 10+)。
⑤Files:所有媒体类型(通常用于搜索)。
开发者不再使用 /sdcard/photo.jpg 这种文件路径,而是使用Content Uri:
Images MediaStore.Images.Media.EXTERNAL_CONTENT_URI 管理图片(PNG/JPEG/WEBP等)
Video MediaStore.Video.Media.EXTERNAL_CONTENT_URI 管理视频(MP4/AVI/MOV等)
Audio MediaStore.Audio.Media.EXTERNAL_CONTENT_URI 管理音频(MP3/AAC/WAV等)
1.2、核心数据列
MediaStore 并不是文件的实际存储路径,而是一个媒体索引表。它会扫描存储设备,将媒体文件的元数据(如文件名、拍摄日期、GPS 信息、时长等)存入数据库。
常用字段(以图片为例):
MediaStore.Images.ImageColumns._ID // 唯一ID
MediaStore.Images.ImageColumns.DISPLAY_NAME // 文件名
MediaStore.Images.ImageColumns.MIME_TYPE // MIME类型
MediaStore.Images.ImageColumns.SIZE // 文件大小(bytes)
MediaStore.Images.ImageColumns.DATE_ADDED // 添加时间戳(s)
MediaStore.Images.ImageColumns.RELATIVE_PATH // 相对路径(API 29+)
优势:开发者无需扫描整个磁盘,只需查询数据库即可快速获取所有照片或视频。
2、查询媒体文件
查询 (Query)是最常用的操作之一。
2.1. 基础查询示例
使用 ContentResolver.query() 配合过滤条件(如 DATE_ADDED)获取媒体列表。
fun queryImages(context: Context): List<ImageItem> {
val images = mutableListOf<ImageItem>()
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_MODIFIED
)
val sortOrder = "${MediaStore.Images.Media.DATE_MODIFIED} DESC"
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
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))
val date = Date(TimeUnit.SECONDS.toMillis(cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_MODIFIED))))
images.add(ImageItem(id, name, date))
}
}
return images
}
2.2、条件筛选查询
添加查询条件,比如查询最近30天的JPG图片:
// 查询最近30天的JPG图片
val selection = "${MediaStore.Images.Media.MIME_TYPE} = ? AND " +
"${MediaStore.Images.Media.DATE_ADDED} >= ?"
val args = arrayOf(
"image/jpeg",
TimeUnit.MILLISECONDS.toSeconds(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(30)).toString()
)
context.contentResolver.query(
uri,
projection,
selection,
args,
sortOrder
) Android获取照片推荐使用照片选择器 (Photo Picker) ,它比直接操作 MediaStore 更安全、更简单。
3、插入/更新媒体文件
除了查询 (Query),常用操作还有:插入 (Insert)、更新/删除 (Update/Delete)。
3.1、写入新文件(兼容Android 10+)
插入 (Insert):通过 ContentResolver.insert() 将新拍摄的照片或下载的音频存入系统库。
fun saveImageToGallery(context: Context, bitmap: Bitmap): Uri? {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.IS_PENDING, 1) // 标记为待处理状态
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp")
}
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
values
) ?: return null
return try {
context.contentResolver.openOutputStream(uri)?.use { os ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, os)
}
// 完成写入后清除待处理标志
values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
context.contentResolver.update(uri, values, null, null)
uri
} catch (e: Exception) {
context.contentResolver.delete(uri, null, null)
null
}
}
3.2、更新元数据
更新/删除 (Update/Delete):修改文件元数据或移除索引(在 Android 10+ 中删除非应用自身创建的文件需用户授权)。
fun renameMediaItem(context: Context, uri: Uri, newName: String): Boolean {
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "$newName.jpg")
}
return context.contentResolver.update(uri, values, null, null) > 0
}
3.3、删除媒体文件
单个文件删除
fun deleteMediaItem(context: Context, uri: Uri): Boolean {
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// 处理作用域存储限制
false
}
} 批量删除
fun deleteMultipleItems(context: Context, uris: List<Uri>) {
val ops = ArrayList<ContentProviderOperation>().apply {
for (uri in uris) {
add(ContentProviderOperation.newDelete(uri).build())
}
}
try {
context.contentResolver.applyBatch(MediaStore.AUTHORITY, ops)
} catch (e: OperationApplicationException) {
e.printStackTrace()
}
}
4、常见问题及技巧
4.1、文件找不到的问题
通过ID重新构建有效URI
// 通过ID重新构建有效URI
fun getValidUri(context: Context, id: Long): Uri {
return ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)
}
4.2、缓存优化策略
通过Glide直接加载MediaStore图片Uri:
// Glide加载MediaStore图片示例
Glide.with(context)
.load(mediaStoreUri)
.diskCacheStrategy(DiskCacheStrategy.NONE) // MediaStore自带缓存机制
.skipMemoryCache(true)
.into(imageView)
4.3、大数据集分页查询
使用分页加载查询
val limit = 50
val offset = pageNumber * limit
val uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI.buildUpon()
.appendQueryParameter("limit", "$offset,$limit")
.build() 后台操作使用CursorLoader:
public class MediaLoader extends CursorLoader {
public MediaLoader(Context context) {
super(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
PROJECTION, SELECTION, ARGS, SORT_ORDER);
}
}
4.4、定期维护媒体库
手动触发媒体扫描更新媒体库:
// 手动触发媒体扫描
MediaScannerConnection.scanFile(
context,
arrayOf(filePath),
arrayOf(mimeType),
null
)
4.5、兼容性处理技巧
老项目兼容好:旧方法 (<= Android 9) 、新方法 (>= Android 10)
fun isScopedStorageEnabled(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
!Environment.isExternalStorageLegacy()
}
fun getBestStorageMethod(context: Context) {
if (isScopedStorageEnabled()) {
// 使用MediaStore API
} else {
// 使用传统文件API
}
}
应当总是使用ContentResolver进行操作文件,避免直接使用文件路径。可以更高效地在各种Android版本上处理媒体文件,同时保证应用的稳定性和兼容性。