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) } ) } } }
问题排查与修复方案
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 } } }解码函数的空值处理
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() }文件点击后的解码逻辑修正
原代码点击文件后直接将文件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() } }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
相关产品推荐
相关产品推荐

