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
相关产品推荐
相关产品推荐

