Android 11分区存储适配指南:MediaStore API与权限申请实战

📅 发布时间:2026/8/8 6:18:07
Android 11分区存储适配指南:MediaStore API与权限申请实战
1. 项目概述Android 11文件权限的“新规矩”如果你最近在开发或维护一个Android应用并且发现原本在Android 10上跑得好好的文件读写功能到了Android 11API 30及以上的设备上突然就“失灵”了那你大概率是遇到了Android存储权限框架的一次重大变革。这不仅仅是多申请一个权限那么简单而是整个文件访问哲学的改变。从Android 11开始Google引入了名为“分区存储”Scoped Storage的强制执行策略旨在更好地保护用户隐私和应用数据安全。简单来说系统给每个应用都划了一个“私人沙箱”你的应用可以自由读写自己沙箱内的文件但想访问沙箱外的公共媒体文件或其他应用的文件就得遵守一套更严格、更精细的规则。这个项目标题“Android11及以上 文件读写权限申请”核心就是解决如何在新的规则下合法、正确地获取访问外部存储的权限让应用的功能不受影响。无论是处理用户选择的图片、下载文件到公共目录还是与其它应用共享数据你都需要重新理解并适配这套新机制。2. 权限体系核心变更与设计思路2.1 从“粗放”到“精细”分区存储的核心思想在Android 11之前应用只要获得了READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE这两个运行时权限就几乎拥有了访问整个外部存储空间的“生杀大权”。你可以随意遍历SD卡在任何目录创建、修改、删除文件。这带来了极大的灵活性的同时也埋下了巨大的隐私和安全隐患。一个手电筒应用为什么要读取我的所有照片这就是旧模式的弊端。Android 11强制的分区存储彻底改变了这一局面。它的核心思想是“最小权限原则”和“用户明确授权”。现在外部存储空间被清晰地划分为两类应用专属目录每个应用在外部存储上都有一个属于自己的、私密的目录通常位于Android/data/package_name/或Android/obb/package_name/。应用无需任何权限即可在此目录内进行任意读写操作且当应用被卸载时这些目录会被自动清理。这相当于你的“私人书房”。共享存储空间主要包含公共的媒体文件图片、视频、音频和下载文件等。应用不能直接通过文件路径去访问这些文件而是必须通过系统提供的API如MediaStore来访问。并且访问不同类型的文件需要不同的、更细粒度的权限或者根本不需要权限而是通过系统的“文件选择器”由用户亲自选择授予访问特定文件的权限。这种设计将文件访问的主动权交还给了用户。应用不能再“偷偷地”扫描整个存储只能通过标准化的、用户可见的方式访问有限的、必要的文件。2.2 新旧权限对照与适配策略面对这个变化我们的适配策略需要根据应用的实际需求来制定。首先要彻底放弃通过File类直接操作共享存储路径如Environment.getExternalStorageDirectory()该方法在Android 11上已废弃并返回应用专属目录的旧思路。权限策略选择需求场景Android 10及以前Android 11及以后推荐方案备注与风险读写应用私有文件无需权限使用getExternalFilesDir()等无需任何权限继续使用getExternalFilesDir()等最安全、最推荐的方式文件随应用卸载而删除。访问媒体文件图片、视频、音频申请READ_EXTERNAL_STORAGE方案A无需权限使用ACTION_OPEN_DOCUMENT或ACTION_PICK等Intent由用户通过系统选择器选取。方案B有限访问申请新的媒体位置权限如ACCESS_MEDIA_LOCATION用于访问照片Exif中的精确位置或使用MediaStoreAPI配合READ_EXTERNAL_STORAGE但权限作用已变化。方案A是未来方向用户体验好且隐私安全。方案B中即使拥有READ_EXTERNAL_STORAGE也只能访问MediaStore中的文件无法通过路径直接打开。创建/修改非媒体公共文件如PDF、文档申请WRITE_EXTERNAL_STORAGE无需权限使用MediaStore.Downloads集合或通过ACTION_CREATE_DOCUMENT、ACTION_OPEN_DOCUMENTIntent与系统文档UI交互。将文件保存到Downloads目录是常见做法通过MediaStore插入记录即可系统会处理实际存储位置。访问所有文件管理类应用申请读写权限申请所有文件访问权限MANAGE_EXTERNAL_STORAGE。这是一个特殊权限需要跳转到系统设置页由用户手动开启且上架Google Play需要声明合规用途。强烈不推荐普通应用使用。Google Play对使用此权限的应用审核极其严格必须提供充分理由如文件管理器、备份还原、防病毒应用。滥用可能导致应用被下架。注意READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE在Android 11上虽然存在但其效力大大减弱。对于媒体文件它们仅代表“可以访问媒体集合”而非整个存储。对于其他文件它们基本无效。因此适配的核心是转变思路从“申请万能钥匙”变为“按需使用专用工具”。3. 关键权限申请与API使用详解3.1 申请标准运行时权限以访问媒体文件为例即便在Android 11上如果你仍希望通过MediaStoreAPI以编程方式访问设备上的所有图片例如做一个相册应用你仍然需要申请READ_EXTERNAL_STORAGE权限。但申请流程和效果已经不同。第一步在AndroidManifest.xml中声明权限uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- 如果还需要写入媒体文件则添加 -- uses-permission android:nameandroid.permission.WRITE_EXTERNAL_STORAGE / !-- 针对Android 10及以下如果需要旧式存储访问可添加requestLegacyExternalStorage属性但Android 11上此标志无效 -- application ... android:requestLegacyExternalStoragetrue !-- 仅对targetSdkVersion29的应用在Android 10上有效 -- /application第二步在代码中动态申请权限使用AndroidX Activity Result API是当前推荐的做法它比传统的onRequestPermissionsResult回调更清晰。// 在Activity或Fragment中 private val requestPermissionLauncher registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean - if (isGranted) { // 权限被授予现在可以通过MediaStore查询媒体文件了 loadImagesFromMediaStore() } else { // 权限被拒绝向用户解释为什么需要这个权限并可能引导至设置页 showPermissionDeniedDialog() } } fun checkAndRequestReadStoragePermission() { when { // 检查权限状态 ContextCompat.checkSelfPermission( this, Manifest.permission.READ_EXTERNAL_STORAGE ) PackageManager.PERMISSION_GRANTED - { // 已有权限直接执行操作 loadImagesFromMediaStore() } // 判断是否应该显示权限请求理由弹窗用户之前拒绝过 ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.READ_EXTERNAL_STORAGE ) - { // 向用户解释为什么需要访问照片例如“需要此权限来显示您的相册” showRationaleDialog { requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } } else - { // 直接请求权限 requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) } } }实操心得在Android 11上即使用户授予了READ_EXTERNAL_STORAGE权限你的应用也只能访问MediaStore中包含的图片、视频、音频文件。你无法通过File.listFiles()去遍历DCIM或Pictures目录。必须使用ContentResolver查询MediaStore。3.2 使用Storage Access FrameworkSAF—— 无权限访问的利器对于让用户选择特定文件或保存文件到特定位置Storage Access Framework是最佳实践。它不需要任何权限通过系统内置的文档UI与用户交互用户选择什么应用就能访问什么。场景一让用户选择一个文件如图片、PDFprivate val openDocumentLauncher registerForActivityResult( ActivityResultContracts.OpenDocument() ) { uri: Uri? - uri?.let { // 用户选择了一个文件uri指向该文件 // 通过ContentResolver打开InputStream读取文件内容 contentResolver.openInputStream(it)?.use { inputStream - // 处理文件流... } // 注意你获得的Uri访问权限可能是临时的。如果需要持久化访问可以调用takePersistableUriPermission val takeFlags Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION // 如果需要写权限 contentResolver.takePersistableUriPermission(it, takeFlags) } } fun openFilePicker() { // 可以指定MIME类型数组来过滤文件类型 val mimeTypes arrayOf(image/*, application/pdf) openDocumentLauncher.launch(mimeTypes) }场景二让用户创建一个新文件并保存private val createDocumentLauncher registerForActivityResult( ActivityResultContracts.CreateDocument(application/pdf) // 默认文件名和类型 ) { uri: Uri? - uri?.let { // 用户指定了保存的位置和文件名uri指向新文件 contentResolver.openOutputStream(it)?.use { outputStream - // 将你的数据写入outputStream val data Hello, Android 11!.toByteArray() outputStream.write(data) } // 同样可以获取持久化权限 val takeFlags Intent.FLAG_GRANT_WRITE_URI_PERMISSION contentResolver.takePersistableUriPermission(it, takeFlags) } } fun saveFile() { createDocumentLauncher.launch(MyDocument.pdf) // 建议的文件名 }提示通过SAF获取的Uri权限在设备重启后可能依然有效如果获取了持久化权限这是访问用户指定文件的推荐方式。它完美体现了“用户控制”的原则。3.3 申请所有文件访问特殊权限MANAGE_EXTERNAL_STORAGE这个权限是最后的“杀手锏”等同于旧时代的读写权限。申请此权限后应用可以通过Environment.getExternalStorageDirectory()这类API访问所有共享存储文件。第一步声明权限uses-permission android:nameandroid.permission.MANAGE_EXTERNAL_STORAGE /第二步跳转至系统设置页在Android 11你不能直接动态请求这个权限。需要引导用户到应用信息设置页手动开启。fun checkAndRequestManageStoragePermission() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.R) { if (Environment.isExternalStorageManager()) { // 已经拥有权限 accessAllFiles() } else { // 引导用户去设置页面开启 val intent Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) intent.data Uri.parse(package: packageName) startActivity(intent) } } else { // Android 10及以下使用旧的运行时权限申请 requestStoragePermissionLegacy() } }重要警告上架Google Play Store时使用此权限必须填写权限声明表详细说明为何你的应用必须使用此权限。普通的数据备份、缓存等功能不足以成为理由。审核非常严格务必谨慎使用。4. 文件操作实战MediaStore API详解既然直接路径访问行不通了那么如何在拥有相应权限或通过SAF获得Uri后实际进行文件操作呢MediaStoreAPI是关键。4.1 查询媒体文件假设我们已经获得了READ_EXTERNAL_STORAGE权限现在要查询设备上的所有图片。fun loadImagesFromMediaStore() { val projection arrayOf( MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DATE_TAKEN, MediaStore.Images.Media.SIZE ) // 按拍摄日期倒序排列 val sortOrder ${MediaStore.Images.Media.DATE_TAKEN} DESC contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, // 查询条件例如只查某文件夹在分区存储下无法直接按路径过滤。 null, sortOrder )?.use { cursor - val idColumn cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val nameColumn cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME) val dateColumn cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN) while (cursor.moveToNext()) { val id cursor.getLong(idColumn) val name cursor.getString(nameColumn) val dateTaken Date(cursor.getLong(dateColumn)) // 获取图片的Uri格式content://media/external/images/media/{id} val contentUri ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id ) // 现在你可以用这个Uri来加载图片例如使用Glide: Glide.with(this).load(contentUri).into(imageView) Log.d(MediaStore, Image: $name, Uri: $contentUri) } } }4.2 向公共目录保存媒体文件如果你想将应用生成的一张图片保存到公共的Pictures目录让其他应用也能看到。fun saveImageToPublicPictures(bitmap: Bitmap, filename: String): Uri? { val contentValues ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, filename) put(MediaStore.Images.Media.MIME_TYPE, image/jpeg) // 指定相对路径系统会决定具体存储位置 put(MediaStore.Images.Media.RELATIVE_PATH, Pictures/MyApp) // 对于Android Q及以上IS_PENDING标志很有用 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { put(MediaStore.Images.Media.IS_PENDING, 1) } } val resolver applicationContext.contentResolver val collection MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) val imageUri resolver.insert(collection, contentValues) imageUri?.let { uri - resolver.openOutputStream(uri)?.use { outputStream - // 将Bitmap压缩为JPEG写入 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) } // 写入完成后将IS_PENDING置为0使文件对其它应用可见 if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { contentValues.clear() contentValues.put(MediaStore.Images.Media.IS_PENDING, 0) resolver.update(uri, contentValues, null, null) } return uri } return null }关键点解析RELATIVE_PATH: 这是一个相对于标准公共目录如Pictures,Movies,Downloads等的路径。你不需要知道绝对路径系统会帮你管理。这比硬编码/sdcard/Pictures/MyApp要安全可靠得多。IS_PENDING: 这是一个非常实用的标志。在Android Q及以上插入一条MediaStore记录后文件可能不会立即对其他应用可见。设置IS_PENDING为1表示文件正在写入中写入完成后再设为0可以避免其他应用读到不完整的文件。4.3 操作非媒体文件如PDF、文本文件对于Downloads目录或其他文档流程类似但使用的Uri和MediaStore集合不同。fun saveTextFileToDownloads(content: String, filename: String) { val resolver applicationContext.contentResolver val contentValues ContentValues().apply { put(MediaStore.Downloads.DISPLAY_NAME, filename) put(MediaStore.Downloads.MIME_TYPE, text/plain) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS /MyAppNotes) } } val uri resolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, contentValues) uri?.let { resolver.openOutputStream(it)?.use { outputStream - outputStream.write(content.toByteArray()) } Toast.makeText(this, 文件已保存至下载目录, Toast.LENGTH_SHORT).show() } }注意从Android 11开始即使应用拥有WRITE_EXTERNAL_STORAGE权限也无法再通过FileAPI在Downloads等共享目录下直接创建文件。必须通过MediaStoreAPI插入一条记录让系统为你创建文件并返回一个Uri。5. 兼容性处理与常见问题排查5.1 如何兼容Android 10及以下版本一个健壮的应用需要处理好不同版本系统的差异。通常采用条件判断的方式。策略版本分叉fun saveFileCompat(bitmap: Bitmap, filename: String) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 (API 29) 及以上使用MediaStore API saveImageViaMediaStore(bitmap, filename) } else { // Android 9 (API 28) 及以下使用旧的文件路径API但需要动态申请WRITE_EXTERNAL_STORAGE权限 if (checkWritePermission()) { saveImageViaFilePath(bitmap, filename) } else { requestWritePermission() } } } SuppressLint(ObsoleteSdkInt) RequiresApi(Build.VERSION_CODES.Q) private fun saveImageViaMediaStore(bitmap: Bitmap, filename: String) { // 使用前面介绍的MediaStore插入方法 } private fun saveImageViaFilePath(bitmap: Bitmap, filename: String) { // 旧方法获取外部存储公共目录路径 val picturesDir Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) val appDir File(picturesDir, MyLegacyApp) if (!appDir.exists()) { appDir.mkdirs() } val imageFile File(appDir, filename) FileOutputStream(imageFile).use { fos - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) fos.flush() } // 通知系统媒体库扫描新文件旧系统需要 MediaScannerConnection.scanFile( this, arrayOf(imageFile.absolutePath), arrayOf(image/jpeg), null ) }关于requestLegacyExternalStorage在targetSdkVersion29的应用中可以在AndroidManifest.xml中设置android:requestLegacyExternalStoragetrue来在Android 10设备上临时禁用分区存储沿用旧行为为迁移争取时间。但在Android 11API 30及以上这个标志完全无效分区存储强制开启。因此它只是一个临时的兼容性手段并非长久之计。5.2 常见问题排查实录问题1应用在Android 11上拥有READ_EXTERNAL_STORAGE权限但调用File.listFiles()返回空或没有权限。原因与解决这是最典型的误区。在Android 11上此权限不再授予你通过文件路径直接访问共享存储的能力。你必须改用MediaStoreAPI来查询文件。FileAPI只能用于访问应用专属目录和通过SAF获得持久化权限的特定文件Uri所对应的路径通过DocumentFile.fromTreeUri或File的某些方法转换但并非所有Uri都支持。问题2通过MediaStore插入文件成功但在系统的“文件”应用或相册里看不到。排查步骤检查RELATIVE_PATH确保你使用的RELATIVE_PATH是标准的公共目录如Environment.DIRECTORY_PICTURES而不是任意路径。检查IS_PENDING标志Android Q如果你在插入时设置了IS_PENDING为1必须在文件内容完全写入后将其更新为0。否则文件对其他应用是隐藏的。检查Uri和集合确保你插入到了正确的MediaStore集合中例如图片应该插入到MediaStore.Images.Media.EXTERNAL_CONTENT_URI。系统媒体库扫描延迟有时系统媒体扫描服务有延迟。你可以尝试发送一个广播对于旧版本或使用MediaScannerConnection来触发扫描但在Android 10以上通过MediaStoreAPI插入的文件通常会自动通知系统。问题3通过SAF选择文件后应用重启后无法再次访问该Uri。解决你需要调用takePersistableUriPermission来获取对该Uri的持久化访问权限。并且在应用启动时可以通过getPersistedUriPermissions()来检查并恢复之前已获取的权限重新获得访问权。问题4应用需要访问设备上的所有文件进行清理/管理但申请MANAGE_EXTERNAL_STORAGE被Google Play拒绝。建议重新评估你的应用是否真的需要如此广泛的访问权限。很多清理功能可以通过访问缓存目录(getExternalCacheDir)、应用专属目录来实现。对于真正的文件管理需求可以专注于使用SAF让用户自主选择文件夹使用ACTION_OPEN_DOCUMENT_TREE然后通过DocumentFileAPI来管理用户授权的那个目录及其子目录这不需要特殊权限也符合Google的政策。问题5第三方库如图片选择库、文件下载库在Android 11上崩溃。解决这是依赖库未适配分区存储导致的。你需要升级该库到已适配Android 11的最新版本。如果库已停止维护考虑寻找替代库如使用官方推荐的PhotoPickerAPI。在AndroidManifest.xml中对特定的activity或provider添加android:requestLegacyExternalStoragetrue可能能缓解问题仅对targetSdk29且在Android 10上运行有效但这只是权宜之计。最终需要推动库作者适配或更换库。适配Android 11的文件权限是一个从“野蛮生长”到“精耕细作”的过程。初期会觉得束手束脚但一旦习惯了通过MediaStore和SAF来操作文件你会发现这套机制在安全性和用户体验上其实更优。关键在于转变思维从“我要访问存储”变为“用户允许我访问什么”。把文件选择权交给用户应用只处理被明确授权的资源这才是移动应用隐私保护的未来方向。在实际开发中我建议尽早将targetSdkVersion升级到30或更高并全面测试文件相关功能利用Android 11的行为变更来倒逼应用架构的现代化改进。