You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)
    }
    // ... 其他代码
}
问题原因
  1. 内存不足导致解码失败:Android 10设备的可用内存通常比模拟器少,3MB的图片解码成Bitmap后,内存占用会远大于原文件大小(比如1080P的JPG解码后可能占用3-4MB以上,高分辨率图片会翻倍),直接加载会触发内存回收,甚至中断解码过程,最终得到空白或损坏的Bitmap。
  2. 同步解码阻塞主线程:在Composable中直接同步执行Bitmap解码,会阻塞UI线程,系统可能终止耗时的解码操作,或导致UI更新不及时,表现为图片加载空白。
  3. 无采样压缩处理:原代码直接解码原图,没有根据显示尺寸(400dp)对图片进行采样压缩,造成不必要的内存浪费。
可行解决方案

方案一:使用Coil图片加载库(推荐)

Coil是Jetpack Compose友好的图片加载库,自动处理异步加载、内存缓存、采样压缩、内存管理,无需手动处理Bitmap。

步骤:

  1. 添加依赖(app/build.gradle.kts):
implementation("io.coil-kt:coil-compose:2.5.0")
  1. 修改代码:
@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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 09:37:03