Android 16 权限默认授予机制与存储目录持久化访问完整实践指南

夏晨姑娘_3276

夏晨姑娘_3276

2026-10-28

421人浏览

原创

Android 16 权限默认授予机制与存储目录持久化访问完整实践指南

本文详解 Android 中通过 ACTION_OPEN_DOCUMENT_TREE 获取的目录访问权限为何在应用重启后失效,并系统性讲解如何调用 takePersistableUriPermission() 实现跨进程、跨生命周期的持久化文件访问,同时结合 Android 权限演进趋势给出合规适配建议。

本文详解 android 中通过 `action_open_document_tree` 获取的目录访问权限为何在应用重启后失效,并系统性讲解如何调用 `takepersistableuripermission()` 实现跨进程、跨生命周期的持久化文件访问,同时结合 android 权限演进趋势给出合规适配建议。

在 Android 开发中,使用 Intent.ACTION_OPEN_DOCUMENT_TREE 授权用户选择一个文件夹(如用于自动备份),是实现“作用域存储”下安全、可控外部存储访问的核心方式。但许多开发者会遇到这样一个典型问题:首次选择目录后功能正常,一旦强制关闭应用(Force Stop)或进程被系统回收,再次启动时 DocumentFile.fromTreeUri() 返回的对象中 findFile() 调用抛出 NullPointerException——根本原因在于:系统授予的 URI 访问权限默认是临时的,仅在当前任务栈存活期间有效;Force Stop 会彻底清除所有已授予的 URI 权限,且不会自动恢复。

✅ 正确做法:持久化 URI 权限(Persistable URI Permission)

Android 自 API 19(KitKat)起引入 ContentResolver.takePersistableUriPermission(),专为解决此问题而设计。它将用户一次授权的 content:// URI 权限“固化”到系统权限管理器中,使该 URI 在 App 重启、设备重 boot 后仍可安全访问(前提是未被用户手动撤销)。

你需要在 onActivityResult() 中、成功获取 URI 后立即调用该方法:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    super.onActivityResult(requestCode, resultCode, resultData);
    if (requestCode == AppConstants.GRANT_ACCESS_TO_AUTO_BACKUP_FILE && resultCode == Activity.RESULT_OK) {
        if (resultData != null) {
            Uri uri = resultData.getData();
            // ✅ 关键步骤:持久化读写权限
            final int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
            getContentResolver().takePersistableUriPermission(uri, takeFlags);

            // 保存 URI 字符串(推荐使用 SharedPreferences)
            spEditor.putString("auto_backup_uri", uri.toString()).apply();

            // 后续创建子目录逻辑保持不变...
            DocumentFile documentFile = DocumentFile.fromTreeUri(this, uri);
            DocumentFile dfFinalDir = documentFile.findFile(getString(R.string.app_name_no_space));
            if (dfFinalDir == null) {
                dfFinalDir = documentFile.createDirectory(getString(R.string.app_name_no_space));
            }
            // ...
        }
    }
}

⚠️ 注意事项:

  • takePersistableUriPermission() 必须在 onActivityResult() 中、RESULT_OK 状态下执行,不能延迟到异步线程或后续生命周期回调中;
  • 权限标志需与实际需求匹配:若仅读取,传 FLAG_GRANT_READ_URI_PERMISSION;若需写入(如本例创建/修改文件),必须同时传入 FLAG_GRANT_WRITE_URI_PERMISSION;
  • 该操作无需用户二次确认,但仅对用户明确通过系统文件选择器授予的 URI 生效(即来自 ACTION_OPEN_DOCUMENT_TREE 或 ACTION_OPEN_DOCUMENT 的结果);
  • 持久化权限可被用户在「设置 → 应用 → [你的App] → 权限 → 存储访问」中手动撤销,代码中应始终做好空值校验。

? 读取时的安全调用模式

在 writeDataToFile() 中,不应假设 uriString 一定有效。务必先校验 URI 是否仍具访问权,并在失败时引导用户重新授权:

Android SMS Gateway
Android SMS Gateway

自托管短信服务,使用 Android 手机作为网关提供 HTTP API,支持发送/接收短信,兼容主流短信网关。

下载
private void writeDataToFile(Intent intent) {
    String uriString = sp.getString("auto_backup_uri", "");
    if (uriString.isEmpty()) {
        // 引导用户重新选择目录
        requestBackupDirectory();
        return;
    }

    Uri uri = Uri.parse(uriString);
    // ✅ 验证持久化权限是否仍然有效(推荐)
    if (!isUriPermissionGranted(uri)) {
        Log.w("TAG", "Persisted permission lost for: " + uri);
        requestBackupDirectory();
        return;
    }

    DocumentFile dfBeforeFinalDir = DocumentFile.fromTreeUri(this, uri);
    DocumentFile dfFinalDir = dfBeforeFinalDir.findFile(getString(R.string.app_name_no_space));
    if (dfFinalDir == null) {
        // 目录可能被用户删除,需重建
        dfFinalDir = dfBeforeFinalDir.createDirectory(getString(R.string.app_name_no_space));
    }

    DocumentFile dfFinalAutoBackupFile = dfFinalDir.findFile("notatki_a");
    if (dfFinalAutoBackupFile == null) {
        dfFinalAutoBackupFile = dfFinalDir.createFile("application/json", "notatki_a");
    }

    try (OutputStream os = getContentResolver().openOutputStream(dfFinalAutoBackupFile.getUri())) {
        os.write(jsonNotes.getBytes(StandardCharsets.UTF_8));
        intent.putExtra("auto_save_status", AppConstants.SUCCESS);
    } catch (IOException e) {
        doWhenException(e, "Failed to write backup file", intent);
    }
}

