第3章 MediaStore通用文件读写:ContentResolver与Uri构建

本章核心:掌握使用 ContentResolver.insert() 创建 MediaStore 条目,通过 URI 打开 OutputStream 写入文件数据,完成分区存储下的通用文件读写操作。规避常见权限与路径问题。

3.1 原理概述

MediaStore 是 Android 系统提供的媒体数据仓库,统一管理图片、视频、音频等公共媒体文件。应用通过 ContentResolver 向 MediaStore 发起增删改查请求,系统负责在底层存储中创建实际文件并分配路径。应用不再直接操作原始文件路径,而是通过 Content URIs(如 content://media/external/images/media/123)进行读写。

MediaStore文件写入流程 App insert(uri, values) MediaStore Provider 创建文件条目 返回 content://.../123 openOutputStream(fileUri) write(buffer) & close() notifyChange(uri, null) 要点: 1) insert() 返回的 Uri 必须完整保存;2) 写入完成后务必调用 notifyChange() 通知系统更新; 3) 如果写入失败,检查 Uri 的 authority 是否为 “media” 以及是否拥有 MANAGE_MEDIA_* 权限。

3.2 实现流程

  1. 准备 ContentValues:设置文件名称、MIME类型、相对路径(或使用默认目录)等元数据。
  2. 调用 ContentResolver.insert():传入 MediaStore.Images/Video/Audio 的外部 URI,并传入 ContentValues,得到返回的新文件 URI。
  3. 打开 OutputStream:通过 context.contentResolver.openOutputStream(fileUri) 获取写入流。
  4. 写入二进制数据:将文件数据(如 Bitmap、字节数组)写入 OutputStream。
  5. 关闭流并通知系统:关闭 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_URIimage/jpeg, image/png
视频MediaStore.Video.Media.EXTERNAL_CONTENT_URIvideo/mp4, video/avi
音频MediaStore.Audio.Media.EXTERNAL_CONTENT_URIaudio/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、华为等主流机型。