第3章 MediaStore通用文件读写:ContentResolver与Uri构建
本章核心:掌握使用
ContentResolver.insert() 创建 MediaStore 条目,通过 URI 打开 OutputStream 写入文件数据,完成分区存储下的通用文件读写操作。规避常见权限与路径问题。
3.1 原理概述
MediaStore 是 Android 系统提供的媒体数据仓库,统一管理图片、视频、音频等公共媒体文件。应用通过 ContentResolver 向 MediaStore 发起增删改查请求,系统负责在底层存储中创建实际文件并分配路径。应用不再直接操作原始文件路径,而是通过 Content URIs(如 content://media/external/images/media/123)进行读写。
3.2 实现流程
- 准备 ContentValues:设置文件名称、MIME类型、相对路径(或使用默认目录)等元数据。
- 调用 ContentResolver.insert():传入 MediaStore.Images/Video/Audio 的外部 URI,并传入 ContentValues,得到返回的新文件 URI。
- 打开 OutputStream:通过
context.contentResolver.openOutputStream(fileUri)获取写入流。 - 写入二进制数据:将文件数据(如 Bitmap、字节数组)写入 OutputStream。
- 关闭流并通知系统:关闭 OutputStream 后,调用
context.contentResolver.notifyChange(fileUri, null)让 MediaScanner 扫描更新。
提示:调用
insert() 后返回的 Uri 可能随 Android 版本不同而格式略有差异(例如附带 is_pending 标志)。始终使用返回的完整 Uri 进行后续操作,不要自行拼接。
3.3 核心 API 与代码示例
3.3.1 写入图片文件
// 1. 创建 ContentValues
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo_" + System.currentTimeMillis() + ".jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp");
// 2. 插入记录,获取 URI
Uri externalUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Uri fileUri = context.getContentResolver().insert(externalUri, values);
// 3. 打开 OutputStream 并写入
try (OutputStream out = context.getContentResolver().openOutputStream(fileUri)) {
Bitmap bitmap = ...; // 你的图片数据
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, out);
out.flush();
} catch (IOException e) {
// 处理写入失败
}
// 4. 通知系统更新
context.getContentResolver().notifyChange(fileUri, null);
3.3.2 写入视频文件
ContentValues values = new ContentValues();
values.put(MediaStore.Video.Media.DISPLAY_NAME, "video_" + System.currentTimeMillis() + ".mp4");
values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4");
values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES + "/MyApp");
Uri videoUri = context.getContentResolver().insert(
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
try (OutputStream out = context.getContentResolver().openOutputStream(videoUri)) {
// 写入视频文件字节数据
byte[] videoData = ...; // 来自网络或本地文件
out.write(videoData);
} catch (IOException e) {
// 处理异常
}
context.getContentResolver().notifyChange(videoUri, null);
3.3.3 写入音频文件
ContentValues values = new ContentValues();
values.put(MediaStore.Audio.Media.DISPLAY_NAME, "record_" + System.currentTimeMillis() + ".ogg");
values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/ogg");
values.put(MediaStore.Audio.Media.RELATIVE_PATH, Environment.DIRECTORY_MUSIC + "/MyApp");
Uri audioUri = context.getContentResolver().insert(
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, values);
// 后续与上述类似
3.4 常用 MediaStore URI 与 MIME 类型对照
| 媒体类型 | 外部 Collection URI | 示例 MIME Type |
|---|---|---|
| 图片 | MediaStore.Images.Media.EXTERNAL_CONTENT_URI | image/jpeg, image/png |
| 视频 | MediaStore.Video.Media.EXTERNAL_CONTENT_URI | video/mp4, video/avi |
| 音频 | MediaStore.Audio.Media.EXTERNAL_CONTENT_URI | audio/mpeg, audio/ogg |
| 下载文件 | MediaStore.Downloads.EXTERNAL_CONTENT_URI(API 29+) | application/pdf, */* |
3.5 调试与验证
- 日志检查:在
insert()后打印返回的 Uri,检查是否以content://media/external/images/media/开头。 - 权限确认:Android 10 及以上,仅需
WRITE_EXTERNAL_STORAGE已弃用,但依然需要用户授予READ_MEDIA_IMAGES/READ_MEDIA_VIDEO(若需读取已保存文件)。写入只需在 manifest 中声明WRITE_EXTERNAL_STORAGE并动态申请(Android 10+ 实际不再检查,但为兼容可保留)。 - 文件查看:使用系统文件管理器(如 Files by Google)查看对应的
Pictures/MyApp目录是否正确生成。
常见错误:
- Uri 为 null:通常因为
RELATIVE_PATH设置无效或 MediaStore 条目已满。检查路径是否在Environment.DIRECTORY_PICTURES等标准目录下,且不要包含非法字符。 - SecurityException:当尝试打开
openOutputStream()时抛出,原因可能是 Uri 对应的文件已被其他进程修改,或应用没有WRITE_EXTERNAL_STORAGE权限(Android 10 以下)。 - 文件写入不完整:未正确处理
flush()或未在 finally 块关闭流。建议使用 try-with-resources。 - 通知缺失导致图库不刷新:写入后必须调用
notifyChange(),否则用户可能无法立即在相册中看到文件。
3.6 性能与稳定性优化
- 批量写入:如需写入多个文件,使用
ContentProviderOperation批量插入,减少跨进程通信次数。 - 避免重复通知:批量写入完成后,对每个 Uri 调用一次
notifyChange()即可,不要在每个文件写入时都通知。 - 大文件分块写入:使用固定大小的缓冲区(如 8KB~64KB)循环写入,避免一次性加载整个文件到内存。
- 使用 Pending URIs:Android 10 开始支持
IS_PENDING标志,插入时设置values.put(MediaStore.Images.Media.IS_PENDING, 1),写入完成后设为 0 并移除此标志。这可以防止其他应用在写入过程中访问未完成的文件。
最佳实践:始终在后台线程(如 Coroutine/AsyncTask)中执行 ContentResolver 操作,避免阻塞主线程。优先使用
IS_PENDING 标志以保证写入完整性。
3.7 部署注意事项
- Android 10(API 29):分区存储强制生效,必须使用 MediaStore 写入公共目录,无法直接使用文件路径。
- Android 11(API 30):新增
MANAGE_EXTERNAL_STORAGE权限,但针对文件管理器等特殊应用;普通应用仍使用 MediaStore 即可。 - Android 12+:进一步收紧对相册访问的限制,但 MediaStore 写入方式不变。
- 不同设备 ROM:部分厂商定制系统可能对
RELATIVE_PATH支持不完整,建议测试时覆盖 vivo、OPPO、华为等主流机型。