Android 15 相机适配:拍照返回崩溃的根因与修复

作者:rousong2026.11.06 21:12浏览量:0

简介:Android 15 收紧了媒体存储对 `_data` 字段的写限制,导致相机拍照返回时抛出 `IllegalArgumentException`。本文从崩溃堆栈入手,剖析分区存储下的权限变更,给出文件路径校验、MediaStore 插入、FileProvider 三条修复路径,并附完整代码与验证清单,帮助开发者一次性完成适配。

Android 15 相机适配:拍照返回崩溃的根因与修复

一、问题现象:一次看似普通的回调崩溃

应用在升级到 Android 15(API 35)后,调用系统相机拍照并返回时进程直接崩溃,堆栈信息具有高度代表性:

  1. Process: com.example.cameraxdemo, PID: 17531
  2. java.lang.RuntimeException: Failure delivering result
  3. ResultInfo{who=null, request=102, result=-1, data=null} to activity
  4. {com.example.cameraxdemo/...CameraActivity}:
  5. java.lang.IllegalArgumentException: Mutation of _data is not allowed.
  6. at android.app.ActivityThread.deliverResults(ActivityThread.java:6371)
  7. at android.app.ActivityThread.handleSendResult(ActivityThread.java:6410)
  8. at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:69)
  9. at android.app.servertransaction.TransactionExecutor.execute(...)

关键信息有三点:

  1. result=-1 表示 Activity.RESULT_OK,说明相机侧拍摄成功;
  2. data=null 表示调用方使用的是带输出文件路径的 MediaStore.EXTRA_OUTPUT 模式;
  3. 崩溃点是 Mutation of _data is not allowed,发生在结果回传阶段,而非拍摄阶段。

这条堆栈的本质是:框架在把结果投递给 Activity 的过程中,尝试写入或修改了 MediaStore 的 _data 列,而 Android 15 已禁止这一操作。 因此崩溃与相机界面本身无关,问题出在调用方对输出目标的处理方式上。

二、根因分析:_data 为什么不再允许被修改

2.1 _data 字段的历史包袱

在早期 Android 版本中,MediaStore.Images.Media._data 是应用直接读写文件绝对路径的主要入口。开发者习惯先构造一个文件路径,再把该路径写入 _data,让媒体库“登记”这条记录。

但从 Android 10 引入分区存储(Scoped Storage)开始,这条路径就逐步被收紧:

版本 对 _data 的策略
Android 9 及以前 可自由读写,路径即文件实体
Android 10~12 直接写 _data 被限制,推荐 RELATIVE_PATH
Android 13~14 进一步限制跨应用媒体文件访问
Android 15 明确禁止对 _data 的变更性写入,违规直接抛异常

2.2 崩溃的真实触发链路

结合堆栈可以还原完整链路:

  1. 应用构造了一个目标 Uri(通常指向外部存储的某个文件);
  2. 通过 Intent.putExtra(MediaStore.EXTRA_OUTPUT, uri) 交给系统相机;
  3. 相机写入数据后,将结果回传给调用方 Activity;
  4. 结果分发过程中,系统尝试解析并规范化该 Uri,其中包含对 _data 列的写入动作;
  5. Android 15 的校验拦截该写入,抛出 IllegalArgumentException;
  6. 异常在 ActivityThread.deliverResults 中未被捕获,升级为 RuntimeException 导致进程崩溃。

结论:错误的输出 Uri 构造方式是根因,EXTRA_OUTPUT 只是把它暴露出来的通道。

三、修复思路:三条可选路径

路径 A:使用 MediaStore 插入生成 Uri(推荐)

不要自己拼接文件路径,而是先通过 ContentResolver 向 MediaStore 插入一条待写记录,拿到系统分配的 content:// Uri,再交给相机。

  1. fun createImageUri(): Uri? {
  2. val values = ContentValues().apply {
  3. put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
  4. put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
  5. // Android 10+ 使用相对路径,避免触碰 _data
  6. put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp")
  7. put(MediaStore.Images.Media.IS_PENDING, 1)
  8. }
  9. return context.contentResolver.insert(
  10. MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values
  11. )
  12. }