// 辅助方法:检查 URI 权限是否仍存在
private boolean isUriPermissionGranted(Uri uri) {
    int flags = getContentResolver().getPersistedUriPermissions().stream()
            .filter(p -> p.getUri().equals(uri))
            .mapToInt(p -> p.getModeFlags())
            .findFirst()
            .orElse(0);
    return (flags & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION)) 
           == (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}

? 与 Android 权限演进趋势的一致性

从 Android 6.0 运行时权限,到 Android 10+ 的作用域存储(Scoped Storage),再到 Android 14+ 的照片选择器(Photo Picker),Google 持续强化“最小权限原则”和“用户可控性”。ACTION_OPEN_DOCUMENT_TREE + takePersistableUriPermission() 正是这一理念下的标准、合规、面向未来的方案——它不依赖 MANAGE_EXTERNAL_STORAGE(已在 Android 11+ 被限制为特殊用途)、不请求全局存储权限,而是让用户精确控制“哪个文件夹、给谁用、用多久”。

✅ 最佳实践总结:

  • 永远不要假设 URI 权限永久有效:每次使用前做存在性与有效性校验;
  • 权限获取与持久化必须同步完成:onActivityResult() 是唯一可靠时机;
  • 优雅降级:权限丢失时提供清晰 UI 引导,而非崩溃;
  • 避免滥用:仅对真正需要长期访问的目录(如备份根目录)持久化,普通临时文件用 getCacheDir() 即可。

通过以上改造,你的应用即可在 Android 10–16+ 全系列版本中稳定实现“一次选择、长期可用”的目录访问能力,既符合平台规范,也大幅提升用户体验与数据可靠性。

相关专题

更多
android开发三大框架
android开发三大框架

android开发三大框架是XUtil框架、volley框架、ImageLoader框架。本专题为大家提供android开发三大框架相关的各种文章、以及下载和课程。

2023.08.14

902

4

android是什么系统
android是什么系统

Android是一种功能强大、灵活可定制、应用丰富、多任务处理能力强、兼容性好、网络连接能力强的操作系统。本专题为大家提供android相关的文章、下载、课程内容,供大家免费下载体验。

2023.08.22

2727

5

android权限限制怎么解开
android权限限制怎么解开

android权限限制可以使用Root权限、第三方权限管理应用程序、ADB命令和Xposed框架解开。详细介绍:1、Root权限,通过获取Root权限,用户可以解锁所有权限,并对系统进行自定义和修改;2、第三方权限管理应用程序,用户可以轻松地控制和管理应用程序的权限;3、ADB命令,用户可以在设备上执行各种操作,包括解锁权限;4、Xposed框架,用户可以在不修改系统文件的情况下修改应用程序的行为和权限。

2023.09.19

5337

7

android重启应用的方法有哪些
android重启应用的方法有哪些

android重启应用有通过Intent、PendingIntent、系统服务、Runtime等方法。本专题为大家提供Android相关的文章、下载、课程内容,供大家免费下载体验。

2023.10.18

685

5

Android语音播放功能实现方法
Android语音播放功能实现方法

实现方法有使用MediaPlayer实现、使用SoundPool实现两种。可以根据具体的需求选择适合的方法进行实现。想了解更多语音播放的相关内容,可以阅读本专题下面的文章。

2024.03.01

964

5

Vite 项目如何打包和部署
Vite 项目如何打包和部署

Vite项目打包部署专题汇集生产构建、产物预览、输出目录、资源路径、Nginx与GitHub Pages部署及常见白屏和路由404问题的已审核文章,帮助读者完成上线流程。

2026.10.28

0

16

Vite 如何搭配 Vue.js
Vite 如何搭配 Vue.js

Vite搭配Vue.js专题汇集创建Vue 3项目、配置开发环境、路径别名、Vue插件、代理、多环境构建与调试的已审核文章,帮助读者完成从初始化到日常开发的配置。

2026.10.28

0

15

Vite 项目目录怎么看
Vite 项目目录怎么看

Vite项目目录专题汇集入口文件、src源码、public静态资源、路径别名、项目根目录和构建输出目录相关的已审核文章,帮助读者理解各文件夹的用途与引用方式。

2026.10.28

0

10

Gradle插件开发入门教程
Gradle插件开发入门教程

本专题介绍Gradle插件开发的基本流程,涵盖插件类编写、自定义任务注册、扩展参数设计和插件应用方法,帮助你理解插件如何组织构建逻辑,减少多个项目之间的重复配置。

2026.10.22

120

11

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Excel 教程
Excel 教程

共162课时 | 44.8万人学习

Java 教程
Java 教程

共578课时 | 194.1万人学习

Uniapp从零开始实现新闻资讯应用
Uniapp从零开始实现新闻资讯应用

共64课时 | 9.6万人学习