Android Bitmap.compress 方法返回 false 的一個(gè)可能原因(jpg文件編碼的分辨率限制)

前言

最近在解決一個(gè)遺留已久的BUG時(shí),發(fā)現(xiàn)調(diào)用 Bitmap 的 compress 方法將 bitmap 導(dǎo)出到文件流時(shí),如果導(dǎo)出的 bitmap 特別大且導(dǎo)出編碼為 Bitmap.CompressFormat.JPEG 的話該方法會(huì)直接返回 false 而沒(méi)有拋出任何錯(cuò)誤。
而對(duì)于同一個(gè) bitmap ,改用 Bitmap.CompressFormat.PNG 就不會(huì)返回 false 而是能正常導(dǎo)出。

原因與解決方法

懶得看分析過(guò)程的可以直接看這里:
經(jīng)過(guò)我的分析,導(dǎo)致 compress 方法返回 false 的原因是 jpg 編碼格式對(duì)于分辨率有最大限制。
谷歌得到的這個(gè)最大限制為:
655,35 X 655,35
但是我使用模擬器和真機(jī)實(shí)際測(cè)試最大尺寸為:
655,00 X 163,93

需要注意的是:
1.上述數(shù)值不區(qū)分寬和高,也就是說(shuō)兩個(gè)值可以互換。
2.上述分辨率尺寸是我使用模擬器(Android 11.0 arm64-v8a)和真機(jī)(小米10u,MIUI12.5.3 ,Android 11)測(cè)試得到的,可能不同系統(tǒng)版本,不同手機(jī)的限制不同,因?yàn)槭诸^設(shè)備有限,無(wú)法一一測(cè)試,網(wǎng)上也沒(méi)有足夠的資料,所以使用時(shí)最好自己實(shí)際測(cè)試一下。

注意

以上只是導(dǎo)致返回 false 的原因之一,實(shí)際原因還有很多,請(qǐng)結(jié)合實(shí)際情況自行判斷。

分析過(guò)程

目前已知的情況是:

  1. 該方法除了返回了 false 外,沒(méi)有其他任何錯(cuò)誤拋出,也沒(méi)有其他任何日志可以供參考。
  2. 已知會(huì)返回 false 的情況是:第一個(gè)參數(shù)也就是圖片編碼為 Bitmap.CompressFormat.JPEG 且 bitmap 特別大。
  3. 如果將圖片編碼改為 Bitmap.CompressFormat.PNG 則不會(huì)返回 false。

