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

Android应用使用内部存储ImageUri显示图片时崩溃问题求助

问题:编辑街头艺术追踪应用条目时未更换图片导致崩溃

我正在开发一款Android应用,用于追踪城市中的街头艺术作品。核心功能如下:

  • Main Activity通过RecyclerView展示ArtPiece对象的图片和文字信息,提供添加按钮创建新条目
  • 点击添加按钮打开AddActivity,用户可输入文字、通过文件选择器选取图片(Uri以字符串形式存储),保存后条目正常显示在RecyclerView中
  • 点击RecyclerView条目进入编辑模式的AddActivity,可修改文字和图片:选择新图片则正常保存,若未更换图片直接保存,应用会崩溃

异常信息

java.lang.SecurityException: Permission Denial: opening provider com.android.providers.media.MediaDocumentsProvider from ProcessRecord{daaa4a9 9467:com.example.graffititracker/u0a186} (pid=9467, uid=10186) requires that you obtain access using ACTION_OPEN_DOCUMENT or related APIs

相关代码

AddActivity代码

class AddActivity : AppCompatActivity() {
    private lateinit var binding: ActivityAddBinding

    private var selectedImageUri: Uri? = null

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityAddBinding.inflate(layoutInflater)
            setContentView(binding.root)

            val mode = intent.getStringExtra("MODE")
            val position = intent.getIntExtra("position", -1)
            var piece = ArtPiece("", Date(1,1,2020), "", "")

            // 如果是编辑模式,加载已有数据到输入框
            if(mode == "EDIT"){
                piece = app.myCollection.getPiece(position)

                binding.txtName.setText(piece.name)
                binding.btnAddPhoto.setText(R.string.btn_ChangePhoto)
            }

            binding.btnSave.setOnClickListener(object: View.OnClickListener{
                override fun onClick(v: View?) {
                    if(binding.txtName.text.isNotEmpty()){
                        val data = Intent()
                        data.putExtra("name", binding.txtName.text.toString())

                        // 编辑模式下,优先使用新选的图片Uri,否则沿用旧Uri
                        if(mode == "EDIT"){
                            data.putExtra("position", position)

                            if(selectedImageUri != null)
                                data.putExtra("imageUri", selectedImageUri?.toString())
                            else
                                data.putExtra("imageUri", piece.image)
                        }
                        else{
                            if(selectedImageUri != null)
                                data.putExtra("imageUri", selectedImageUri?.toString())
                            else
                                data.putExtra("imageUri", "")
                        }

                        setResult(RESULT_OK, data)
                        finish()
                    }
                    else{
                        Toast.makeText(this@AddActivity, "请填写所有字段!", Toast.LENGTH_SHORT).show()
                    }
                }
            })

            // 选择图片按钮点击事件
            binding.btnAddPhoto.setOnClickListener(object: View.OnClickListener{
                override fun onClick(v: View?) {
                    // 用ACTION_OPEN_DOCUMENT而非ACTION_PICK避免权限问题(之前用ACTION_PICK编辑时会崩溃)
                    val galleryIntent = Intent(Intent.ACTION_OPEN_DOCUMENT, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
                    startActivityForResult(galleryIntent, 1)
                }
            })
        }

        override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
            super.onActivityResult(requestCode, resultCode, data)
            if(requestCode == 1 && resultCode == RESULT_OK){
                data?.data?.let{
                    selectedImageUri = it
                }
            }
        }

    }

RecyclerView适配器代码

override fun onBindViewHolder(holder: RecyclerViewAdapter.MyViewHolder, position: Int) {
        val piece = app.myCollection.getPiece(position)

        if(piece.image != "")
            holder.imageView.setImageURI(Uri.parse(piece.image))
        else
            holder.imageView.setImageResource(R.drawable.resource_default)

        holder.tvName.text = piece.name
        holder.tvDate.text = piece.date.toString()

        holder.itemView.setOnClickListener{
            listener.onItemClick(piece)
        }

        holder.itemView.setOnLongClickListener {
            longClickListener.onItemLongClick(piece)
            true
        }
    }

Manifest权限配置

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
解决方案

问题根源:使用ACTION_OPEN_DOCUMENT获取的Uri仅授予应用临时访问权限,当应用重启或再次复用该Uri时,权限已失效。编辑模式下未更换图片时,直接复用旧Uri会触发权限拒绝异常。

解决步骤:

  1. 持久化Uri访问权限
    在onActivityResult获取图片Uri后,调用takePersistableUriPermission获取持久访问权限,确保后续可正常访问该Uri:

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if(requestCode == 1 && resultCode == RESULT_OK){
            data?.data?.let{ uri ->
                selectedImageUri = uri
                // 获取读写持久权限
                val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                contentResolver.takePersistableUriPermission(uri, takeFlags)
            }
        }
    }
    
  2. 编辑模式初始化时验证权限
    加载已有条目时,检查旧Uri的权限是否有效,若失效则提示用户重新选择图片:

    if(mode == "EDIT"){
        piece = app.myCollection.getPiece(position)
        binding.txtName.setText(piece.name)
        binding.btnAddPhoto.setText(R.string.btn_ChangePhoto)
        
        // 验证已有Uri的访问权限
        val uri = Uri.parse(piece.image)
        try {
            contentResolver.openInputStream(uri)?.close()
        } catch (e: SecurityException) {
            // 权限失效,清空Uri并提示用户
            piece.image = ""
            Toast.makeText(this, "图片权限已失效,请重新选择", Toast.LENGTH_SHORT).show()
        }
    }
    
  3. 最优方案:将图片复制到应用私有目录
    直接存储文件路径而非系统Uri,彻底绕过权限问题。可添加图片复制方法:

    private fun copyImageToPrivateDir(uri: Uri): String? {
        try {
            val inputStream = contentResolver.openInputStream(uri)
            val fileName = "art_${System.currentTimeMillis()}.jpg"
            val outputFile = File(filesDir, fileName)
            inputStream?.use { input ->
                outputFile.outputStream().use { output ->
                    input.copyTo(output)
                }
            }
            return outputFile.absolutePath
        } catch (e: IOException) {
            e.printStackTrace()
            return null
        }
    }
    

    使用时,选择图片后调用此方法存储文件路径,适配器中直接加载文件:

    // 选择图片后处理
    val filePath = copyImageToPrivateDir(uri)
    selectedImagePath = filePath
    
    // 适配器中展示图片
    if(piece.imagePath != ""){
        holder.imageView.setImageBitmap(BitmapFactory.decodeFile(piece.imagePath))
    } else {
        holder.imageView.setImageResource(R.drawable.resource_default)
    }
    

内容的提问来源于stack exchange,提问作者pralnistroj

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 14:35:00