You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 22:48:18