性能优化-图片压缩性能优化

前文介绍了系统的Bitmap处理方式,那么在这一节中来说一说一个第三方的开源库,又来解决图片压缩的问题

话外题

Android使用Bitmap处理图片,处理出来的JPEG图片质量略差,那么为什么会这样呢?
这里有一个历史问题,当时skia开源引擎用来处理JPEG,Android也采用了这种引擎,然而对其做了阉割处理,也就是去掉了其中的哈夫曼算法,采用了定长编码算法,然而在解码的时候依旧使用了哈夫曼算法,这就使得处理后的图片变大了,这也是基于Android在当时的性能比较低采取的迫不得已做法,这个问题一直延续着
那么,图片的压缩该怎么做呢?也就是说不采用系统的Bitmap API,而是采用哈夫曼算法的压缩,在现在的Android机上,已经不像以前那样性能差了,可以支持哈夫曼算法了
关于霍夫曼编码参见:霍夫曼编码

jpeg开源库使用前准备

在这里,我们使用一个开源库进行图片的哈夫曼编码,用以改善Bitmap自身的不足
首先下载源代码:http://www.ijg.org/
我这里下载的是最新的版本:jpegsrc.v9c.tar.gz
接下来将其编译成so动态库,其套路和之前NDK差不多,首先看一看configure文件的帮助信息

./configure --help

然后编译,我这里设置一直存在问题,就参考了这篇文章:编译Android环境的libjpeg-turbo
脚本在编写的时候严格控制空格,可以有制表符

NDK_PATH=/usr/ndk/android-ndk-r10e
BUILD_PLATFORM=linux-x86_64
TOOLCHAIN_VERSION=4.8
ANDROID_VERSION=9

HOST=arm-linux-androideabi
SYSROOT=${NDK_PATH}/platforms/android-${ANDROID_VERSION}/arch-arm
ANDROID_CFLAGS="-march=armv7-a -mfloat-abi=softfp -fprefetch-loop-arrays -mfpu=neon -mthumb -D__ANDROID__ -D__ARM_ARCH_7__  --sysroot=${SYSROOT}"

TOOLCHAIN=${NDK_PATH}/toolchains/${HOST}-${TOOLCHAIN_VERSION}/prebuilt/${BUILD_PLATFORM}

export CPP=${TOOLCHAIN}/bin/${HOST}-cpp
export AR=${TOOLCHAIN}/bin/${HOST}-ar
export NM=${TOOLCHAIN}/bin/${HOST}-nm
export CC=${TOOLCHAIN}/bin/${HOST}-gcc
export LD=${TOOLCHAIN}/bin/${HOST}-ld
export RANLIB=${TOOLCHAIN}/bin/${HOST}-ranlib
export OBJDUMP=${TOOLCHAIN}/bin/${HOST}-objdump
export STRIP=${TOOLCHAIN}/bin/${HOST}-strip

sh ./configure --host=${HOST} 
   CFLAGS="${ANDROID_CFLAGS} -O3 -fPIE" 
   CPPFLAGS="${ANDROID_CFLAGS}" 
   LDFLAGS="${ANDROID_CFLAGS} -pie" --with-simd ${1+"$@"} --with-jpeg9 
   --prefix=$(pwd)/android/armeabi-v7a/

make
make install

编写完成,赋予执行权限,然后执行就可以在指定目录生成编译后的文件了
在lib文件夹有一个so动态库,在include中有四个头文件

Android中使用

在Android Studio新建项目,记得添加C/C++支持
将so动态库和头文件导入libs文件夹,这里可以随意指定,后面在gradle中指明路径便可

修改gradle,添加jni目录

···
sourceSets {
    main {
        jniLibs.srcDirs = ['src/main/jni']
    }
}
externalNativeBuild {
    cmake {
        cppFlags ""
    }
    ndk {
        abiFilters "armeabi-v7a"
    }
}
···

修改CMakeLists.txt,将so动态库和头文件添加至项目

cmake_minimum_required(VERSION 3.4.1)

add_library( jpegBitmap
             SHARED
             src/main/jni/jpegBitmap.c )

add_library( jpeg
             SHARED
             IMPORTED)
set_target_properties( jpeg
                       PROPERTIES IMPORTED_LOCATION
                       ${CMAKE_SOURCE_DIR}/src/main/jni/armeabi-v7a/libjpeg.so)

include_directories(${CMAKE_SOURCE_DIR}/src/main/jni/include)

find_library( log-lib
              log )

target_link_libraries( jpegBitmap
                       jnigraphics
                       jpeg
                       ${log-lib} )

编写native方法类

import android.graphics.Bitmap;

public class JPEGUtils {

    private static final int DEFAULT_QUALITY = 80;

