
1. 问题根源为什么Android 11的权限模型变了如果你是一个从Android 6.0运行时权限时代一路走过来的开发者可能会觉得权限管理已经够麻烦了。但到了Android 11API 30当你尝试用熟悉的FileOutputStream或File类去读写一个看似“公共”的目录比如Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)时突然迎面砸来一个open failed: EACCES (Permission denied)异常那种感觉就像你拿着自家房门的钥匙却打不开小区的大门。这不仅仅是“权限没申请”那么简单而是Android整个外部存储访问策略的一次根本性转向。在Android 10API 29之前应用通过申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限理论上就能访问整个外部存储即SD卡或模拟的共享存储空间。这种模式被称为“宽泛存储访问”Broad Storage Access。但从Android 10开始Google引入了“分区存储”Scoped Storage并在Android 11中将其设为强制默认行为。这个变化的核心思想是“沙盒化”和“用户数据最小化访问”。打个比方以前的应用像一个有小区门禁卡的访客进了小区外部存储后理论上可以敲任何一家的门访问任何目录。而现在Android 11把应用变成了一个只能进入自己专属公寓应用私有目录如Android/data/package_name/以及少数几个公共会客区如MediaStore管理的媒体目录的住户。你想去邻居家其他应用创建的目录或者去小区的公共储物柜非媒体类公共目录放点东西门卫系统会直接把你拦下来告诉你“权限不足”EACCES。这个EACCES错误正是系统在严格执行分区存储规则。即使你的AndroidManifest.xml里声明了WRITE_EXTERNAL_STORAGE权限并且在运行时也获得了用户授权在Android 11及以上版本这个权限的作用范围也被极大地限制了。它主要只对以下两种情况有效访问通过MediaStore API暴露的媒体文件图片、视频、音频。在应用卸载后仍需保留的文件需要用户通过系统文件选择器SAF明确授权。对于其他任何试图直接使用java.io.FileAPI去读写非应用私有目录或非MediaStore指定目录的行为系统都会抛出EACCES。这就是你遇到问题的根本原因。1.1 核心概念分区存储下的“可访问”区域要解决问题必须先搞清楚在Android 11上你的应用能“合法”地在哪里读写文件。主要分为三大区域应用私有目录这是最安全、最无阻的领域。路径通常为Context.getFilesDir()内部存储的私有文件目录。Context.getExternalFilesDir(String type)外部存储的私有文件目录。这里的“外部存储”指的是设备内置的共享存储空间但为你的应用划出的私有区域。你可以在这里创建任意类型的文件无需任何权限。应用卸载后这些文件会被清除。共享媒体集合这是通过MediaStore API访问的“公共会客区”。主要包括MediaStore.Images.Media.EXTERNAL_CONTENT_URIMediaStore.Video.Media.EXTERNAL_CONTENT_URIMediaStore.Audio.Media.EXTERNAL_CONTENT_URIMediaStore.Downloads.EXTERNAL_CONTENT_URI(Android 10) 访问这些URI对应的文件如图片、下载文件需要相应的运行时权限如READ_EXTERNAL_STORAGE但你必须使用ContentResolver的openInputStream、openOutputStream等方法或者使用MediaStore的insert、update、delete操作。直接使用文件路径File API是行不通的。其他公共目录受限访问例如Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS)。在Android 11上应用无法直接通过File API访问这些目录。写入的唯一标准方式是使用系统文件选择器Storage Access Framework, SAF让用户亲自为你指定一个位置。理解这三块区域的差异是解决所有EACCES问题的第一步。你的代码很可能正试图用错误的方式访问了一个错误的地方。2. 诊断与排查你的代码到底卡在哪一步当open failed: EACCES出现时别急着乱改AndroidManifest.xml或权限请求代码。先做一次系统的诊断定位问题根源。我通常遵循以下排查流程这能帮你节省大量时间。2.1 第一步检查目标路径与Android版本首先在抛出异常的代码附近打印出你试图访问的完整文件路径和当前的Android版本Build.VERSION.SDK_INT。val targetFile File(path) Log.e(“FileAccess”, “尝试访问路径: ${targetFile.absolutePath}”) Log.e(“FileAccess”, “当前SDK版本: ${Build.VERSION.SDK_INT}”) try { FileOutputStream(targetFile).use { … } } catch (e: IOException) { Log.e(“FileAccess”, “IOException: ${e.message}”, e) }关键分析点路径是否在应用私有目录内检查路径是否包含你的应用包名如/storage/emulated/0/Android/data/com.yourapp/。如果是那理论上不应该有EACCES问题可能出在目录创建或权限声明错误上极少数情况。路径是否指向公共媒体目录例如/storage/emulated/0/DCIM/,/storage/emulated/0/Download/。如果是并且SDK_INT 30那么直接使用File API就是问题的根源。SDK版本是否30只有Android 11及以上分区存储才强制生效。如果你的targetSdkVersion设置为29或以下应用可以暂时绕过分区存储但Google Play有上架要求。确认这一点能明确问题边界。2.2 第二步检查AndroidManifest.xml中的权限与特性声明这是最容易被忽略或配置错误的地方。打开你的AndroidManifest.xml文件检查以下内容是否声明了requestLegacyExternalStorage对于targetSdkVersion 29的应用可以通过在application标签内添加android:requestLegacyExternalStorage”true”来临时禁用分区存储。但在targetSdkVersion 30时这个标志在大多数新安装的应用上无效对升级应用有临时豁免。不要依赖它作为Android 11的解决方案。权限声明是否正确访问共享媒体文件读uses-permission android:name”android.permission.READ_EXTERNAL_STORAGE” /访问共享媒体文件写在Android 10及以下需要WRITE_EXTERNAL_STORAGE在Android 11及以上对于MediaStore管理的图片、视频、音频文件写入自身创建的文件通常不需要此权限但写入Downloads集合可能需要MANAGE_EXTERNAL_STORAGE这个权限审核严格慎用。特别注意READ_EXTERNAL_STORAGE在Android 11上只影响通过MediaStore对媒体文件的读取。它不授予对任意文件路径的访问权。是否错误使用了MANAGE_EXTERNAL_STORAGE这是一个“核弹级”权限允许应用访问所有文件除少数系统目录。使用它需要上架Google Play时声明并可能面临审核且用户在设置中手动开启。除非你是文件管理器、备份或反病毒应用否则绝对不要使用它。如果你的应用声明了此权限很可能是过度申请并且可能触发EACCES因为用户没在设置里开启。2.3 第三步检查运行时权限请求与授予状态即使声明了权限用户也可能拒绝。你需要动态检查。// 检查读取媒体文件权限 val readPermissionGranted ContextCompat.checkSelfPermission( this, Manifest.permission.READ_EXTERNAL_STORAGE ) PackageManager.PERMISSION_GRANTED // 对于Android 10及以下检查写入权限 val writePermissionGranted if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ) PackageManager.PERMISSION_GRANTED } else { // Android 11写入媒体文件到MediaStore通常不需要WRITE权限 true } if (!readPermissionGranted) { // 申请READ_EXTERNAL_STORAGE权限 ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), REQUEST_CODE_READ_PERM ) }重要提示在Android 11上即使用户授予了READ_EXTERNAL_STORAGE权限你的应用也只能通过MediaStore API访问媒体文件。直接File(«/storage/emulated/0/Download/myfile.pdf»).exists()依然会返回false或抛出异常。权限授予不等于路径访问畅通。完成以上三步诊断你就能清晰地知道你的应用在哪个Android版本上试图用什么方法访问哪个特定区域以及当前具备什么样的权限状态。这为选择正确的解决方案提供了精确的地图。3. 解决方案针对不同场景的“正确打开方式”根据诊断结果你需要为不同的文件访问需求选择对应的API和策略。以下是针对最常见场景的解决方案。3.1 场景一读写应用私有文件最推荐如果你的文件不需要与其他应用共享或者只是临时缓存强烈建议存放到应用私有目录。这是最安全、最稳定、无需权限的方式。写入私有文件示例// 写入到外部存储的私有目录优先空间可能更大 val appExternalDir getExternalFilesDir(null) // 如 /storage/emulated/0/Android/data/com.yourapp/files/ val privateFile File(appExternalDir, “my_private_data.txt”) try { FileOutputStream(privateFile).use { fos - fos.write(“Hello Private World”.toByteArray()) } Log.i(“FileAccess”, “文件写入成功: ${privateFile.absolutePath}”) } catch (e: IOException) { Log.e(“FileAccess”, “写入私有文件失败”, e) // 这里出现EACCES的可能性极低如果出现检查存储状态是否已满或为只读 }从私有文件读取示例val inputFile File(getExternalFilesDir(null), “my_private_data.txt”) try { FileInputStream(inputFile).use { fis - val content fis.readBytes().toString(Charsets.UTF_8) Log.i(“FileAccess”, “读取内容: $content”) } } catch (e: FileNotFoundException) { Log.e(“FileAccess”, “文件不存在”) } catch (e: IOException) { Log.e(“FileAccess”, “读取失败”, e) }实操心得getExternalFilesDir(String type)中的type参数可以是Environment.DIRECTORY_PICTURES等系统会在你的私有目录下创建对应的子文件夹便于管理。应用卸载时这些文件会被自动清理。如果数据需要永久保留考虑备份到云端或使用下一节的MediaStore方式。3.2 场景二访问或保存媒体文件图片、视频、音频、下载文件这是分区存储改革后访问“公共”内容的标准方式。你必须和MediaStore打交道。保存一张图片到公共相册DCIM// 1. 准备图片内容这里用Bitmap示例 val bitmap: Bitmap … // 你的图片Bitmap val contentValues ContentValues().apply { put(MediaStore.Images.Media.DISPLAY_NAME, “my_photo_${System.currentTimeMillis()}.jpg”) put(MediaStore.Images.Media.MIME_TYPE, “image/jpeg”) if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 可以指定相对路径 put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_DCIM “/MyAppPhotos”) } } // 2. 使用ContentResolver插入到MediaStore val resolver contentResolver var imageUri: Uri? null try { imageUri resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) if (imageUri ! null) { resolver.openOutputStream(imageUri)?.use { outputStream - // 3. 将Bitmap写入到ContentResolver提供的OutputStream中 bitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream) } Log.i(“FileAccess”, “图片已保存至MediaStore: $imageUri”) // 注意你无法直接得到一个稳定的文件路径。如果需要路径可以通过Uri查询但不要依赖它进行File操作。 } } catch (e: Exception) { Log.e(“FileAccess”, “保存图片到MediaStore失败”, e) // 如果用户拒绝权限或存储空间不足可能会在这里抛出异常 }从MediaStore读取一张图片// 假设你有一个MediaStore中图片的Uri val imageUri: Uri … // 例如从相册选择器返回的Uri try { contentResolver.openInputStream(imageUri)?.use { inputStream - val bitmap BitmapFactory.decodeStream(inputStream) // 使用这个bitmap } } catch (e: SecurityException) { // 可能没有READ_EXTERNAL_STORAGE权限 Log.e(“FileAccess”, “读取权限不足”, e) } catch (e: IOException) { Log.e(“FileAccess”, “读取文件流失败”, e) }关键点解析不再需要文件路径整个过程你都不需要知道文件在/storage/emulated/0/DCIM/…的具体路径。你操作的是Uri。这是思维上最大的转变。权限写入MediaStore如上例保存图片在Android 10及以上通常不需要WRITE_EXTERNAL_STORAGE权限。读取则需要READ_EXTERNAL_STORAGE权限Android 11仅对媒体文件有效。Downloads集合对于下载文件使用MediaStore.Downloads.EXTERNAL_CONTENT_URI。但请注意向Downloads集合写入文件在Android 11可能需要MANAGE_EXTERNAL_STORAGE权限除非使用SAF。3.3 场景三访问非媒体类公共文件或特定目录使用SAF当你的应用需要让用户选择任意位置保存一个文档如PDF、TXT或者打开一个其他应用创建的非媒体文件时系统文件选择器Storage Access Framework, SAF是唯一的标准答案。启动SAF让用户选择文件保存位置val intent Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type “application/pdf” // 指定MIME类型例如PDF putExtra(Intent.EXTRA_TITLE, “my_document.pdf”) // 建议的文件名 // 可选指定初始目录Android 5.0 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { putExtra(DocumentsContract.EXTRA_INITIAL_URI, …) // 一个Document Uri } } startActivityForResult(intent, REQUEST_CODE_CREATE_DOC)在onActivityResult中处理用户选择override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode REQUEST_CODE_CREATE_DOC resultCode Activity.RESULT_OK) { data?.data?.let { uri - // 用户选择了一个位置uri指向该文件可能尚未存在 try { contentResolver.openOutputStream(uri)?.use { outputStream - // 将你的文件内容写入这个OutputStream outputStream.write(“PDF content here…”.toByteArray()) } // 保存成功这个uri是长期有效的你可以持久化存储这个uri以便下次访问。 persistUriPermission(uri) // 重要尝试持久化访问权限见下文 } catch (e: Exception) { Log.e(“FileAccess”, “写入用户选择的位置失败”, e) } } } }持久化URI权限 SAF返回的Uri可能带有临时访问权限。为了在应用重启后还能访问这个文件你需要“持久化”这个权限。private fun persistUriPermission(uri: Uri) { val takeFlags Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION contentResolver.takePersistableUriPermission(uri, takeFlags) // 之后你可以将uri.toString()保存到SharedPreferences或数据库中。 }后续通过保存的Uri字符串重新访问文件val savedUriString … // 从持久化存储中读取 val uri Uri.parse(savedUriString) try { contentResolver.openInputStream(uri)?.use { … } } catch (e: SecurityException) { // 权限已丢失例如用户清除了数据需要重新通过SAF请求 Log.e(“FileAccess”, “URI权限已失效需要重新请求用户选择”) }SAF的核心优势与注意事项用户完全掌控用户决定文件存到哪里安全透明。访问任意位置包括SD卡、云存储提供商如Google Drive如果安装了相关插件等。无需敏感权限应用不需要申请MANAGE_EXTERNAL_STORAGE这种宽泛权限。缺点流程依赖用户交互无法在后台静默操作。URI的持久化访问可能因用户操作如在系统设置中清除权限而失效。4. 高级策略与兼容性处理在实际项目中你往往需要支持广泛的Android版本例如从Android 5.0到Android 13。这就需要一套兼容性策略在不同版本上采用不同的API。4.1 版本分发的兼容性代码结构一个健壮的文件访问工具类可能会长这样object FileAccessHelper { /** * 保存一个图片文件。根据Android版本自动选择最佳策略。 * param context 上下文 * param bitmap 要保存的位图 * param fileName 建议的文件名不含路径 * return 保存成功后文件的Uri失败返回null */ fun saveImageToPublic(context: Context, bitmap: Bitmap, fileName: String): Uri? { return if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { // Android 10 (Q) 使用MediaStore API saveImageViaMediaStore(context, bitmap, fileName) } else if (Build.VERSION.SDK_INT Build.VERSION_CODES.M) { // Android 6.0 - 9.0 (M-P) 检查权限并使用旧版File API如果targetSdkVersion 29 // 注意如果targetSdkVersion 29即使在Android 9上分区存储也可能生效取决于requestLegacyExternalStorage。 if (hasWriteStoragePermission(context)) { saveImageViaLegacyFileApi(context, bitmap, fileName) } else { // 请求权限 requestWritePermission(context) null } } else { // Android 5.0及以下直接使用File API无运行时权限 saveImageViaLegacyFileApi(context, bitmap, fileName) } } RequiresApi(Build.VERSION_CODES.Q) private fun saveImageViaMediaStore(context: Context, bitmap: Bitmap, fileName: String): Uri? { // 实现参考3.2节 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, Environment.DIRECTORY_DCIM “/MyApp”) } // … 插入并写入数据 } SuppressLint(“ObsoleteSdkInt”) private fun saveImageViaLegacyFileApi(context: Context, bitmap: Bitmap, fileName: String): Uri? { // 旧方法直接写入公共目录 val picturesDir Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) val outputFile File(picturesDir, “MyApp/$fileName”) outputFile.parentFile?.mkdirs() try { FileOutputStream(outputFile).use { fos - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos) fos.flush() } // 将File转换为Urifile:// 格式。注意在Android 7.0直接暴露file:// Uri可能引发FileUriExposedException需要使用FileProvider。 return Uri.fromFile(outputFile) } catch (e: IOException) { Log.e(“FileAccess”, “Legacy save failed”, e) return null } } // … 其他辅助方法权限检查、请求等 }4.2 关于requestLegacyExternalStorage的真相很多老文章会提到在AndroidManifest.xml中设置android:requestLegacyExternalStorage”true”来兼容。你需要了解它的局限性targetSdkVersion 29设置此标志为true可以在Android 10设备上完全禁用分区存储应用行为退回Android 9。targetSdkVersion 30对于全新安装的应用此标志被系统忽略分区存储强制开启。对于从低版本升级上来的应用系统会暂时允许此标志生效以保持兼容。但一旦用户卸载重装或应用被系统“清除数据”这个豁免就失效了。结论不要将requestLegacyExternalStorage作为Android 11的长期解决方案。它只是一个短暂的迁移辅助工具。你的代码必须为分区存储使用MediaStore/SAF做好准备。4.3 处理来自其他应用的文件ContentProvider与FileProvider你的应用除了自己创建文件还经常需要处理其他应用分享过来的文件如图片分享、文件打开。这些文件通常以content://类型的Uri传递。安全地打开一个Content Urifun openContentUriSafely(context: Context, uri: Uri) { try { // 1. 检查Uri的权限可选但推荐 context.contentResolver.query(uri, null, null, null, null)?.use { cursor - // 可以检查MIME_TYPE等信息 val mimeType cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)) } // 2. 打开输入流 context.contentResolver.openInputStream(uri)?.use { inputStream - // 读取文件内容 val bytes inputStream.readBytes() // … 处理bytes } } catch (e: SecurityException) { // 可能没有权限。对于接收到的Intent系统通常会授予临时权限。 // 如果需要持久化访问需要调用 takePersistableUriPermission (仅对Document Uri有效) Log.e(“FileAccess”, “安全异常无权限访问此Uri”, e) } catch (e: FileNotFoundException) { Log.e(“FileAccess”, “文件未找到”, e) } catch (e: IOException) { Log.e(“FileAccess”, “IO异常”, e) } }当你的应用需要向其他应用提供文件时使用FileProvider 如果你需要将一个私有文件如拍照后临时存储的图片分享给其他应用如微信直接传递file://Uri在Android 7.0以上会崩溃。必须使用FileProvider。在AndroidManifest.xml中声明FileProviderapplication … provider android:name“androidx.core.content.FileProvider” android:authorities“${applicationId}.fileprovider” android:exported“false” android:grantUriPermissions“true” meta-data android:name“android.support.FILE_PROVIDER_PATHS” android:resource“xml/file_paths” / /provider /application创建res/xml/file_paths.xml?xml version“1.0” encoding“utf-8”? paths !-- 将应用私有缓存目录共享出去 -- cache-path name“shared_cache” path“/” / !-- 将应用私有文件目录共享出去 -- files-path name“shared_files” path“/” / !-- 将外部存储私有目录共享出去 -- external-files-path name“external_files” path“/” / !-- 示例共享DCIM目录下的文件需适配分区存储 -- !-- 在Android 11上这通常无效因为你的应用可能无法直接访问该路径 -- !-- external-path name“external_storage_root” path“.” / -- /paths生成一个安全的Content Uri并分享val imageFile File(context.cacheDir, “temp_photo.jpg”) // 使用FileProvider获取Uri val contentUri: Uri FileProvider.getUriForFile( context, “${context.packageName}.fileprovider”, // 必须与Manifest中authorities一致 imageFile ) val shareIntent Intent(Intent.ACTION_SEND).apply { type “image/jpeg” putExtra(Intent.EXTRA_STREAM, contentUri) // 授予接收Intent的应用对此Uri的临时读取权限 addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } startActivity(Intent.createChooser(shareIntent, “分享图片”))这套组合拳能让你在复杂的Android文件系统中游刃有余同时保证安全性和兼容性。核心思想就是放弃对绝对路径的执念拥抱Uri和ContentResolver根据不同的Android版本和访问意图选择最合适的API通道。一开始可能会觉得绕但习惯之后你会发现这才是更清晰、更安全的文件交互模型。