Comprehensive solution of image compression for Android performance optimization

In Android, we often encounter image compression scenarios, such as uploading images to the server, including user avatars of personal information, and sometimes face recognition also needs to capture images, etc. In this case, we all need to do certain processing on the image, such as size, size and other compression.

Common image compression methods

  • quality compression
  • size compression
  • libjpeg
quality compression

First we have to introduce an api--Bitmap.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;
}
复制代码

compress() is the system's API and is a common method for quality and size compression.

public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream); This method has three parameters:
Bitmap.CompressFormat format image compression format;
int quality image compression rate, O-100. 0 compresses 100%, 100 means no Compression; OutputStream stream writes the output stream of compressed data;
public boolean compress(Bitmap.CompressFormat format, int quality, OutputStream stream); This method has three parameters:

  1. Bitmap.CompressFormat format The compression format of the image;
  2. int quality image compression rate, O-100. 0 compresses 100%, 100 means no compression;
  3. OutputStream stream to write compressed data to the output stream;

Return Value: Returns true if the compressed data was successfully written to the output stream.

Fake code

val baos= ByteArrayoutputstream ()
    try {
        var quality = 50
        do {
            quality -= 10
            baos.reset()
            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
        } while (baos.toByteArray().size / 1024 > 100)
        fos.write(baos.toByteArray(o))
    }catch (ex : Throwable) {
        ex.printStackTrace ()} finally {
        fos.apply i this: FileOutputStream
        flush ()
        close ()
    }
复制代码
size compression

Let's take a look at a property Options first

  • The property inJustDecodeBounds, if the value is true, will not return the actual bitmap, nor will it allocate memory space to avoid memory overflow.
  • Allows us to query image information, including image size information, options.outHeight (the original height of the image) and option.outWidth (the original width of the image).

两次decode,传入不同的options配置:

image.png

部分伪代码

    val reqWidth = 500
    val reqHeight = 300
    val bitmap = decodeSampledBitmapFromFile(imageFile, reqWidth, reqHeight)
    val fos = Fileoutputstream(
            File(applicationContext.filesDir,
                    child: "$ {system.currentTimeMillis() }_scale.jpg")
    )
    try {
        val quality = 50
        bitmap.compress(Bitmap.CompressFormat.JPEG, quality, fos)
        catch(ex: Throwable) {
            ex.printstackTrace() finally {
                fos.apply {
                    flush()
                    close()

                }
            }
        }
    }
}

private fun decodeSampledBitmapFromFile(imageFile: File,reqWidth: Int,reqHeight: Int): Bitmap
{
    return BitmapFactory.Options().run {
        inJustDecodeBounds = true
        //先获取原始图片的宽高,不会将Bitmap加载到内存中,返回null
        BitmapFactory.decodeFile(imageFile.absolutePath, opts: this)
        inSamplesize = calculateInSampleSize(options: this, reqWidth,reqHeight)
        inJustDecodeBounds - false
        BitmapFactory.decodeFile(imageFile.absolutePath, opts : this)
    }
}

private fun calculateInSampleSize(context: BitmapFactory, reqWidth: Int, reqHeight: Int): Int {
    //解构语法,获取原始图片的宽高
    val (height: Int, width: Int) = options.run { outHeight to outwidth }
    //计算最大的inSampleSize值,该值为2的幂次方,并同时保持这两个值高度和宽度大于请求的高度和宽度。
    //原始图片的宽高要大于要求的宽高
    var inSampleSize = 1
    if (height > reqHeight || width > reqWidth) {
        val halfHeight: Int = height / 2
        val halfwidth: Int = width / 2
        while (halfHeight / inSampleSize >= reqHeight && halfwidth / inSampleSize >= reqWidth) {
            inSampleSize *= 2
        }
    }
    return inSampleSize
}
复制代码

inSampleSize都是2的倍数 .
BitmapFactory 给我们提供了一个解析图片大小的参数类 BitmapFactory.Options ,把这个类的对象的 inJustDecodeBounds 参数设置为 true,这样解析出来的 Bitmap 虽然是个 null,但是 options 中可以得到图片的宽和高以及图片的类型。得到了图片实际的宽和高之后我们就可以进行压缩设置了,主要是计算图片的采样率。

  • 第一次采样已经结束,我们已经成功的计算出了sampleSize的大小
  • 第二次采样时我需要将图片加载出来显示,不能只加载图片的框架,因此inJustDecodeBounds属性要设置为false