拍摄完成后记得清除 IS_PENDING:

  1. fun publishImage(uri: Uri) {
  2. val values = ContentValues().apply {
  3. put(MediaStore.Images.Media.IS_PENDING, 0)
  4. }
  5. context.contentResolver.update(uri, values, null, null)
  6. }

优势在于:Uri 由系统生成、归属明确,完全绕开 _data,兼容 Android 10 到 15 全版本。

路径 B:FileProvider 共享私有目录

若图片不需要进入公共媒体库,可写入应用私有目录,再通过 FileProvider 暴露安全的 content:// Uri 给相机:

  1. <provider
  2. android:name="androidx.core.content.FileProvider"
  3. android:authorities="${applicationId}.fileprovider"
  4. android:exported="false"
  5. android:grantUriPermissions="true">
  6. <meta-data
  7. android:name="android.support.FILE_PROVIDER_PATHS"
  8. android:resource="@xml/file_paths" />
  9. </provider>
  1. val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "temp.jpg")
  2. val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
  3. intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
  4. intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)

注意:必须显式授予写权限,否则相机侧无法写入,表现为拍完后文件为空。

路径 C:迁移到 CameraX 输出管线

如果项目本就使用 CameraX,最彻底的做法是不再走系统相机的 Intent 链路,改用 ImageCapture 直接输出到 OutputFileOptions:

  1. val outputOptions = ImageCapture.OutputFileOptions.Builder(
  2. context.contentResolver,
  3. MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
  4. contentValues
  5. ).build()
  6. imageCapture.takePicture(
  7. outputOptions,
  8. ContextCompat.getMainExecutor(context),
  9. object : ImageCapture.OnImageSavedCallback {
  10. override fun onImageSaved(output: ImageCapture.OutputFileResults) { /* 处理 */ }
  11. override fun onError(exc: ImageCaptureException) { /* 处理 */ }
  12. }
  13. )

这条路径把拍摄与存储都收敛到应用内,不再依赖外部 Activity 的结果回传,从根源上规避了 deliverResults 阶段的校验问题。

四、兼容性注意事项

  1. 不要假设 data 不为空。在 EXTRA_OUTPUT 模式下 data 通常为 null,需从预设 Uri 读取图片。
  2. 手动构造 file:// Uri 已不可靠。Android 7 起 FileUriExposedException 就会拦截,Android 15 则进一步切断了经由 _data 的兜底通道。
  3. 区分 IS_PENDING 生命周期。未清除 IS_PENDING 的记录对其他应用不可见,容易造成“图片已拍成功但相册看不到”的错觉。
  4. 处理用户取消。resultCode != RESULT_OK 时应清理已插入的空记录,避免媒体库残留脏数据。
  5. 权限声明要匹配目标版本。Android 13+ 使用细分媒体权限,Android 14+ 涉及部分照片授权,需在请求逻辑中做版本分支。
  6. 异常兜底。即便完成适配,也建议在 onActivityResult / ActivityResultLauncher 回调中包裹 try-catch,防止个别机型的定制行为引发二次崩溃。

五、验证清单

场景 预期结果
Android 15 拍照后返回 无崩溃,图片可读取
Android 10~14 拍照后返回 行为一致,无回归
用户中途取消拍摄 无残留空记录,无异常
连续多次拍摄 Uri 不冲突,文件不覆盖
首次安装未授权媒体权限 权限流程正常,不崩溃

六、小结

Mutation of _data is not allowed 并非 Android 15 的“新 bug”,而是分区存储演进到一定阶段的必然结果。它把过去依靠路径字符串“蒙混过关”的写法彻底堵死,倒逼开发者使用系统分配的 content:// Uri 来完成媒体文件的创建与共享。

实践中最稳妥的策略是:能走 MediaStore 就不拼路径,能用 CameraX 就不开外部 Activity,必须共享文件就用 FileProvider 并显式授权。 按此原则改造后,不仅 Android 15 的崩溃会消失,整体存储访问逻辑也会更符合现代 Android 的权限模型,后续版本升级的迁移成本将显著降低。