    public static void compressBitmap(Bitmap bitmap, String path){
        compressBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(),DEFAULT_QUALITY, path.getBytes(), true);
    }

    public static void compressBitmap(Bitmap bitmap, String path, boolean optimize){
        compressBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(),DEFAULT_QUALITY, path.getBytes(), optimize);
    }

    public static void compressBitmap(Bitmap bitmap, String path, int quality){
        compressBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), quality, path.getBytes(), true);
    }

    public static void compressBitmap(Bitmap bitmap, String path, int quality, boolean optimize){
        compressBitmap(bitmap, bitmap.getWidth(), bitmap.getHeight(), quality, path.getBytes(), optimize);
    }


    public native static int compressBitmap(Bitmap bitmap, int width, int height, int quality, byte[] fileNameByte, boolean optimize);

    static {
        System.loadLibrary("jpeg");
        System.loadLibrary("jpegBitmap");
    }
}

对应native生成Native源文件
jepg的使用和ffmpeg一样,遵循固定套路,在这里,jpeg的套路是:
1、将android的bitmap解码,并转换成RGB数据(argb)
2、JPEG对象分配空间以及初始化
3、指定压缩数据源
4、获取文件信息
5、为压缩设置参数,比如图像大小、类型、颜色空间
6、开始压缩
7、压缩结束
8、释放资源

#include <jni.h>
#include <string.h>
#include <android/bitmap.h>
#include <android/log.h>
#include <stdio.h>
#include <setjmp.h>
#include <malloc.h>
#include <stdint.h>
#include <time.h>
#include "jpeglib.h"

#define LOG_TAG "jni"
#define LOGW(...)  __android_log_write(ANDROID_LOG_WARN,LOG_TAG,__VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,__VA_ARGS__)

#define true 1
#define false 0

typedef uint8_t BYTE;

char *error;
struct my_error_mgr {
    struct jpeg_error_mgr pub;
    jmp_buf setjmp_buffer;
};

typedef struct my_error_mgr *my_error_ptr;

METHODDEF(void) my_error_exit(j_common_ptr cinfo) {
    my_error_ptr myerr = (my_error_ptr) cinfo->err;
    error = (char *) myerr->pub.jpeg_message_table[myerr->pub.msg_code];
    LOGE("jpeg_message_table[%d]:%s", myerr->pub.msg_code,
         myerr->pub.jpeg_message_table[myerr->pub.msg_code]);
    // LOGE("addon_message_table:%s", myerr->pub.addon_message_table);
    // LOGE("SIZEOF:%d",myerr->pub.msg_parm.i[0]);
    // LOGE("sizeof:%d",myerr->pub.msg_parm.i[1]);
    longjmp(myerr->setjmp_buffer, 1);
}

int generateJPEG(BYTE *data, int w, int h, int quality,
                 const char *outfilename, jboolean optimize) {

    //jpeg的结构体,保存的比如宽、高、位深、图片格式等信息
    struct jpeg_compress_struct jcs;

    //当读完整个文件的时候就会回调my_error_exit这个退出方法。setjmp是一个系统级函数,是一个回调
    struct my_error_mgr jem;
    jcs.err = jpeg_std_error(&jem.pub);
    jem.pub.error_exit = my_error_exit;
    if (setjmp(jem.setjmp_buffer)) {
        return 0;
    }

    //初始化jsc结构体
    jpeg_create_compress(&jcs);
    //打开输出文件 wb:可写byte
    FILE *f = fopen(outfilename, "wb");
    if (f == NULL) {
        return 0;
    }
    //设置结构体的文件路径
    jpeg_stdio_dest(&jcs, f);
    jcs.image_width = w;//设置宽高
    jcs.image_height = h;

    //设置哈夫曼编码
    jcs.arith_code = false;
    int nComponent = 3;
    //颜色的组成rgb
    jcs.input_components = nComponent;
    //设置结构体的颜色空间为rgb
    jcs.in_color_space = JCS_RGB;

    //全部设置默认参数
    jpeg_set_defaults(&jcs);
    //是否采用哈弗曼表数据计算 品质相差5-10倍
    jcs.optimize_coding = optimize;
    //设置质量
    jpeg_set_quality(&jcs, quality, true);
    //开始压缩(是否写入全部像素)
    jpeg_start_compress(&jcs, TRUE);

    JSAMPROW row_pointer[1];
    int row_stride;
    //一行的rgb数量
    row_stride = jcs.image_width * nComponent;
    //一行一行遍历
    while (jcs.next_scanline < jcs.image_height) {
        //得到一行的首地址
        row_pointer[0] = &data[jcs.next_scanline * row_stride];
        //此方法会将jcs.next_scanline加1
        jpeg_write_scanlines(&jcs, row_pointer, 1);//row_pointer就是一行的首地址,1:写入的行数
    }
    jpeg_finish_compress(&jcs);//结束
    jpeg_destroy_compress(&jcs);//销毁 回收内存
    fclose(f);//关闭文件

    return 1;
}

