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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:05:28