Android Kotlin:调用saveImage保存拍摄图片时应用崩溃求助
问题排查与解决方案
核心崩溃原因
- 主线程执行耗时IO操作:
saveImage中的图片解码、压缩保存都是耗时操作,直接在Compose重组的主线程执行,会触发ANR甚至崩溃。 - 空指针隐患:
contentResolver.insert可能返回null,此时调用imageUri.toString()会抛出空指针;getBitmap解码失败也会返回null,后续bitmap.compress会崩溃。 - Compose重复执行保存逻辑:
if (capturedImageUri.path?.isNotEmpty() == true)块在每次重组时都会执行,导致重复调用saveImage,多次写入文件引发异常。 - FileProvider配置错误:原代码中
context.packageName + "\.provider"的转义符多余,会导致authority不匹配,无法正常访问拍摄的临时文件。 - 权限覆盖不全: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
相关产品推荐
相关产品推荐

