一土水丰色今口 2025-12-20 18:55 采纳率: 98.3%
浏览 2
已采纳

Android 11+如何适配分区存储权限?

在Android 11(API 30)及以上版本中,应用无法直接通过路径访问外部存储中的非自有文件,即使已声明`WRITE_EXTERNAL_STORAGE`权限。这导致许多依赖文件路径操作的应用出现“Permission Denied”或找不到文件的问题。常见疑问是:为何申请了存储权限仍无法读写公共目录(如DCIM、Download)?其核心原因在于Android 11强化了分区存储(Scoped Storage)机制,限制了对共享存储的自由访问。开发者必须使用MediaStore API或Storage Access Framework(SAF)来访问公共媒体文件或用户选择的文件。如何正确迁移旧有文件操作逻辑以适配新存储模型,成为适配Android 11+的关键挑战。
  • 写回答

1条回答 默认 最新

  • 曲绿意 2025-12-20 19:01
    关注

    Android 11+ 存储权限与分区存储(Scoped Storage)深度解析

    1. 背景:为何申请了 WRITE_EXTERNAL_STORAGE 仍无法访问公共目录?

    在 Android 10(API 29)引入分区存储(Scoped Storage)后,Google 进一步在 Android 11(API 30)中强化了该机制。即使应用声明了 WRITE_EXTERNAL_STORAGE 权限并获得用户授权,也无法直接通过文件路径(如 /storage/emulated/0/DCIM/Camera/IMG_001.jpg)读写非自有文件。

    核心原因在于:从 Android 11 开始,系统强制执行更严格的沙盒模型,限制应用对共享外部存储的自由访问,以增强用户隐私保护。

    这意味着传统基于 File 类的路径操作将失效,尤其影响相册、文件管理器、备份工具等依赖全局文件扫描的应用。

    2. 分区存储(Scoped Storage)的核心机制

    • 应用专属目录:位于 Context.getExternalFilesDir() 下,无需额外权限即可自由读写。
    • 媒体集合访问:通过 MediaStore API 访问图片、视频、音频等公共媒体文件。
    • 文档和下载文件:使用 MediaStore.DownloadsStorage Access Framework (SAF) 访问 Download 目录等。
    • 保留旧行为的例外:可通过在 AndroidManifest.xml 中设置 requestLegacyExternalStorage="true" 暂时绕过限制,但仅适用于 targetSdkVersion ≤ 29 的应用;targetSdkVersion ≥ 30 时该标志无效。

    3. 常见问题分析流程图

    mermaid
        graph TD
            A[应用无法访问 DCIM/Download 文件] --> B{是否声明 WRITE_EXTERNAL_STORAGE?}
            B -- 否 --> C[需添加权限]
            B -- 是 --> D{targetSdkVersion >= 30?}
            D -- 否 --> E[可尝试 requestLegacyExternalStorage=true]
            D -- 是 --> F[必须使用 MediaStore 或 SAF]
            F --> G[图片/视频? → MediaStore.Images]
            F --> H[文档/任意文件? → SAF Intent]
        

    4. 技术迁移方案对比表

    场景旧方式新推荐方式适配建议
    读取相机照片new File(Environment.getExternalStoragePublicDirectory(DIRECTORY_DCIM), "Camera")MediaStore.Images.Media.EXTERNAL_CONTENT_URI使用 ContentResolver 查询并获取 Uri
    保存截图直接 write 到 /Pictures/Screenshots插入 MediaStore 图像条目后写入流先 insert 再 openOutputStream
    选择任意文件自定义文件浏览器遍历 SD 卡Intent ACTION_OPEN_DOCUMENT依赖用户授权返回的 Document URI
    批量删除下载文件file.delete()ContentResolver.delete(uri, null, null)需通过查询获取对应 MediaStore URI
    创建应用专属缓存/storage/emulated/0/MyAppCachegetExternalFilesDir(null)完全合法且推荐

    5. 关键代码示例:使用 MediaStore 保存图片

    private Uri insertImageToMediaStore(ContentResolver resolver, String displayName, String mimeType) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DISPLAY_NAME, displayName);
            values.put(MediaStore.Images.Media.MIME_TYPE, mimeType);
            values.put(MediaStore.Images.Media.IS_PENDING, 1);
    
            return resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        }
    
        // 使用示例
        Uri uri = insertImageToMediaStore(getContentResolver(), "screenshot.png", "image/png");
        try (OutputStream out = getContentResolver().openOutputStream(uri)) {
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
        }
    
        // 完成后取消 pending 状态
        ContentValues updateValues = new ContentValues();
        updateValues.put(MediaStore.Images.Media.IS_PENDING, 0);
        getContentResolver().update(uri, updateValues, null, null);

    6. Storage Access Framework(SAF)实践指南

    当需要用户主动选择文件时,应使用 SAF:

    // 打开文档选择器
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("*/*"); // 可限定为 image/* 等
    startActivityForResult(intent, REQUEST_CODE_PICK_FILE);
    
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CODE_PICK_FILE && resultCode == RESULT_OK && data != null) {
            Uri documentUri = data.getData();
            // 持久化权限(可选)
            getContentResolver().takePersistableUriPermission(documentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
            // 使用 DocumentFile.fromSingleUri 处理
            DocumentFile pickedFile = DocumentFile.fromSingleUri(this, documentUri);
            Log.d("SAF", "File name: " + pickedFile.getName());
        }
    }

    7. 迁移策略与最佳实践

    1. 评估现有文件操作逻辑,识别所有使用 File 路径访问共享存储的代码点。
    2. 将媒体文件操作统一迁移到 MediaStore API,避免硬编码路径。
    3. 对于非媒体文件(如 PDF、ZIP),优先使用 SAF 提供用户选择入口。
    4. 合理利用 DocumentFile 封装类简化 SAF 操作。
    5. 测试不同厂商设备的行为差异,部分 OEM 厂商可能有定制策略。
    6. 考虑使用 Jetpack 库(如 androidx.core:core-filesystem)辅助兼容性处理。
    7. 监控 ANR 和慢查询问题,MediaStore 查询应异步执行。
    8. 向用户提供清晰的引导说明,解释为何需要“选择文件”而非自动扫描。
    9. 针对企业或管理类应用,可探索 MANAGE_EXTERNAL_STORAGE 权限(需 Google Play 特殊审批)。
    10. 持续关注 Android 13+ 对照片选择器(Photo Picker)等新 API 的演进。
    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

问题事件

  • 已采纳回答 12月21日
  • 创建了问题 12月20日