Skip to content

Android 13 外部存储写入权限解决方案

TIP

关键变更提醒:自 Android 11(API 30)起,WRITE_EXTERNAL_STORAGE 权限在共享存储空间中不再提供任何额外访问权限。此变更在 Android 13(API 33)中进一步强化。

问题描述

当应用在 Android 13 及以上设备中请求 WRITE_EXTERNAL_STORAGE 权限时:

  1. 运行时权限弹窗不再显示
  2. 权限授予机制完全失效
  3. 仅影响 Android 13+ 设备(Android 12 及以下正常)

根本原因在于 Android 存储权限模型的重大变更:

  • 精细媒体权限:Android 13 引入 READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIO 替代原媒体访问权限
  • 权限废止:从 Android 11 开始,WRITE_EXTERNAL_STORAGE 对于共享存储空间访问已无实际效果
  • 存储分区限制:应用无法自由访问外部存储中其他应用的文件目录

推荐解决方案

方案1: 使用应用专属存储 (推荐)

适用于无需共享的应用私有文件(如密钥库):

java
File appDir = getExternalFilesDir("KeystoreDir");
if (!appDir.exists()) {
    if (!appDir.mkdirs()) {
        // 目录创建失败处理
        Toast.makeText(this, "目录创建失败", Toast.LENGTH_SHORT).show();
        return;
    }
}

File keyFile = new File(appDir, "keystore.bks");
try (FileOutputStream fos = new FileOutputStream(keyFile)) {
    // 写入文件内容
} catch (IOException e) {
    e.printStackTrace();
}

优势

  • ✅ 无需任何权限(包括在 AndroidManifest.xml 中声明)
  • ✅ 文件随应用卸载自动清除
  • ✅ 支持所有 Android 版本

注意

存储在 getExternalFilesDir() 的文件:

  1. 只能被本应用访问
  2. 不会出现在相册/文件管理器等公共视图中
  3. 用户可通过"设置 > 应用 > 存储"清除

方案2: 存储访问框架 (SAF)

当需要用户指定存储位置时(如下载目录):

java
// 启动目录选择器
private ActivityResultLauncher<Intent> dirLauncher = registerForActivityResult(
    new ActivityResultContracts.StartActivityForResult(),
    result -> {
        if (result.getResultCode() == RESULT_OK) {
            Uri treeUri = result.getData().getData();
            persistUriPermission(treeUri); // 持久化权限
            saveFileToUri(treeUri, "file.txt", "内容");
        }
    }
);

void pickDirectory() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    dirLauncher.launch(intent);
}

void persistUriPermission(Uri uri) {
    getContentResolver().takePersistableUriPermission(uri,
        Intent.FLAG_GRANT_READ_URI_PERMISSION | 
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}

void saveFileToUri(Uri baseUri, String name, String content) {
    DocumentFile dir = DocumentFile.fromTreeUri(this, baseUri);
    DocumentFile file = dir.createFile("text/plain", name);
    
    try (OutputStream os = getContentResolver().openOutputStream(file.getUri())) {
        os.write(content.getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    }
}

方案3: 管理所有文件权限 (谨慎使用)

仅适用于文件管理器类应用(需 Play 商店特殊批准):

xml
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
java
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
    if (!Environment.isExternalStorageManager()) {
        // 跳转系统设置页
        Intent intent = new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);
        startActivity(intent);
    }
    // 已有权限则可自由写入
} else {
    // Android 10 及以下保持原有逻辑
}

警告

使用 MANAGE_EXTERNAL_STORAGE 时:

  • 必须通过 Google Play 声明表审批
  • 非文件管理器类应用将被拒绝上架
  • Play 控制台会有额外审核步骤

版本兼容处理

实现向下兼容的优雅方案:

java
public void saveFileWithFallback() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        // Android 13+ 使用应用专属存储
        saveToAppSpecificDir();
    } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
        // Android 11-12 检查旧权限
        if (Environment.isExternalStorageManager()) {
            saveLegacyWay();
        } else {
            requestLegacyPermission();
        }
    } else {
        // Android 10 及以下请求运行时权限
        if (checkSelfPermission(WRITE_EXTERNAL_STORAGE) == PERMISSION_GRANTED) {
            saveLegacyWay();
        } else {
            requestPermissions(new String[]{WRITE_EXTERNAL_STORAGE}, 101);
        }
    }
}

最佳实践总结

方法使用场景权限需求用户可见性
应用专属存储私有配置文件、缓存数据无需不可见
SAF (存储访问框架)导出用户可见文件需用户主动选择可见
媒体存储 API图片、音视频文件READ_MEDIA_*可见
所有文件访问文件管理器类应用MANAGE_EXTERNAL_STORAGE全部可见

升级建议

  1. 优先使用应用专属目录存储私有文件(最简方案)
  2. 使用 MediaStore API 处理媒体文件
  3. 通过 Intent.ACTION_CREATE_DOCUMENT 创建用户可见文档
  4. 仅当绝对必要时才考虑管理所有文件权限 ::>

更新您的应用以遵循这些模式,将确保在 Android 13 及以上设备实现无缝存储访问,同时通过 Play 商店的合规审核。