简介:Android 15 收紧了媒体存储对 `_data` 字段的写限制,导致相机拍照返回时抛出 `IllegalArgumentException`。本文从崩溃堆栈入手,剖析分区存储下的权限变更,给出文件路径校验、MediaStore 插入、FileProvider 三条修复路径,并附完整代码与验证清单,帮助开发者一次性完成适配。
应用在升级到 Android 15(API 35)后,调用系统相机拍照并返回时进程直接崩溃,堆栈信息具有高度代表性:
Process: com.example.cameraxdemo, PID: 17531java.lang.RuntimeException: Failure delivering resultResultInfo{who=null, request=102, result=-1, data=null} to activity{com.example.cameraxdemo/...CameraActivity}:java.lang.IllegalArgumentException: Mutation of _data is not allowed.at android.app.ActivityThread.deliverResults(ActivityThread.java:6371)at android.app.ActivityThread.handleSendResult(ActivityThread.java:6410)at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:69)at android.app.servertransaction.TransactionExecutor.execute(...)
关键信息有三点:
result=-1 表示 Activity.RESULT_OK,说明相机侧拍摄成功;data=null 表示调用方使用的是带输出文件路径的 MediaStore.EXTRA_OUTPUT 模式;Mutation of _data is not allowed,发生在结果回传阶段,而非拍摄阶段。这条堆栈的本质是:框架在把结果投递给 Activity 的过程中,尝试写入或修改了 MediaStore 的 _data 列,而 Android 15 已禁止这一操作。 因此崩溃与相机界面本身无关,问题出在调用方对输出目标的处理方式上。
_data 为什么不再允许被修改_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 的变更性写入,违规直接抛异常 |
结合堆栈可以还原完整链路:
Uri(通常指向外部存储的某个文件);Intent.putExtra(MediaStore.EXTRA_OUTPUT, uri) 交给系统相机;Uri,其中包含对 _data 列的写入动作;IllegalArgumentException;ActivityThread.deliverResults 中未被捕获,升级为 RuntimeException 导致进程崩溃。结论:错误的输出 Uri 构造方式是根因,EXTRA_OUTPUT 只是把它暴露出来的通道。
不要自己拼接文件路径,而是先通过 ContentResolver 向 MediaStore 插入一条待写记录,拿到系统分配的 content:// Uri,再交给相机。
fun createImageUri(): Uri? {val values = ContentValues().apply {put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")// Android 10+ 使用相对路径,避免触碰 _dataput(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyApp")put(MediaStore.Images.Media.IS_PENDING, 1)}return context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)}
拍摄完成后记得清除 IS_PENDING:
fun publishImage(uri: Uri) {val values = ContentValues().apply {put(MediaStore.Images.Media.IS_PENDING, 0)}context.contentResolver.update(uri, values, null, null)}
优势在于:Uri 由系统生成、归属明确,完全绕开 _data,兼容 Android 10 到 15 全版本。
若图片不需要进入公共媒体库,可写入应用私有目录,再通过 FileProvider 暴露安全的 content:// Uri 给相机:
<providerandroid:name="androidx.core.content.FileProvider"android:authorities="${applicationId}.fileprovider"android:exported="false"android:grantUriPermissions="true"><meta-dataandroid:name="android.support.FILE_PROVIDER_PATHS"android:resource="@xml/file_paths" /></provider>
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "temp.jpg")val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
注意:必须显式授予写权限,否则相机侧无法写入,表现为拍完后文件为空。
如果项目本就使用 CameraX,最彻底的做法是不再走系统相机的 Intent 链路,改用 ImageCapture 直接输出到 OutputFileOptions:
val outputOptions = ImageCapture.OutputFileOptions.Builder(context.contentResolver,MediaStore.Images.Media.EXTERNAL_CONTENT_URI,contentValues).build()imageCapture.takePicture(outputOptions,ContextCompat.getMainExecutor(context),object : ImageCapture.OnImageSavedCallback {override fun onImageSaved(output: ImageCapture.OutputFileResults) { /* 处理 */ }override fun onError(exc: ImageCaptureException) { /* 处理 */ }})
这条路径把拍摄与存储都收敛到应用内,不再依赖外部 Activity 的结果回传,从根源上规避了 deliverResults 阶段的校验问题。
data 不为空。在 EXTRA_OUTPUT 模式下 data 通常为 null,需从预设 Uri 读取图片。file:// Uri 已不可靠。Android 7 起 FileUriExposedException 就会拦截,Android 15 则进一步切断了经由 _data 的兜底通道。IS_PENDING 生命周期。未清除 IS_PENDING 的记录对其他应用不可见,容易造成“图片已拍成功但相册看不到”的错觉。resultCode != RESULT_OK 时应清理已插入的空记录,避免媒体库残留脏数据。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 的权限模型,后续版本升级的迁移成本将显著降低。