libjpeg
  • libjpeg是一个完全用C语言编写的库,包含了被广泛使用的JPEG解码、JPEG编码和其他的JPEG功能的实现。
  • libjpeg-turbo图像编解码器,使用了SIMD指令来加速x86、x86-64、ARM和 PowerPC系统上的JPEG压缩和解压缩,libjpeg-turbo 的速度通常是libjpeg 的2-6倍。
  • 可以使用采用哈夫曼
  • 微信采用的方式
图片压缩流程

image.png

其实最重要的是把ARGB转换为RBG,也就是把每个像素4个字节,转换为每个像素3个字节。
导入对应的so库文件即可编写C的代码 jpeg.so 和 jpeg-turbo.so
编写这部分的代码需要NDK的环境和C语言的基础
伪代码

int generateCompressJPEG(BYTE *data, int w, int h, int quality, const char *outfileName, jboolean optimize) {
    //结构体相当于java的类
    struct jpeg_compress_struct jcs;
    //当读完整个文件的时候回回调
    struct my_error_mgr jem;
    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
    //setjmp是一个系统级函数,是一个回调
    if (setjmp(jem.setjmp_buffer)) {
        return 0;
    }
    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件  wb可写  rb可读
    FILE *f = fopen(outfileName, "wb");
    if (f == NULL) {
        return 0;
    }
    //设置结构体的文件路径,以及宽高
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;
    jcs.image_height = h;
    //TRUE=arithmetic coding, FALSE=Huffman
    jcs.arith_code = false;
    int nComponent = 3;
    // 颜色的组成rgb,三个 of color components in input image
    jcs.input_components = nComponent;
    // 设置颜色空间为rgb
    jcs.in_color_space = JCS_RGB;
    jpeg_set_defaults(&jcs);
    // 是否采用哈夫曼
    jcs.optimize_coding = optimize;
    //设置质量
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩
    jpeg_start_compress(&jcs, TRUE);
    JSAMPROW row_pointer[1];
    int row_stride;
    row_stride = jcs.image_width * nComponent;
    while (jcs.next_scanline < jcs.image_height) {
        //得到一行的首地址
        row_pointer[0] = &data[jcs.next_scanline * row_stride];
        jpeg_write_scanlines(&jcs, row_pointer, 1);
    }
    // 压缩结束
    jpeg_finish_compress(&jcs);
    // 销毁回收内存
    jpeg_destroy_compress(&jcs);
    //关闭文件
    fclose(f);
    return 1;
}
复制代码
for (int i = 0; i < bitmapInfo.height; ++i) {
    for (int j= 0; j < bitmapInfo.width; ++j){
        if (bitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888){
            //0x2312faff ->588446463
            color = *(int *) (pixelsColor);
            // 从color值中读取RGBA的值/ /ABGR
            b = (color >> 16)& 0xFE;
            g = (color >> 8)& OxFF;
            r = (color >> 0) & OxFF;
            *data = r;
            * (data + 1) =g;
            *(data + 2) = b;
            data += 3;
            //移动步长4个字节
            pixelsColor +- 4 ;
        }else {
            return -2;
        }
        
    // 是否采用哈夫曼
     jcs.optimize_coding = optimize;
复制代码

至此,三种图片压缩的方法已经介绍完毕了。

总结

经过图片压缩实践,质量压缩和libjpeg最后的图片的大小一样,效果也和原图差不多。
其实,经过我翻查原码发现,新版本的
Bitmap.compress() 会调用

boolean result = nativeCompress(mNativePtr, format.nativeInt,
        quality, stream, new byte[WORKING_COMPRESS_STORAGE]);

private static native boolean nativeCompress(long nativeBitmap, int format,
                                        int quality, OutputStream stream,
                                        byte[] tempStorage);

复制代码

其实最后也会调用到nativeCompress的压缩,也会采用哈夫曼算法,提高压缩效率。

既然这样,那么这里为什么还要介绍libjpeg的方法呢?
  • 兼容低版本,早起的compress没有采用哈夫曼算法
  • 大厂的跨平台算法

Guess you like

Origin juejin.im/post/7087388674735734797