Android Q及以上读取外部存储遇FileNotFoundException问题求助
java.io.FileNotFoundException: EACCES (Permission denied)问题 嗨,这个问题我做聊天应用文件发送功能时踩过同款坑,核心原因就是Android Q(API 29)开始强制推行的**分区存储(Scoped Storage)**机制——它彻底重构了APP访问外部存储的规则,哪怕你在Manifest里配置了权限、运行时也申请了,直接用file:// URI访问Download这类公共目录的文件还是会被拒权。
为什么旧版本正常,Q+就报错?
Android 9及以前,APP申请READ_EXTERNAL_STORAGE/WRITE_EXTERNAL_STORAGE权限后,就能直接通过文件路径访问外部存储的任意文件;但Q+的分区存储把外部存储分成了APP私有目录和公共媒体目录,公共目录的文件不能再通过file:// URI直接访问,必须通过**Content URI(content://开头)**或者存储访问框架(SAF)来操作。你现在拿到的file:///storage/emulated/0/Download/myFile.pdf这种URI,在Q+系统里是不被允许直接打开的。
具体解决方法
1. 推荐:改用系统文件选择器获取Content URI(长期合规方案)
放弃自己构造file:// URI的方式,通过调用系统自带的文件选择器让用户选择文件,这样返回的就是合规的content:// URI,直接用你现有的contentResolver.openInputStream()代码就能正常读取。
示例代码(用AndroidX的ActivityResultContracts替代旧的onActivityResult):
// 注册文件选择回调 val pickFileLauncher = registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? -> uri?.let { // 复用你原有的文件读取逻辑 context.contentResolver.openInputStream(it)?.use { inputStream -> val buffer = ByteArray(1024) var numBytesRead: Int while (true) { numBytesRead = inputStream.read(buffer) if (numBytesRead == -1) break // 处理读取到的字节数据 } } } } // 触发文件选择器,可指定MIME类型(比如只选图片用"image/*") pickFileLauncher.launch("*/*")
如果需要支持多选或持久化访问权限(用户下次打开APP无需重新选择),可以改用ActivityResultContracts.OpenDocument。
2. 临时兼容:开启Legacy存储模式(不推荐长期使用)
如果暂时没时间迁移到SAF,可以在AndroidManifest的<application>标签里添加属性,让APP在Q+系统里暂时沿用旧的存储权限规则:
<application ... android:requestLegacyExternalStorage="true"> ... </application>
注意:这个属性在Android 11(API 30)及以上会被忽略,而且Google后续会逐步淘汰该模式,仅建议作为临时过渡方案。
额外提醒
- Android 10+读取公共媒体文件,仅需申请
READ_EXTERNAL_STORAGE权限;如果通过SAF选择文件,甚至不需要申请存储权限,系统会自动授权访问用户选择的文件。 - 不要尝试把
content://URI转成file://URI,这在Q+系统里完全行不通,还会引发更复杂的权限问题。
内容的提问来源于stack exchange,提问作者james04




