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

ImageView.setImageURI()从SharedPreferences加载图片URI遇权限错误无法显示

解决自定义PreferenceFragment初始化时的权限问题

这问题我之前帮不少开发者排查过,核心就是页面重启/初始化时,你之前持久化的图片URI失去了访问权限——Android的权限机制对持久化URI有严格的验证逻辑,尤其是Scoped Storage(Android 10+)生效后,直接保存URI字符串是不够的,必须在用户选择图片的时机主动申请持久化权限。

下面是一步步的解决方案:

1. 获取图片时立即申请持久化权限

当用户通过Intent选择图片返回后,必须在这次会话里调用ContentResolver.takePersistableUriPermission,把临时权限转为持久权限。比如用Activity Result API的写法:

// 在Fragment里注册图片选择器
private val pickImageLauncher = registerForActivityResult(GetContent()) { uri ->
    uri ?: return@registerForActivityResult

    // 申请读权限的持久化标记
    val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    requireContext().contentResolver.takePersistableUriPermission(uri, takeFlags)

    // 保存URI到偏好设置
    PreferenceManager.getDefaultSharedPreferences(requireContext()).edit()
        .putString("selected_image_uri", uri.toString())
        .apply()

    // 更新Preference的显示(缩略图+文件名)
    updatePreferenceDisplay(uri)
}

这里要注意:如果等到页面初始化时再申请,系统会直接拒绝,因为没有用户交互触发的权限请求是不被允许的。

2. 初始化时验证权限并处理

在Fragment的onResume或者onViewCreated里,读取保存的URI后先检查权限状态,再决定是否加载内容:

override fun onResume() {
    super.onResume()
    val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext())
    val savedUriStr = sharedPrefs.getString("selected_image_uri", null)

    savedUriStr?.let {
        val uri = Uri.parse(it)
        // 检查当前是否拥有该URI的读权限
        val hasPermission = requireContext().contentResolver.checkUriPermission(
            uri,
            android.os.Process.myPid(),
            android.os.Process.myUid(),
            Intent.FLAG_GRANT_READ_URI_PERMISSION
        ) == PackageManager.PERMISSION_GRANTED

        if (hasPermission) {
            // 权限有效,加载图片和文件名
            loadImageAndFileName(uri)
        } else {
            // 权限失效,提示用户重新选择
            val imagePref = findPreference<CustomImagePreference>("image_preference")
            imagePref?.apply {
                summary = "请重新选择图片"
                setIcon(R.drawable.ic_image_placeholder)
            }
        }
    }
}

3. 适配Android 13+的媒体权限

如果你的APP目标SDK是33及以上,除了URI的持久权限,还需要在Manifest里声明READ_MEDIA_IMAGES权限:

<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

并且在首次打开设置页面时,主动请求这个权限,避免因为缺少媒体权限导致即使有URI权限也无法读取内容。

4. 安全获取文件名与加载图片

不要直接操作文件路径(Scoped Storage下禁止),用ContentResolver查询文件名,用图片加载库(Glide/Coil)加载缩略图:

private fun loadImageAndFileName(uri: Uri) {
    // 查询文件名
    val cursor = requireContext().contentResolver.query(
        uri,
        arrayOf(MediaStore.Images.Media.DISPLAY_NAME),
        null,
        null,
        null
    )
    cursor?.use {
        if (it.moveToFirst()) {
            val fileName = it.getString(it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
            findPreference<CustomImagePreference>("image_preference")?.summary = fileName
        }
    }

    // 加载缩略图(用Glide示例)
    Glide.with(this)
        .load(uri)
        .placeholder(R.drawable.ic_image_placeholder)
        .centerCrop()
        .into(findPreference<CustomImagePreference>("image_preference")?.iconView!!)
}

常见坑点提醒

  • 必须使用content://格式的URI,不要用file://——后者在Scoped Storage下会被限制访问;
  • 持久化权限只对用户通过系统文件选择器选择的URI生效,手动构造的URI无法申请持久权限;
  • 不要在后台线程之外直接加载图片,避免阻塞UI线程。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.25 04:19:09