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


    前文介绍了系统的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质量的,对比大小如下:
    对比结果

  • 相关阅读:
    所谓的底层问题
    字符流与字节流
    字节和字符,对信息进行编码
    Asp.net和数据库的一些概念
    谈.NET,由编译器开始谈起
    Extjs中的dom,Ext.Element和Ext.Component对象的关系
    WCF和Delphi通信时序列化的问题
    认真的考虑了下领域模型,发现设计是最难的部分。书上的例子各个对象职责划分的不错,可惜能看懂不代表能设计出。
    MS100 [011020]
    MS100[001]
  • 原文地址:https://www.cnblogs.com/cj5785/p/10664634.html
Copyright © 2020-2023  润新知