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

Android Kotlin:调用saveImage保存拍摄图片时应用崩溃求助

问题排查与解决方案

核心崩溃原因

  1. 主线程执行耗时IO操作:saveImage中的图片解码、压缩保存都是耗时操作,直接在Compose重组的主线程执行,会触发ANR甚至崩溃。
  2. 空指针隐患:contentResolver.insert可能返回null,此时调用imageUri.toString()会抛出空指针;getBitmap解码失败也会返回null,后续bitmap.compress会崩溃。
  3. Compose重复执行保存逻辑:if (capturedImageUri.path?.isNotEmpty() == true)块在每次重组时都会执行,导致重复调用saveImage,多次写入文件引发异常。
  4. FileProvider配置错误:原代码中context.packageName + "\.provider"的转义符多余,会导致authority不匹配,无法正常访问拍摄的临时文件。
  5. 权限覆盖不全:Android 9及以下版本保存图片需要WRITE_EXTERNAL_STORAGE权限,原逻辑未处理。

分步修复方案

1. 修正FileProvider的Authority配置

原代码中转义符错误,修改为正确的authority格式,并用remember缓存临时文件和Uri,避免每次重组重复创建:

val context = LocalContext.current
val file = remember { context.createImageFile() }
val uri = remember {
    FileProvider.getUriForFile(
        Objects.requireNonNull(context),
        "${context.packageName}.provider", // 移除多余转义符
        file
    )
}

2. 将保存操作移至后台线程

使用协程在IO线程执行耗时操作,避免阻塞主线程,同时用LaunchedEffect确保只在图片Uri变化时执行一次保存:

val scope = rememberCoroutineScope()
var photoURI by remember { mutableStateOf("") }

// ... 其他代码保持不变

if (capturedImageUri.path?.isNotEmpty() == true) {
    AsyncImage(
        model = ImageRequest.Builder(context)
            .data(capturedImageUri)
            .crossfade(true)
            .build(),
        contentDescription = "image taken"
    )

    LaunchedEffect(capturedImageUri) {
        scope.launch(Dispatchers.IO) {
            try {
                val savedUri = saveImage(context.applicationContext.contentResolver, capturedImageUri)
                withContext(Dispatchers.Main) {
                    photoURI = savedUri
                }
            } catch (e: Exception) {
                e.printStackTrace()
                withContext(Dispatchers.Main) {
                    Toast.makeText(context, "保存失败: ${e.message}", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

3. 修复saveImage函数的空指针与异常处理

添加空值判断,用use自动管理输出流,适配Android 10+的存储路径:

fun saveImage(contentResolver: ContentResolver, capturedImageUri: Uri): String {
    val bitmap = getBitmap(capturedImageUri, contentResolver) ?: throw Exception("图片解码失败")

    val values = ContentValues().apply {
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}")
        // Android 10+ 指定保存到相册的自定义目录
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/MyRecipes")
        }
    }

    val imageUri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        ?: throw Exception("无法创建图片存储条目")

    contentResolver.openOutputStream(imageUri)?.use { outputStream ->
        if (!bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)) {
            throw Exception("图片压缩失败")
        }
    } ?: throw Exception("无法打开输出流")

    return imageUri.toString()
}

4. 完善getBitmap的异常捕获

private fun getBitmap(selectedPhotoUri: Uri, contentResolver: ContentResolver): Bitmap? {
    return try {
        when {
            Build.VERSION.SDK_INT < 28 -> MediaStore.Images.Media.getBitmap(contentResolver, selectedPhotoUri)
            else -> {
                val source = ImageDecoder.createSource(contentResolver, selectedPhotoUri)
                ImageDecoder.decodeBitmap(source)
            }
        }
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

5. 补充Android 9及以下的权限处理

在Manifest中添加权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28" />

修改权限申请逻辑,同时检查CAMERA和WRITE权限:

val requiredPermissions = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
    arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
} else {
    arrayOf(Manifest.permission.CAMERA)
}

val permissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
    val allGranted = permissions.all { it.value }
    if (allGranted) {
        cameraLauncher.launch(uri)
    } else {
        Toast.makeText(context, "缺少必要权限", Toast.LENGTH_SHORT).show()
    }
}

Button(
    onClick = {
        val allGranted = requiredPermissions.all {
            ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
        }
        if (allGranted) {
            cameraLauncher.launch(uri)
        } else {
            permissionLauncher.launch(requiredPermissions)
        }
    },
    // ... 其他属性保持不变
) {
    // ... 按钮内容保持不变
}

内容的提问来源于stack exchange,提问作者Martino Narducci

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 01:53:12