Android图片压缩在部分小众设备上生成全黑Bitmap的问题求助
Android图片压缩在部分小众设备上生成全黑Bitmap的问题求助
大家好,我最近碰到一个棘手的问题:我的App需要完成「拍照→压缩→上传后端」的完整流程,大部分设备上运行都没问题,但在一些小众设备(比如mPTech Hammer_Blade_3,搭载Android 10系统)上,压缩后的图片会变成全黑的,我翻来覆去检查自己的代码,却找不到问题所在,想请各位大佬帮忙看看。
先跟大家说下我的实现流程:
- 拍照部分用的是
ActivityResultContracts.TakePicture(),提前指定URI来保存拍摄的照片 - 拍照成功后调用自定义的压缩类处理图片,压缩逻辑在API 28+使用
ImageDecoder(支持HEIC/HEIF格式,同时处理EXIF旋转和降采样),API 26-27则回退到BitmapFactory.decodeFileDescriptor
下面是我的压缩类完整代码:
class ImageCompressor( private val context: Context, private val dispatcher: CoroutineDispatcher = Dispatchers.Default ) { private var calculatedInSampleSize: Int = 1 /** * Compress image represented by [sourceUri] and save it to [targetFile]. * * Decoding strategy: * - **API 28+**: uses the Android 9.0+ `ImageDecoder` API for all formats * including HEIC/HEIF, applying EXIF rotation and down-sampling in one step. * - **API 26–27**: falls back to `BitmapFactory.decodeFileDescriptor` * * @param sourceUri [Uri] to source image. * @param targetFile [File] to which compressed image will be saved. * @param compressRatio The [CompressRatio]. * @return <code>true</code> if successfully compressed, otherwise <code>false</code> */ suspend fun compress( sourceUri: Uri, targetFile: File, compressRatio: CompressRatio ): Boolean = withContext(dispatcher) { calculatedInSampleSize = 1 val contentResolver = context.contentResolver val descriptor = contentResolver.openFileDescriptor(sourceUri, "r", null) val statSize = descriptor?.statSize ?: 0L if (statSize <= 0L) { Timber.w("statSize is $statSize; falling back to query OpenableColumns") } val queriedSize = if (statSize > 0L) statSize else getFileSize(sourceUri) if (queriedSize <= 0L) { Timber.e("Original file appears to be empty or unreadable.") return@withContext false } Timber.d("Compressing file with original size=$queriedSize bytes, maxSizeBytes=${compressRatio.maxSizeBytes}, maxWidthOrHeight=${compressRatio.maxWidthOrHeight}, quality=${compressRatio.quality}") descriptor?.use { var compressedSize: Long = 0 var iterationCount = 0 do { iterationCount++ val bitmap = sourceUri.decodeBitmapCompat( contentResolver, compressRatio.maxWidthOrHeight, compressedSize == 0L ) Timber.d("Iteration $iterationCount: Decoded bitmap with inSampleSize=$calculatedInSampleSize, bitmap size=${bitmap?.width}x${bitmap?.height}") compressedSize = bitmap?.save(targetFile, compressRatio.quality) ?: 0 if (compressedSize == 0L) { Timber.e("Failed to compress image - compressed size is 0") break } Timber.d("Iteration $iterationCount: Compressed file size=$compressedSize bytes (${(compressedSize * 100.0 / queriedSize).toInt()}% of original), target max size=${compressRatio.maxSizeBytes}") if (compressedSize > compressRatio.maxSizeBytes) { Timber.d("File still too large, will try with higher compression in next iteration") } } while (compressedSize > compressRatio.maxSizeBytes) val success = compressedSize != 0L && targetFile.exists() && targetFile.length() > 0 Timber.d("Compression ${if (success) "succeeded" else "failed"} after $iterationCount iterations. Final size: $compressedSize bytes") success } == true } private fun Uri.decodeSampledBitmap( contentResolver: ContentResolver, maxSize: Int, calculateSampleSize: Boolean ): Bitmap? { val bitmapUri = this var sampledBitmap: Bitmap? = null try { BitmapFactory.Options().run { val descriptor = contentResolver.openFileDescriptor( bitmapUri, "r", null )?.fileDescriptor descriptor?.let { if (calculateSampleSize) { inJustDecodeBounds = true BitmapFactory.decodeFileDescriptor(it, null, this) calculateInSampleSize(this, maxSize) } inSampleSize = calculatedInSampleSize inJustDecodeBounds = false sampledBitmap = BitmapFactory.decodeFileDescriptor(it, null, this) } } } catch (e: Exception) { Timber.w(e, "Could not decode sampled bitmap.") } calculatedInSampleSize *= 2 // for next iteration if file still too big return sampledBitmap?.rotate(contentResolver, this@decodeSampledBitmap) } /** * Decodes this Uri into a Bitmap, down-sampling so both dimensions ≤ maxSize. * On API 28+ it uses ImageDecoder (native HEIC/HEIF support + all other formats). * On older APIs it falls back to BitmapFactory sampling. */ private fun Uri.decodeBitmapCompat( resolver: ContentResolver, maxSize: Int, calculateSampleSize: Boolean ): Bitmap? { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val src = ImageDecoder.createSource(resolver, this) try { ImageDecoder.decodeBitmap(src) { decoder, info, _ -> if (!calculateSampleSize) { val w = info.size.width val h = info.size.height val origMax = maxOf(w, h).toFloat() if (origMax > maxSize) { val ratio = origMax / maxSize decoder.setTargetSize((w / ratio).toInt(), (h / ratio).toInt()) } } } } catch (e: IOException) { Timber.w(e, "Failed to decode bitmap with ImageDecoder") // Fallback to BitmapFactory if ImageDecoder fails decodeSampledBitmap(resolver, maxSize, calculateSampleSize) } } else { decodeSampledBitmap(resolver, maxSize, calculateSampleSize) } } }
我已经做了这些排查,但还是没找到问题根源:
- 拍照生成的原始图片是正常的,没有全黑的情况,说明拍照环节没问题
- 在出问题的设备上,压缩后的文件大小是正常的,但打开就是全黑
- 我试过调整压缩质量、采样率参数,还是会出现全黑的情况
有没有大佬遇到过类似的小众设备兼容问题?或者能帮我看看代码里有没有什么容易忽略的坑?比如ImageDecoder在某些设备上的实现bug?或者EXIF旋转处理的问题?
内容来源于stack exchange
相关产品推荐
相关产品推荐

