Android Bitmap.compress 方法返回 false 的一个可能原因(jpg文件编码的分辨率限制)

前言

最近在解决一个遗留已久的BUG时,发现调用 Bitmap 的 compress 方法将 bitmap 导出到文件流时,如果导出的 bitmap 特别大且导出编码为 Bitmap.CompressFormat.JPEG 的话该方法会直接返回 false 而没有抛出任何错误。
而对于同一个 bitmap ,改用 Bitmap.CompressFormat.PNG 就不会返回 false 而是能正常导出。

原因与解决方法

懒得看分析过程的可以直接看这里:
经过我的分析,导致 compress 方法返回 false 的原因是 jpg 编码格式对于分辨率有最大限制。
谷歌得到的这个最大限制为:
655,35 X 655,35
但是我使用模拟器和真机实际测试最大尺寸为:
655,00 X 163,93

需要注意的是:
1.上述数值不区分宽和高,也就是说两个值可以互换。
2.上述分辨率尺寸是我使用模拟器(Android 11.0 arm64-v8a)和真机(小米10u,MIUI12.5.3 ,Android 11)测试得到的,可能不同系统版本,不同手机的限制不同,因为手头设备有限,无法一一测试,网上也没有足够的资料,所以使用时最好自己实际测试一下。

注意

以上只是导致返回 false 的原因之一,实际原因还有很多,请结合实际情况自行判断。

分析过程

目前已知的情况是:

  1. 该方法除了返回了 false 外,没有其他任何错误抛出,也没有其他任何日志可以供参考。
  2. 已知会返回 false 的情况是:第一个参数也就是图片编码为 Bitmap.CompressFormat.JPEG 且 bitmap 特别大。
  3. 如果将图片编码改为 Bitmap.CompressFormat.PNG 则不会返回 false。

当我遇到这个BUG的时候,结合上述已知情况,我首先想到的是要追踪 compress 方法的实现方式,试图从源码中找到造成这个错误的原因。
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;
    }

可以看到,改方法只是做了一些简单的判断,其核心调用了 JNI 代码。
所以追踪到C++源码如下:
(源码来自:Android图片编码机制深度解析(Bitmap,Skia,libJpeg)

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
                            int format, int quality,
                            jobject jstream, jbyteArray jstorage) {
    SkImageEncoder::Type fm;  //创建类型变量
    //将java层类型变量转换成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;
    }
    //判断当前bitmap指针是否为空
    bool success = false;
    if (NULL != bitmap) {
        SkAutoLockPixels alp(*bitmap);

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

    //创建SkWStream变量用于将压缩后的图片数据输出
        SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
        if (NULL == strm) {
            return false;
        }
    //根据编码类型,创建SkImageEncoder变量,并调用encodeStream对bitmap
    //指针指向的图片数据进行编码,完成后释放资源。
        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 创建失败
  4. 最后是调用的 encodeStream 返回 false

经过我的一一确认,1-3点是没有问题的,所以最后只剩下了第4点,但是第4点又是调用了另外一个很复杂的库,实在是无心去查看。
于是我转变思路,既然会导致这个问题出现的原因有两个,就是编码为JPG时且bitmap特别大时,那会不会是内存溢出呢?
虽然正常来说,内存溢出会抛出OOM错误(事实上,如果我手动把bitmap设置的特别大,也会抛出OOM),但是我们不妨试一下,看看两者之间有何联系。
测试代码如下:

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
    }
}

通过使用上述代码,我不停的测试到底分辨率达到多少时,会返回 false ,终于,测出来达到 655,00 X 163,93 能够刚好不返回 false。
至此,可以确定,之所以会返回 false 确实和分辨率有关。
至于为什么会有限制以及为什么是这个尺寸,刚兴趣的可以去了解一下 jpg 编码的实现,以及研究一下 libjpeg 的源码,我水平有限,就不深究了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容