Android 10+ 中 Scoped Storage 对应用文件访问带来了哪些限制?

解读

国内面试中,面试官问“Scoped Storage 带来了哪些限制”,并不是想听“不能乱写文件”这种一句话结论,而是想确认候选人是否真正踩过坑:是否知道旧代码在 targetSdk>=30 机器上突然崩溃的根因;是否能在不申请 MANAGE_EXTERNAL_STORAGE 这种“华为应用市场直接拒”的权限下,把文件写到用户可见、卸载不清、还能被微信/钉钉调起的位置;是否理解 MediaStore、SAF、FileProvider、RELATIVE_PATH、IS_PENDING、DATA 字段废弃、分区存储、强制过滤、路径重定向等细节。回答时要“先给结论,再给场景,最后给国内合规方案”,让面试官听到“这人确实上过线”。

知识点

  1. 分区存储(Scoped Storage)核心:以应用沙箱为边界,/sdcard 访问被重定向,直接 File.path 失效。
  2. 访问范围三级跳:
    私有目录(Context.getFilesDir 等)无限制;
    媒体文件(图片、视频、音频)通过 MediaStore 只读自己创建、读写需权限;
    非媒体文件(PDF、DOC、工程日志)只能走 SAF 或 MediaStore#createWriteRequest,无法直接路径创建。
  3. 权限变化:
    READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE 在 Android 11+ 仅授权“媒体文件”子集;
    想访问所有文件必须 MANAGE_EXTERNAL_STORAGE,国内商店(华为、OPPO、VIVO)审核红线,上架基本拒绝。
  4. 路径陷阱:
    Environment.getExternalStorageDirectory() 返回 /storage/emulated/0,但 targetSdk>=30 时直接 File.mkdirs() 返回 false;
    MediaStore.Images.Media.DATA 字段正式废弃,通过 _ID+ContentResolver.openFileDescriptor 读写;
    新建文件必须填充 RELATIVE_PATH、IS_PENDING,否则插入成功但用户相册不可见。
  5. 共享与卸载:
    应用卸载后 /Android/data/<pkg>/ 自动清;
    想让文件“卸载保留、可被第三方打开”只能放到 DCIM、Documents、Download 三个公共目录,且需 MediaStore 或 SAF。
  6. 性能差异:
    MediaStore 批量查询依赖索引,1000 张图片冷启动首次扫描耗时 200 ms+;
    SAF 单文件 createFile() 平均耗时 8-10 ms,比直接 File 慢 5-7 倍,需异步处理。
  7. 国内合规:
    工信部 164 号文要求“最小必要”,申请 MANAGE_EXTERNAL_STORAGE 必须提供“核心功能依赖”说明,否则下架;
    主流做法:媒体文件用 MediaStore+权限,文档用 SAF 让用户点一次“另存为”,日志写私有目录+FileProvider 分享。

答案

结论先行:Scoped Storage 把“整个 SD 卡随便读写”的时代终结了,应用只能直接访问私有目录与媒体公共目录;非媒体文件想创建必须走 SAF 或 MediaStore 新建请求,旧代码直接 new File("/sdcard/xxx").createNewFile() 在 targetSdk>=30 机器上会静默失败。

具体限制分四条展开:

  1. 路径访问被重定向:Environment.getExternalStorageDirectory()、getExternalStoragePublicDirectory() 依然返回旧路径,但内核通过挂载命名空间把访问重定向到 /storage/emulated/0/Android/data/<pkg>/,导致 mkdirs、listFiles 全部失效。
  2. 权限颗粒度变细:WRITE_EXTERNAL_STORAGE 在 Android 11+ 被降级为“媒体文件” scoped 权限,无法创建非媒体文件;想访问所有文件只能申请 MANAGE_EXTERNAL_STORAGE,而国内商店视其为“高危权限”,无核心功能直接拒审。
  3. 文件元数据入口强制走 MediaStore:图片、视频、音频必须 insert() 拿到 content:// 风格 URI,再通过 ContentResolver.openOutputStream 写入;DATA 字段废弃,直接解析路径会拿到空值。
  4. 共享与卸载策略变化:只有 DCIM、Pictures、Movies、Music、Download、Documents 六个公共目录在卸载后保留;任何尝试在根目录新建自定义文件夹(如 /sdcard/MyApp)都会被系统拒绝,且不会提示原因,调试困难。

落地方案:

  • 拍照/录屏:使用 MediaStore.Images.Media.insertImage(),填充 RELATIVE_PATH="Pictures/MyApp"、IS_PENDING=1,写入完成后置 IS_PENDING=0,相册即时可见。
  • 导出 PDF/日志:启动 SAF Intent.ACTION_CREATE_DOCUMENT,让用户选保存位置,拿到 URI 后使用 ContentResolver.openOutputStream,无需任何权限,合规且卸载无残留。
  • 热更新/大文件缓存:放 Context.getExternalFilesDir(null),路径 /sdcard/Android/data/<pkg>/files,卸载即清,避免占用用户空间。
  • 旧代码兼容:targetSdk 暂时 29 可关闭分区存储,但华为应用市场 2023Q4 开始强制 targetSdk≥31,必须整改,否则无法上架。

拓展思考

  1. 如果产品坚持“静默保存到根目录”且无法改交互,能否用 JNI 调用 openat 绕过 vfs 重定向?——可以绕过检测,但会触发 GMS 安全扫描,Google Play 直接下架;国内商店也会因“过度收集”被投诉,属于高风险方案。
  2. 折叠屏/多用户场景下,Scoped Storage 的挂载点如何区分主屏与副屏?——系统为每个用户/每个 profile 创建独立挂载命名空间,getExternalFilesDir 返回路径已带 userId,应用无需特殊处理,但 ContentResolver 查询需加 “AND owner_package_name=?” 条件,否则把副屏照片也列出来。
  3. Android 14 开始引入 Photo Picker,完全不走权限模型,是否意味着 MediaStore 会被废弃?——Photo Picker 仅解决“读取”场景,写入仍需 MediaStore;Google 明确未来三年 MediaStore 仍是唯一合法写入通道,提前适配可避免二次返工。