當(dāng)我遇到這個(gè)BUG的時(shí)候,結(jié)合上述已知情況,我首先想到的是要追蹤 compress 方法的實(shí)現(xiàn)方式,試圖從源碼中找到造成這個(gè)錯(cuò)誤的原因。
compress 方法的源碼如下:

    @WorkerThread
    public boolean compress(CompressFormat format, int quality, OutputStream stream) {
        checkRecycled("Can't compress a recycled bitmap");
        // do explicit check before calling the native method
        if (stream == null) {
            throw new NullPointerException();
        }
        if (quality < 0 || quality > 100) {
            throw new IllegalArgumentException("quality must be 0..100");
        }
        StrictMode.noteSlowCall("Compression of a bitmap is slow");
        Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
        boolean result = nativeCompress(mNativePtr, format.nativeInt,
                quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
        Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        return result;
    }

可以看到,改方法只是做了一些簡(jiǎn)單的判斷,其核心調(diào)用了 JNI 代碼。
所以追蹤到C++源碼如下:
(源碼來(lái)自:Android圖片編碼機(jī)制深度解析(Bitmap,Skia,libJpeg)

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
                            int format, int quality,
                            jobject jstream, jbyteArray jstorage) {
    SkImageEncoder::Type fm;  //創(chuàng)建類型變量
    //將java層類型變量轉(zhuǎn)換成Skia的類型變量
    switch (format) {
    case kJPEG_JavaEncodeFormat:
        fm = SkImageEncoder::kJPEG_Type;
        break;
    case kPNG_JavaEncodeFormat:
        fm = SkImageEncoder::kPNG_Type;
        break;
    case kWEBP_JavaEncodeFormat:
        fm = SkImageEncoder::kWEBP_Type;
        break;
    default:
        return false;
    }
    //判斷當(dāng)前bitmap指針是否為空
    bool success = false;
    if (NULL != bitmap) {
        SkAutoLockPixels alp(*bitmap);

        if (NULL == bitmap->getPixels()) {
            return false;
        }

    //創(chuàng)建SkWStream變量用于將壓縮后的圖片數(shù)據(jù)輸出
        SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
        if (NULL == strm) {
            return false;
        }
    //根據(jù)編碼類型,創(chuàng)建SkImageEncoder變量,并調(diào)用encodeStream對(duì)bitmap
    //指針指向的圖片數(shù)據(jù)進(jìn)行編碼,完成后釋放資源。
        SkImageEncoder* encoder = SkImageEncoder::Create(fm);
        if (NULL != encoder) {
            success = encoder->encodeStream(strm, *bitmap, quality);
            delete encoder;
        }
        delete strm;
    }
    return success;
}

從上述源碼可以看出,可能返回 false 的地方有:

  1. 編碼格式不存在
  2. bitmap 為空
  3. SkWStream 創(chuàng)建失敗
  4. 最后是調(diào)用的 encodeStream 返回 false

經(jīng)過(guò)我的一一確認(rèn),1-3點(diǎn)是沒(méi)有問(wèn)題的,所以最后只剩下了第4點(diǎn),但是第4點(diǎn)又是調(diào)用了另外一個(gè)很復(fù)雜的庫(kù),實(shí)在是無(wú)心去查看。
于是我轉(zhuǎn)變思路,既然會(huì)導(dǎo)致這個(gè)問(wèn)題出現(xiàn)的原因有兩個(gè),就是編碼為JPG時(shí)且bitmap特別大時(shí),那會(huì)不會(huì)是內(nèi)存溢出呢?
雖然正常來(lái)說(shuō),內(nèi)存溢出會(huì)拋出OOM錯(cuò)誤(事實(shí)上,如果我手動(dòng)把bitmap設(shè)置的特別大,也會(huì)拋出OOM),但是我們不妨試一下,看看兩者之間有何聯(lián)系。
測(cè)試代碼如下:

package com.example.myapplication

import android.graphics.Bitmap
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Environment
import android.util.Log
import android.widget.Button
import java.io.File
import java.io.FileOutputStream


private const val TAG = "el, in Main"

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        startCompress()
    }

    private fun startCompress() {
        val mainBtn = findViewById<Button>(R.id.main_btn)
        mainBtn.setOnClickListener {
            //val width =  65500
            //val height = 16393
            val height =  65500
            val width = 16393

            val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.RGB_565)

            val savePath = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES).toString())

            saveBitmap2File(bitmap, "test", savePath, 50)
        }
    }

    private fun saveBitmap2File(
        bitmap: Bitmap,
        fileName: String,
        savePath: File?,
        quality: Int): File {

        val f = File(savePath, "$fileName.jpg")
        val imgFormat: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG
        if (!f.createNewFile()) {
            Log.w(TAG, "file " + f + "has already exist")
        }

        val outputStream = FileOutputStream(f)

        //Log.i(TAG, "saveBitmap2File: bitmap's width="+bitmap.getWidth()+" height="+bitmap.getHeight());
        if (!bitmap.compress(imgFormat, quality, outputStream)) {
            Log.e(TAG, "saveBitmap2File: write bitmap to file fail!")
            throw Exception("saveBitmap2File: write bitmap to file fail!")
        }

        try {
            outputStream.flush()
            outputStream.close()
        } catch (e: Exception) {
            Log.e(TAG, "saveBitmap2File: ", e)
        }
        return f
    }
}

通過(guò)使用上述代碼,我不停的測(cè)試到底分辨率達(dá)到多少時(shí),會(huì)返回 false ,終于,測(cè)出來(lái)達(dá)到 655,00 X 163,93 能夠剛好不返回 false。
至此,可以確定,之所以會(huì)返回 false 確實(shí)和分辨率有關(guān)。
至于為什么會(huì)有限制以及為什么是這個(gè)尺寸,剛興趣的可以去了解一下 jpg 編碼的實(shí)現(xiàn),以及研究一下 libjpeg 的源碼,我水平有限,就不深究了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、Drawable與Bitmap對(duì)比 1.1、定義對(duì)比 Bitmap和Drawable定義時(shí)注定不是一個(gè)東西。B...
    code希必地閱讀 1,336評(píng)論 0 1
  • android bitmap compress android的照相功能隨著手機(jī)硬件的發(fā)展,變得越來(lái)越強(qiáng)大,能夠找...
    HelloWorld丶小工匠閱讀 1,131評(píng)論 0 0
  • 目錄介紹 01.如何計(jì)算Bitmap占用內(nèi)存1.1 如何計(jì)算占用內(nèi)存1.2 上面方法計(jì)算內(nèi)存對(duì)嗎1.3 一個(gè)像素占...
    楊充211閱讀 4,448評(píng)論 1 9
  • 本文已授權(quán)微信公眾號(hào) : code小生(codexiaosheng) 在微信公眾平臺(tái)原創(chuàng)首發(fā) 前言 在平時(shí)的 An...
    Smashing丶閱讀 9,510評(píng)論 9 41
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來(lái)的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過(guò)就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,895評(píng)論 2 7

友情鏈接更多精彩內(nèi)容