Android 10下Jetpack Compose读取外部存储图片失败求助:权限已获但文件不可读
Android 10下Jetpack Compose读取外部存储图片失败排查
问题背景
正在将一款大型天文应用迁移至Jetpack Compose,已完成Room数据库、OpenGL视图与渲染器、多个复杂页面等迁移工作,但卡在读取外部存储图片的基础功能上。
使用环境
- Android 10(API 29)
- AndroidManifest已添加
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>权限 - 依赖库:
implementation "com.google.accompanist:accompanist-permissions:0.29.1-alpha" implementation "io.coil-kt:coil-compose:2.2.2"
问题现象
测试Composable函数中:
- 外部存储目录可访问,目标文件存在且大小正确
- 权限检查结果为已授予
- 但
file.canRead()返回false,图片无法加载显示
测试代码
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class) @Composable fun SheetContent() { val context = LocalContext.current val readExternalMemoryState = rememberPermissionState( Manifest.permission.READ_EXTERNAL_STORAGE ) if (readExternalMemoryState.status.isGranted) { Text("Read External memory permission Granted") } else { AskPermission(permissionState = readExternalMemoryState) } val path = Environment.getExternalStorageDirectory().toString() + "/Download/" val file = File(path + "solar-system.png") val filesize = file.length() if (file.exists()) { Text("File exist with size: $filesize") when (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_EXTERNAL_STORAGE )) { PackageManager.PERMISSION_GRANTED -> { //File read logic here Text("checkSelfPermission gives status: Granted") if (file.canRead()) { Text("File can be read") Image( rememberAsyncImagePainter(file), contentDescription = "" ) } else { Text("File not readable") } } } } else { Text("Not available") } }
输出结果
Read External memory permission Granted File exist with size: 20534 checkSelfPermission gives status: Granted File not readable
解决方案
方案1:启用旧版存储访问兼容(快速解决)
Android 10引入了分区存储机制,默认限制了直接通过File路径访问外部存储文件的权限。在AndroidManifest.xml的<application>标签中添加以下属性,即可暂时禁用分区存储,恢复旧的文件访问逻辑:
<application ... android:requestLegacyExternalStorage="true"> ... </application>
添加后重新编译运行,file.canRead()应该会返回true,Coil也能正常加载图片。
方案2:适配分区存储(推荐长期方案)
如果希望遵循Android 10及以上的分区存储规范,建议通过MediaStore查询文件Uri,再用Coil加载Uri而非直接操作File:
- 编写工具函数获取Download目录下指定文件名的Uri:
fun getImageUriFromDownloads(context: Context, fileName: String): Uri? { val projection = arrayOf(MediaStore.Images.Media._ID) val selection = "${MediaStore.Images.Media.DISPLAY_NAME} = ?" val selectionArgs = arrayOf(fileName) val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC" context.contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, sortOrder )?.use { cursor -> if (cursor.moveToFirst()) { val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID) val id = cursor.getLong(idColumn) return ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id) } } return null }
- 在Compose中修改加载逻辑:
@OptIn(ExperimentalMaterialApi::class, ExperimentalPermissionsApi::class) @Composable fun SheetContent() { val context = LocalContext.current val readExternalMemoryState = rememberPermissionState( Manifest.permission.READ_EXTERNAL_STORAGE ) if (readExternalMemoryState.status.isGranted) { Text("Read External memory permission Granted") val imageUri = remember { getImageUriFromDownloads(context, "solar-system.png") } imageUri?.let { uri -> Text("File found via MediaStore") Image( painter = rememberAsyncImagePainter(uri), contentDescription = "Solar system image" ) } ?: run { Text("File not found in MediaStore") } } else { AskPermission(permissionState = readExternalMemoryState) } }
补充说明
- 分区存储是Android 10及以上的默认行为,
requestLegacyExternalStorage在Android 11(API 30)及以上会被忽略,所以长期来看推荐采用方案2适配。 - Coil支持直接加载
Uri对象,无需额外转换,适配分区存储时使用Uri加载是更合规的方式。
内容的提问来源于stack exchange,提问作者Jelle
相关产品推荐
相关产品推荐

