Android升级compileSdk30后保存图片传URI跳转崩溃问题
问题根因
崩溃触发点是FilterCollageActivity.kt第53行的空指针异常,对应代码savedImageUri!!.toString(),核心原因是API版本适配缺失:
- compileSdk升级到30后,targetSDK对应Android 11(API 30)系统强制启用分区存储机制,原有直接操作外部存储公共目录的
saveBitmap实现执行失败,未完成savedImageUri的赋值,此时savedImageUri为null,使用!!强制转非空直接触发崩溃。 - 现有代码还存在两处高版本适配隐患:一是跨页面传递URI时先转字符串再解析的写法容易丢失格式和权限;二是
ShowImageActivity中直接把URI字符串转为File对象读取的逻辑,在分区存储模式下无对应文件路径的访问权限,哪怕解决了空指针也会触发权限错误。 - 降级到compileSdk 29可正常运行,是因为API 29允许通过
android:requestLegacyExternalStorage="true"配置关闭分区存储,旧的文件读写逻辑可以正常执行,savedImageUri能正常赋值。
适配API 30+的正确实现
- 重写
saveBitmap方法,通过MediaStoreAPI适配分区存储,保存图片到系统相册并正确返回Uri:
private fun saveBitmap(bitmap: Bitmap): Uri? { val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, "collage_${System.currentTimeMillis()}.jpg") put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_DCIM}/Collage/") put(MediaStore.Images.Media.IS_PENDING, 1) } val contentResolver = contentResolver val insertUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) ?: return null contentResolver.openOutputStream(insertUri)?.use { outputStream -> bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) } contentValues.clear() contentValues.put(MediaStore.Images.Media.IS_PENDING, 0) contentResolver.update(insertUri, contentValues, null, null) sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, insertUri)) return insertUri }
- 修正点击保存逻辑的空判断,移除强制非空调用,直接传递Uri对象而非字符串:
R.id.img_save -> { checkClick() isFromSaved = true val resultUri = try { saveBitmap(screenShot) } catch (e: Throwable) { e.printStackTrace() null } if (resultUri == null) { Toast.makeText(this, "图片保存失败,请重试", Toast.LENGTH_SHORT).show() return } savedImageUri = resultUri val intent = Intent(this, ShowImageActivity::class.java) intent.putExtra("image_uri", savedImageUri) startActivityForResult(intent, 2) finish() }
- 修正
ShowImageActivity的Uri读取逻辑,移除无效的File转换操作:
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) requestWindowFeature(Window.FEATURE_NO_TITLE) window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN) setContentView(R.layout.activity_show_image) // 兼容各版本从Intent取Parcelable类型的Uri val imageUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra("image_uri", Uri::class.java) } else { @Suppress("DEPRECATION") intent.getParcelableExtra<Uri>("image_uri") } if (imageUri == null) { finish() return } img_show = findViewById<View>(R.id.img_show) as ImageView display = resources.displayMetrics density = resources.displayMetrics.density D_width = display!!.widthPixels D_height = (display!!.heightPixels.toFloat() - density * 150.0f).toInt() val layoutParams = RelativeLayout.LayoutParams(D_width, ViewGroup.LayoutParams.WRAP_CONTENT) layoutParams.addRule(RelativeLayout.CENTER_IN_PARENT, RelativeLayout.TRUE) img_show!!.layoutParams = layoutParams // 直接使用Content Uri加载图片,不需要转File img_show!!.setImageURI(imageUri) // 删除原有 saved_file = File(image_uri!!) 逻辑,API30+无该路径的直接访问权限 }
适配注意事项
- 使用
MediaStore写入公共相册目录不需要申请WRITE_EXTERNAL_STORAGE权限,该权限在API 29及以上版本已经失效,不需要额外声明。 - 跨组件传递媒体Uri时优先直接传递
Uri序列化对象,不要转成字符串后再解析,避免出现Uri格式错误、临时权限丢失的问题。 - API 30+禁止通过文件路径直接访问外部存储的公共媒体文件,所有文件操作都需要通过
ContentResolver配合content://类型的Uri完成,否则会抛出安全异常。
内容的提问来源于stack exchange,提问作者Đức Nguyễn Văn
相关产品推荐
相关产品推荐

