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

Android Kotlin中解码文件内Base64图片字符串崩溃问题排查

Android中Kotlin+Jetpack Compose图片转Base64保存后解码崩溃问题

我在Android平台使用Kotlin结合Jetpack Compose实现图片保存功能,因为需要在文件中存储图片以外的额外数据,所以计划将图片编码为Base64字符串后写入文件。具体流程为:通过Coil从URI加载图片→转换为Drawable→转为Bitmap→生成字节数组→编码为Base64字符串。

保存Base64字符串到文件时应用未崩溃,文件名显示正常,但无法确认文件内容是否有效。点击文件尝试解码回Bitmap时应用崩溃,Logcat提示decodeByteArray方法不能为null,推测文件未写入有效内容。

以下是完整代码:

// 保存文件的目录
val directory = File(this.cacheDir, "images")
if (!directory.exists()) {
    directory.mkdirs()
}

// 目录文件列表状态
var directoryFiles by remember { mutableStateOf<List<File>>(directory.listFiles()?.toList() ?: listOf())}

// 选中图片的URI
var imageUri by remember { mutableStateOf<Uri?>(null) }

// 将URI转换为Bitmap的函数
suspend fun convertToBitmap(): Bitmap {
    val imageLoader = this.imageLoader
    val request = ImageRequest.Builder(this)
        .data(imageUri)
        .build()
    return (imageLoader.execute(request).drawable as BitmapDrawable).bitmap
}

// 将Bitmap转换为字节数组的函数
suspend fun convertToBytes(): ByteArray {
    val outputStream = ByteArrayOutputStream()
    convertToBitmap().compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
    return outputStream.toByteArray()
}

// 将字节数组编码为Base64字符串的函数
suspend fun encodeToBase64(): String {
    val bytes = convertToBytes()
    return Base64.encodeToString(bytes, Base64.URL_SAFE)
}

// 将Base64字符串解码回Bitmap的函数
fun decodeBase64(base64: String): Bitmap {
    val bytes = Base64.decode(base64, Base64.URL_SAFE)
    return BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}

// 图片选择器Launcher
val imagePickerLauncher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.PickVisualMedia(),
    onResult = {uri ->
        imageUri = uri
    }
)

// 应用布局
Column(
    modifier = Modifier
        .fillMaxSize()
        .padding(WindowInsets.systemBars.asPaddingValues()),
    horizontalAlignment = Alignment.CenterHorizontally,
    verticalArrangement = Arrangement.spacedBy(20.dp)
) {

    // 按钮行
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceEvenly
    ) {
        // 选择图片按钮
        Button(onClick = {
            imagePickerLauncher.launch(
                PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
            )
        }) { Text("选择图片") }

        // 编码并保存图片按钮
        Button(onClick = {
            CoroutineScope(Dispatchers.IO).launch {
                val file = File(directory, directoryFiles.size.toString())
                file.writeText(encodeToBase64())
                directoryFiles = directory.listFiles()?.toList() ?: listOf()
            }
        }) { Text("保存图片") }
    }

    // 显示选中/解码后的图片
    AsyncImage(
        model = imageUri,
        contentDescription = null,
        modifier = Modifier
            .background(Color.LightGray)
            .size(200.dp)
    )

    // 显示已保存文件列表
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        items(directoryFiles) { file ->
            Text(
                file.name,
                fontSize = 20.sp,
                modifier = Modifier.clickable {
                    decodeBase64(file.readText())
                    imageUri = Uri.fromFile(file)
                }
            )
        }
    }
}

问题排查与修复方案

  1. Coil加载Drawable的强转风险
    原代码中直接将Coil加载的Drawable强转为BitmapDrawable,如果加载的是矢量图或其他非Bitmap类型的Drawable,会抛出类型转换异常,导致编码失败,文件写入空内容。修改convertToBitmap函数:

    suspend fun convertToBitmap(): Bitmap {
        val imageLoader = this.imageLoader
        val request = ImageRequest.Builder(this)
            .data(imageUri)
            .allowHardware(false) // 禁用硬件位图,避免转码异常
            .build()
        val result = imageLoader.execute(request)
        val drawable = result.drawable ?: error("加载图片失败")
        
        return when (drawable) {
            is BitmapDrawable -> drawable.bitmap
            else -> {
                // 将非BitmapDrawable转为Bitmap
                val bitmap = Bitmap.createBitmap(
                    drawable.intrinsicWidth,
                    drawable.intrinsicHeight,
                    Bitmap.Config.ARGB_8888
                )
                val canvas = Canvas(bitmap)
                drawable.setBounds(0, 0, canvas.width, canvas.height)
                drawable.draw(canvas)
                bitmap
            }
        }
    }
    
  2. 解码函数的空值处理
    BitmapFactory.decodeByteArray在字节数组无效时会返回null,原代码直接返回会导致崩溃,添加空值安全处理:

    fun decodeBase64(base64: String): Bitmap? {
        return runCatching {
            val bytes = Base64.decode(base64, Base64.URL_SAFE)
            BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
        }.getOrNull()
    }
    
  3. 文件点击后的解码逻辑修正
    原代码点击文件后直接将文件Uri赋值给imageUri,但文件存储的是Base64字符串而非图片,无法直接显示。需要将解码后的Bitmap写入临时文件再生成Uri:

    modifier = Modifier.clickable {
        val bitmap = decodeBase64(file.readText())
        bitmap?.let { bmp ->
            val tempFile = File(cacheDir, "temp_decoded.jpg")
            val outputStream = FileOutputStream(tempFile)
            bmp.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
            outputStream.close()
            imageUri = Uri.fromFile(tempFile)
        } ?: run {
            Toast.makeText(context, "文件内容无效,解码失败", Toast.LENGTH_SHORT).show()
        }
    }
    
  4. State更新的线程安全
    保存文件后更新directoryFiles需要在主线程执行,避免State更新异常:

    Button(onClick = {
        CoroutineScope(Dispatchers.IO).launch {
            val file = File(directory, directoryFiles.size.toString())
            val base64Str = encodeToBase64()
            file.writeText(base64Str, Charsets.UTF_8)
            // 切换到主线程更新State
            withContext(Dispatchers.Main) {
                directoryFiles = directory.listFiles()?.toList() ?: listOf()
            }
        }
    }) { Text("保存图片") }
    

内容的提问来源于stack exchange,提问作者Bob Rasner

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 05:43:10