如何在不申请 READ_EXTERNAL_STORAGE 的情况下访问用户照片?

解读

面试官问的是“不申请 READ_EXTERNAL_STORAGE”,而不是“不申请任何权限”。
国内 Android 10+ 强制分区存储(Scoped Storage),READ_EXTERNAL_STORAGE 在 targetSdk≥33 时已被 Google 废弃,厂商 ROM 也会直接拒绝。
因此考点是:

  1. 是否知道分区存储模型;
  2. 能否用系统提供的“用户主动参与”机制绕过旧权限;
  3. 是否了解国内特殊路径(如 DCIM、Pictures)与媒体库 MediaStore 的权限差异;
  4. 能否给出落地代码片段与版本兼容策略。
    回答时切忌只说“用 SAF”或“用 MediaStore”,必须分场景、分版本、分用户交互把路径讲清,并主动提及国内机型兼容与性能细节,体现“上线过千万级 App”的经验。

知识点

  1. Scoped Storage 分区存储:Android 10 默认开启,11 强制,13 废弃 READ_EXTERNAL_STORAGE。
  2. MediaStore 公有目录:DCIM、Pictures、Movies、Music、Download 在自身 App 创建的文件无需权限即可读写;读取他 App 创建的图像需
    • API ≤28:READ_EXTERNAL_STORAGE
    • API 29-32:无权限只能读“自己创建”或“媒体文件所在 App 已卸载”
    • API 33+:READ_MEDIA_IMAGES(新权限)
  3. SAF(Storage Access Framework):通过 Intent.ACTION_OPEN_DOCUMENT 让用户单选或多选图像,返回 Uri,无需任何存储权限;可持久化授权 takePersistableUriPermission。
  4. PhotoPicker(系统组件):Android 13 引入 androidx.activity:activity:1.7+ 通过 ActivityResultContracts.PickVisualMedia() 调起系统级照片选择器,无需声明权限,返回 Uri 列表;国内 OPPO/小米/Vivo 已系统内置,低端机 fallback 到 SAF。
  5. 媒体库“自己创建”判定:MediaStore.Images.Media.OWNER_PACKAGE_NAME 与当前包名一致即可直接读取,可用于“编辑后保存再展示”场景。
  6. 文件路径转 Uri:File → FileProvider → content:// 私有 Uri,仅限本 App 访问,不涉权限。
  7. 国内合规:工信部 164 号文要求“最小必要权限”,直接拒绝 READ_EXTERNAL_STORAGE 上架审核;使用 SAF/PhotoPicker 零权限方案可一次性通过。

答案

分三条主线给出可落地方案,按优先级排序:

  1. 首选系统 PhotoPicker(零权限,体验最好)
    在 App 模块 build.gradle 中
    implementation "androidx.activity:activity:1.8.0"
    调用代码:
    val pickMedia = registerForActivityResult(PickVisualMedia()) { uri: Uri? ->
    uri?.let { loadBitmap(it) }
    }
    pickMedia.launch(PickVisualMediaRequest(PickVisualMedia.ImageOnly))
    无需在 Manifest 声明任何存储权限;Android 13+ 走系统组件,12L 以下自动 fallback 到 SAF。
    国内主流 ROM 已内置,低端机无阉割实测覆盖率 96%+。

  2. SAF 单选/多选(兼容至 API 19)
    Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
    type = "image/*"
    addCategory(Intent.CATEGORY_OPENABLE)
    putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
    }
    返回后 onActivityResult 拿到 clipData 或 data.uri;通过
    contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
    持久化授权,重启 App 仍可读。
    优点:无权限、可复用;缺点:UI 不可定制,首次用户需手动点选目录。

  3. 仅访问“自己创建”的图片(静默、无 UI)
    保存时主动插入 MediaStore:
    val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "demo_${System.currentTimeMillis()}.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
    put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
    }
    val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
    uri?.let { resolver.openOutputStream(it).use { out -> bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out) } }
    之后同一包名可直接通过该 Uri 读取,无需任何权限;适用于“拍照编辑后再次展示”闭环场景。

版本兼容策略:

  • targetSdk 33 及以上:完全移除 READ_EXTERNAL_STORAGE、READ_MEDIA_IMAGES 声明,统一走 PhotoPicker/SAF。
  • targetSdk 32 及以下:仍可按上述三条路径,不申请旧权限;若需要读取他 App 图片,优先引导用户用 SAF,而不是申请权限,避免国内商店审核驳回。
  • 性能注意:SAF 返回的 Uri 非文件路径,不可直接 new File;使用 ContentResolver.openFileDescriptor 拿到 fd,再映射到 BitmapRegionDecoder 实现大图局部解码,防止 OOM。
  • 合规提示:隐私政策中需声明“我们使用系统图片选择器,不申请任何存储权限,仅读取您选择的图片”。

拓展思考

  1. 如果需求是“批量备份用户所有照片”,无权限方案无法满足,只能引导用户授予 READ_MEDIA_IMAGES(API 33+)并通过 MediaStore 分页加载;此时需补充“权限使用目的”弹窗,否则国内商店以“权限与功能无关”驳回。
  2. PhotoPicker 在多用户/工作资料场景下返回的 Uri 可能指向其他用户空间,需 catch SecurityException 并提示“无法读取该图片”。
  3. Android 14 引入 部分访问照片(Selected Photos Access)机制,用户可仅授予“选中的几张”授权,需重新测试 takePersistableUriPermission 有效期。
  4. 车载/电视无 GMS 设备:SAF 仍可用,但系统 DocumentsUI 可能被厂商精简,需内置 fallback 到自定义网络导入(如扫码传图)。
  5. 安全角度:拿到 Uri 后禁止直接 send 给第三方 SDK,应先 copy 到 App 私有目录并重新生成 content:// FileProvider Uri,防止文件描述符泄漏。