美文网首页
Android Bitmap.compress 方法返回 fal

Android Bitmap.compress 方法返回 fal

作者: equationl | 来源:发表于2021-07-27 21:22 被阅读0次

    前言

    最近在解决一个遗留已久的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 的源码,我水平有限,就不深究了。

    相关文章

      网友评论

          本文标题:Android Bitmap.compress 方法返回 fal

          本文链接:https://www.haomeiwen.com/subject/zzxamltx.html