别再乱用MANAGE_EXTERNAL_STORAGE了!Android 11+文件访问的正确姿势(附MediaStore/SAF实战代码)
Android 11存储权限的优雅适配告别MANAGE_EXTERNAL_STORAGE的野蛮操作每次看到开发者为了省事直接申请MANAGE_EXTERNAL_STORAGE权限我的内心都在滴血。这就像为了打开一个门锁而选择拆掉整面墙——粗暴、危险且后患无穷。在Android 11及更高版本中Google对文件访问权限的收紧并非无的放矢而是为了保护用户隐私和数据安全。本文将带你深入理解存储权限的演变逻辑并掌握更优雅的适配方案。1. 为什么MANAGE_EXTERNAL_STORAGE成为开发者最后的选项在Android 10之前存储权限管理相对宽松READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE几乎可以满足所有需求。但随着隐私保护意识的提升Google开始收紧这一政策隐私沙盒计划限制应用随意访问用户数据作用域存储(Scoped Storage)应用只能访问自己创建的文件和特定媒体类型权限分级制度将权限分为普通、签名和特殊三类MANAGE_EXTERNAL_STORAGE属于特殊权限它的设计初衷是给真正的文件管理器类应用使用的。Google Play对这类权限的审核极其严格普通应用很难通过审核。提示根据Google Play统计90%申请了该权限的应用都被拒绝上架或要求修改。2. 现代Android存储体系的三大支柱2.1 MediaStore媒体文件的专属通道对于图片、视频、音频等媒体文件MediaStore是最佳选择。它提供了结构化的访问方式// 查询所有图片 val projection arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.SIZE ) val cursor contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, ${MediaStore.Images.Media.DATE_ADDED} DESC ) cursor?.use { while (it.moveToNext()) { val id it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)) val name it.getString(it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)) val size it.getInt(it.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)) val contentUri ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id ) // 使用contentUri处理图片 } }2.2 Storage Access Framework (SAF)用户主导的文件选择SAF的核心是尊重用户选择权。通过Intent.ACTION_OPEN_DOCUMENT或Intent.ACTION_CREATE_DOCUMENT让用户自己决定哪些文件可以被访问// 请求用户选择一个文件 val intent Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type application/pdf putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } startActivityForResult(intent, REQUEST_CODE_PICK_PDF) // 处理返回结果 override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (requestCode REQUEST_CODE_PICK_PDF resultCode RESULT_OK) { data?.data?.let { uri - // 获取持久化访问权限 contentResolver.takePersistableUriPermission( uri, Intent.FLAG_GRANT_READ_URI_PERMISSION ) // 使用uri访问文件 } } }2.3 应用专属存储空间无需权限的安全区每个应用都有自己专属的存储空间位于内部存储/data/data/包名外部存储/Android/data/包名在这个区域内应用可以自由读写无需任何权限// 获取应用专属外部存储目录 val externalFilesDir getExternalFilesDir(null) // 创建新文件 val newFile File(externalFilesDir, my_data.txt) newFile.writeText(Hello, Scoped Storage!)3. 实战场景不同需求下的最佳实践3.1 场景一开发文档扫描应用错误做法申请MANAGE_EXTERNAL_STORAGE权限直接扫描整个存储空间。正确方案使用MediaStore访问图片库让用户选择要扫描的图片使用SAF让用户指定输出PDF的保存位置处理后的文件保存在应用专属目录或用户指定的位置关键代码示例// 扫描图片并转换为PDF fun scanDocument(context: Context, imageUris: ListUri) { // 在缓存目录创建临时PDF val outputFile File(context.cacheDir, scanned_${System.currentTimeMillis()}.pdf) // 实际转换逻辑... // 让用户选择保存位置 val intent Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type application/pdf putExtra(Intent.EXTRA_TITLE, scanned_document.pdf) } context.startActivity(intent) }3.2 场景二开发备份还原工具错误做法请求全盘访问权限来备份用户数据。正确方案使用SAF让用户选择备份文件位置备份数据存储在应用专属目录还原时同样通过SAF让用户选择备份文件3.3 场景三媒体文件批量处理错误做法直接遍历文件系统查找目标文件。正确方案利用MediaStore的查询能力// 批量查询最近7天拍摄的照片 val calendar Calendar.getInstance().apply { add(Calendar.DAY_OF_YEAR, -7) } val selection ${MediaStore.Images.Media.DATE_TAKEN} ? val selectionArgs arrayOf(calendar.timeInMillis.toString()) val cursor contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, selection, selectionArgs, ${MediaStore.Images.Media.DATE_TAKEN} DESC )4. 真的需要MANAGE_EXTERNAL_STORAGE这些情况例外虽然大多数应用应该避免使用这个权限但确实存在一些合理的使用场景真正的文件管理器应用杀毒软件需要全盘扫描备份还原整个设备数据的工具即使在这些情况下申请流程也应当谨慎在AndroidManifest.xml中声明权限检查环境是否支持引导用户跳转到系统设置页面fun checkAndRequestManageStoragePermission(activity: Activity) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { if (!Environment.isExternalStorageManager()) { val intent Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply { data Uri.parse(package:${activity.packageName}) } activity.startActivity(intent) } } }重要提示即使获得了这个权限应用在Android 11上仍然无法访问其他应用的专属目录。在最近的一个项目中我们接手了一个因为滥用MANAGE_EXTERNAL_STORAGE而被Google Play拒绝的应用。通过重构使用MediaStore和SAF不仅顺利通过了审核用户反馈也显示文件操作的体验更加清晰和安全。这再次证明遵循平台规范不是限制而是为了构建更好的应用生态。