Jetpack Compose加载图库图片异常:Android10设备部分图片无法显示
问题描述
我是Android开发新手,正在学习Jetpack Compose,遇到一个问题:
- 编写的图库图片选择加载代码在Pixel 2 API 28模拟器上运行正常
- 在LG K22(Android 10)设备上,从/Pictures/Viber文件夹选200KB左右的图片能正常显示;但从Download、DCIM等文件夹选3MB左右的图片时,仅显示空白区域,上传到Firebase Storage后显示全黑
- 将可正常加载的图片复制到Download目录后,从应用加载依然成功,目前仅发现图片大小存在差异
现有代码
LoadImage 组件
@Composable fun LoadImage( bitmap: MutableState<Bitmap?> ) { var imageUri by remember { mutableStateOf<Uri?>(null) } val context = LocalContext.current val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> imageUri = uri } Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(6.dp) ) { Button(onClick = { launcher.launch("image/*") }) { Text(text = "Izaberi sliku") } } Log.d("DEBUG", "IMAGE URI $imageUri") imageUri?.let { if (Build.VERSION.SDK_INT < 28) { bitmap.value = MediaStore.Images .Media.getBitmap(context.contentResolver, it) } else { val source = ImageDecoder .createSource(context.contentResolver, it) bitmap.value = ImageDecoder.decodeBitmap(source) } } }
ShowImage 调用代码
@Composable fun ShowImage( member: Member, snackScope: CoroutineScope, snackHostState: SnackbarHostState, ) { val bitmap = remember { mutableStateOf<Bitmap?>(null) } if (bitmap.value != null) { Image( painter = rememberAsyncImagePainter(bitmap.value), contentDescription = null, modifier = Modifier.size(400.dp) ) } Row( modifier = Modifier .padding(12.dp), verticalAlignment = Alignment.CenterVertically ) { LoadImage(bitmap = bitmap) } // ... 其他代码 }
问题原因
- 内存不足导致解码失败:Android 10设备的可用内存通常比模拟器少,3MB的图片解码成Bitmap后,内存占用会远大于原文件大小(比如1080P的JPG解码后可能占用3-4MB以上,高分辨率图片会翻倍),直接加载会触发内存回收,甚至中断解码过程,最终得到空白或损坏的Bitmap。
- 同步解码阻塞主线程:在Composable中直接同步执行Bitmap解码,会阻塞UI线程,系统可能终止耗时的解码操作,或导致UI更新不及时,表现为图片加载空白。
- 无采样压缩处理:原代码直接解码原图,没有根据显示尺寸(400dp)对图片进行采样压缩,造成不必要的内存浪费。
可行解决方案
方案一:使用Coil图片加载库(推荐)
Coil是Jetpack Compose友好的图片加载库,自动处理异步加载、内存缓存、采样压缩、内存管理,无需手动处理Bitmap。
步骤:
- 添加依赖(app/build.gradle.kts):
implementation("io.coil-kt:coil-compose:2.5.0")
- 修改代码:
@Composable fun LoadImage(onImageSelected: (Uri?) -> Unit) { val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> onImageSelected(uri) } Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(6.dp) ) { Button(onClick = { launcher.launch("image/*") }) { Text(text = "Izaberi sliku") } } } @Composable fun ShowImage( member: Member, snackScope: CoroutineScope, snackHostState: SnackbarHostState, ) { var selectedImageUri by remember { mutableStateOf<Uri?>(null) } selectedImageUri?.let { uri -> AsyncImage( model = uri, contentDescription = "Selected image", modifier = Modifier.size(400.dp), contentScale = ContentScale.Fit, // 可选:添加占位图和错误图 // placeholder = painterResource(R.drawable.placeholder), // error = painterResource(R.drawable.error) ) } Row( modifier = Modifier .padding(12.dp), verticalAlignment = Alignment.CenterVertically ) { LoadImage(onImageSelected = { selectedImageUri = it }) } // ... 其他代码 }
方案二:手动实现异步采样压缩(适合学习)
如果想手动处理,需要在后台线程解码图片,并根据显示尺寸进行采样:
@Composable fun LoadImage(bitmap: MutableState<Bitmap?>) { var imageUri by remember { mutableStateOf<Uri?>(null) } val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val launcher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri: Uri? -> imageUri = uri } Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(6.dp) ) { Button(onClick = { launcher.launch("image/*") }) { Text(text = "Izaberi sliku") } } imageUri?.let { uri -> LaunchedEffect(uri) { coroutineScope.launch(Dispatchers.IO) { val targetSize = with(context.resources) { 400.dp.toPx().toInt() } val sampledBitmap = decodeSampledBitmapFromUri(context, uri, targetSize, targetSize) withContext(Dispatchers.Main) { bitmap.value = sampledBitmap } } } } } // 采样压缩工具方法 private fun decodeSampledBitmapFromUri( context: Context, uri: Uri, reqWidth: Int, reqHeight: Int ): Bitmap? { return context.contentResolver.openInputStream(uri)?.use { inputStream -> val options = BitmapFactory.Options().apply { inJustDecodeBounds = true BitmapFactory.decodeStream(inputStream, null, this) inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight) inJustDecodeBounds = false } context.contentResolver.openInputStream(uri)?.use { stream -> BitmapFactory.decodeStream(stream, null, options) } } } private fun calculateInSampleSize( options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int ): Int { val height = options.outHeight val width = options.outWidth var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { val halfHeight = height / 2 val halfWidth = width / 2 while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) { inSampleSize *= 2 } } return inSampleSize }
额外注意事项
- 上传Firebase Storage时,直接上传Uri对应的文件流(用
contentResolver.openInputStream(uri)获取),不要上传解码后的Bitmap,避免体积增大和出错。 - 所有IO/解码操作必须放在后台线程,避免阻塞UI。
内容的提问来源于stack exchange,提问作者vm381
相关产品推荐
相关产品推荐