/**
 * byte数组转C的字符串
 */
char *jstrinTostring(JNIEnv *env, jbyteArray barr) {
    char *rtn = NULL;
    jsize alen = (*env)->GetArrayLength(env, barr);
    jbyte *ba = (*env)->GetByteArrayElements(env, barr, 0);
    if (alen > 0) {
        rtn = (char *) malloc(alen + 1);
        memcpy(rtn, ba, alen);
        rtn[alen] = 0;
    }
    (*env)->ReleaseByteArrayElements(env, barr, ba, 0);
    return rtn;
}

JNIEXPORT jint JNICALL
Java_com_cj5785_jpegtest_JPEGUtils_compressBitmap(JNIEnv *env, jclass type, jobject bitmap,
                                                  jint width, jint height,
                                                  jint quality, jbyteArray fileNameByte,
                                                  jboolean optimize) {
    BYTE *pixelscolor;
    //1.将bitmap里面的所有像素信息读取出来,并转换成RGB数据,保存到二维byte数组里面
    //处理bitmap图形信息方法1 锁定画布
    AndroidBitmap_lockPixels(env, bitmap, (void **) &pixelscolor);
    //2.解析每一个像素点里面的rgb值(去掉alpha值),保存到一维数组data里面
    BYTE *data;
    BYTE r, g, b;
    data = (BYTE *) malloc(width * height * 3);//每一个像素都有三个信息RGB
    BYTE *tmpdata;
    tmpdata = data;//临时保存data的首地址
    int i = 0, j = 0;
    int color;
    for (i = 0; i < height; ++i) {
        for (j = 0; j < width; ++j) {
            //解决掉alpha
            //获取二维数组的每一个像素信息(四个部分a/r/g/b)的首地址
            color = *((int *) pixelscolor);//通过地址取值
            //0~255:
            //a = ((color & 0xFF000000) >> 24);
            r = ((color & 0x00FF0000) >> 16);
            g = ((color & 0x0000FF00) >> 8);
            b = ((color & 0x000000FF));
            //改值!!!----保存到data数据里面
            *data = b;
            *(data + 1) = g;
            *(data + 2) = r;
            data = data + 3;
            //一个像素包括argb四个值,每+4就是取下一个像素点
            pixelscolor += 4;
        }
    }
    //处理bitmap图形信息方法2 解锁
    AndroidBitmap_unlockPixels(env, bitmap);
    char *fileName = jstrinTostring(env, fileNameByte);
    //调用libjpeg核心方法实现压缩
    int resultCode = generateJPEG(tmpdata, width, height, quality, fileName, optimize);
    if (resultCode == 0) {
        return -1;
    }
    LOGW("处理完成");
    return 1;
}

然后再调用试试看结果,在Activity里做了下实验

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Environment;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import java.io.File;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "cj5785";
    private String pathRoot;
    private String inPath;
    private Bitmap bitmap;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        pathRoot = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
        inPath = pathRoot + "test.jpg";
        Log.d(TAG, "path = " + inPath);
        bitmap = BitmapFactory.decodeFile(inPath);
    }

    public void defaultCheck(View view) {
        final String outPath = pathRoot + "default.jpg";
        new Thread(new Runnable() {
            @Override
            public void run() {
                JPEGUtils.compressBitmap(bitmap, outPath);
            }
        }).start();
    }

    public void noHF90(View view) {
        final String outPath = pathRoot + "noHF90.jpg";
        new Thread(new Runnable() {
            @Override
            public void run() {
                JPEGUtils.compressBitmap(bitmap, outPath, 90, false);
            }
        }).start();
    }

    public void withHF90(View view) {
        final String outPath = pathRoot + "withHF90.jpg";
        new Thread(new Runnable() {
            @Override
            public void run() {
                JPEGUtils.compressBitmap(bitmap, outPath, 90);
            }
        }).start();
    }

    public void noHF80(View view) {
        final String outPath = pathRoot + "noHF80.jpg";
        new Thread(new Runnable() {
            @Override
            public void run() {
                JPEGUtils.compressBitmap(bitmap, outPath, false);
            }
        }).start();
    }
}

生成了默认的(哈夫曼编码,80质量),没有哈夫曼编码90质量,有哈夫曼编码90质量,没有哈夫曼编码80质量的四张生成图片,对比原图,其体积都小了很多,后来又做了一个哈夫曼100质量的,对比大小如下:
对比结果

原文地址:https://www.cnblogs.com/cj5785/p/10664634.html