阅读 928

性能优化 (四) ubuntu 完美编译 libjpeg 图像压缩库,媲美微信图片压缩算法

性能优化系列

APP 启动优化

UI 绘制优化

内存优化

图片压缩

长图优化

电量优化

Dex 加解密

动态替换 Application

APP 稳定性之热修复原理探索

APP 持续运行之进程保活实现

ProGuard 对代码和资源压缩

APK 极限压缩

使用方式

  1. 在 project/build.gradle 上添加以下代码

    allprojects {
    		repositories {
    			...
    			maven { url 'https://jitpack.io' }
    		}
    	}
    复制代码
  2. 在 app/build.gradle 添加依赖

    dependencies {
            implementation 'com.github.yangkun19921001:LIBJPEG_SAMPLE:v1.0.1'
    }
    复制代码
  3. 压缩使用

    //bitmap        : 需要压缩的 bitmap
    //q             : 压缩质量 建议 30 - 50
    //outputFilePath: 压缩之后存储的图片地址
    JpegUtils.native_Compress(Bitmap bitmap,int q,String outputFilePath);
    复制代码

JEPG 是什么?

相信有一部分使用 iPhone 手机用微信发送图片的时候,明明图片大小只有 1M ,但清晰度比 Android 手机 5 M 图片大小的还要清晰,那么这是为什么呢 ?。

当时谷歌开发 Android 的时候,考虑了大部分手机的配置并没有那么高,所以对图片处理使用的是 Skia。当然这个库的底层还是用的 jpeg 图片压缩处理。但是为了能够适配低端的手机(这里的低端是指以前的硬件配置不高的手机,CPU 和内存在手机上都非常吃紧,性能差),由于哈夫曼算法比较吃 CPU 并且编解码慢,被迫用了其他的算法。所以 Skia 在进行图片处理在低版本中并没有开启哈弗曼算法。

那么,JEPG 到底是什么?JEPG (全称是 Joint Photographic Experts Group) 是一种常见的一种图像格式,为什么我在这里会提到 JEPG 呢?是因为开源了一个 C/C++ 库底层是基于哈夫曼算法对图片的压缩 (libjpeg),下面我们就来着重了解下 libjpeg 这个库

libjpeg 简介

libjpeg-turbo 是一个 JPEG 图像编解码器,它使用 SIMD 指令(MMX,SSE2,AVX2,NEON,AltiVec)来加速 x86,x86-64,ARM 和 PowerPC 系统上的基线 JPEG 压缩和解压缩,以及渐进式JPEG 压缩 x86 和 x86-64 系统。在这样的系统上,libjpeg-turbo 的速度通常是 libjpeg 的 2 - 6 倍,其他条件相同。在其他类型的系统上,凭借其高度优化的霍夫曼编码例程,libjpeg-turbo 仍然可以大大超过 libjpeg。在许多情况下,libjpeg-turbo 的性能可与专有的高速 JPEG 编解码器相媲美。 libjpeg-turbo 实现了传统的 libjpeg API 以及功能较弱但更直接的 TurboJPEG API 。 libjpeg-turbo 还具有色彩空间扩展,允许它从/解压缩到32位和大端像素缓冲区(RGBX,XBGR等),以及功能齐全的 Java 接口。 libjpeg-turbo 最初基于 libjpeg / SIMD,这是由 Miyasaka Masaru 开发的 libjpeg v6b 的 MMX 加速衍生物。 TigerVNC 和 VirtualGL 项目在 2009 年对编解码器进行了大量增强,并且在2010年初,libjpeg-turbo 分拆成一个独立项目,目标是为更广泛的用户提供高速 JPEG压缩/解压缩技术。开发人员。

现在我们大概了解到了 libjpeg 是一个对图像编解码库,现在我们需要准备环境去编译 libjpeg。

编译准备工作

系统: Ubuntu 18.04 也可以使用我下载好的 提取码:biyt

libjpeg: libjepg 2.0.2

cmake: cmake-3.14.4-Linux-x86_64.tar.gz

ndk: android-ndk-r17c

开始发车准备编译

  1. ubuntu 中下载 libjpeg

    1. wget github.com/libjpeg-tur…

    2. 解压 tar xvf 2.0.2.tar.gz

    3. 编译参考

  2. ubuntu 中安装 cmake

    1. 删除原来的 apt-get autoremove cmake

    2. wget github.com/Kitware/CMa…

    3. 解压 tar zxvf cmake-3.14.3.tar.gz

    4. 创建软连接

      1. mv cmake-3.14.3-Linux-x86_64 /opt/cmake-3.14.3
      2. ln -sf /opt/cmake-3.14.3/bin/* /usr/bin/

      输入 cmake -- version 如果有这样的显示代表安装成功

  3. 进入到 libjpeg 目录,生成 shell 脚本

    1. vim build.sh 新建一个文件

      # lib-name
      MY_LIBS_NAME=libjpeg-turbo_2.0.2
      # 源码目录
      MY_SOURCE_DIR=/home/yangkun/libjpeg-turbo-2.0.2
      MY_BUILD_DIR=yangkun
      
      # android-cmake
      CMAKE_PATH=/opt/cmake-3.14.4/bin
      
      export PATH=${CMAKE_PATH}/bin:$PATH
      
      NDK_PATH=/home/yangkun//android-ndk-r17c
      BUILD_PLATFORM=linux-x86_64
      TOOLCHAIN_VERSION=4.9
      ANDROID_VERSION=24
      
      ANDROID_ARMV5_CFLAGS="-march=armv5te"
      ANDROID_ARMV7_CFLAGS="-march=armv7-a -mfloat-abi=softfp -mfpu=neon"  # -mfpu=vfpv3-d16  -fexceptions -frtti
      ANDROID_ARMV8_CFLAGS="-march=armv8-a"  	# -mfloat-abi=softfp -mfpu=neon -fexceptions -frtti
      ANDROID_X86_CFLAGS="-march=i386 -mtune=intel -mssse3 -mfpmath=sse -m32"
      ANDROID_X86_64_CFLAGS="-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel"
      
      # params($1:arch,$2:arch_abi,$3:host,$4:compiler,$5:cflags,$6:processor)
      build_bin() {
      
          echo "-------------------star build $2-------------------------"
      
          ARCH=$1                # arm arm64 x86 x86_64
          ANDROID_ARCH_ABI=$2    # armeabi armeabi-v7a x86 mips
          # 最终编译的安装目录
          PREFIX=$(pwd)/dist/${MY_LIBS_NAME}/${ANDROID_ARCH_ABI}/
          HOST=$3
          COMPILER=$4
          PROCESSOR=$6
          SYSROOT=${NDK_PATH}/platforms/android-${ANDROID_VERSION}/arch-${ARCH}
          CFALGS="$5"
          TOOLCHAIN=${NDK_PATH}/toolchains/${HOST}-${TOOLCHAIN_VERSION}/prebuilt/${BUILD_PLATFORM}
          
          # build 中间件
          BUILD_DIR=./${MY_BUILD_DIR}/${ANDROID_ARCH_ABI}
      
          export CFLAGS="$5 -Os -D__ANDROID_API__=${ANDROID_VERSION} --sysroot=${SYSROOT} \
                         -isystem ${NDK_PATH}/sysroot/usr/include \
                         -isystem ${NDK_PATH}/sysroot/usr/include/${HOST} "
          export LDFLAGS=-pie
      
          echo "path==>$PATH"
          echo "build_dir==>$BUILD_DIR"
          echo "ARCH==>$ARCH"
          echo "ANDROID_ARCH_ABI==>$ANDROID_ARCH_ABI"
          echo "HOST==>$HOST"
          echo "CFALGS==>$CFALGS"
          echo "COMPILER==>$COMPILER-gcc"
          echo "PROCESSOR==>$PROCESSOR"
      
          mkdir -p ${BUILD_DIR}   #创建当前arch_abi的编译目录,比如:binary/armeabi-v7a
          cd ${BUILD_DIR}         #此处 进了当前arch_abi的2级编译目录
      
      #运行时创建临时编译链文件toolchain.cmake
      cat >toolchain.cmake << EOF 
      set(CMAKE_SYSTEM_NAME Linux)
      set(CMAKE_SYSTEM_PROCESSOR $6)
      set(CMAKE_C_COMPILER ${TOOLCHAIN}/bin/${COMPILER}-gcc)
      set(CMAKE_FIND_ROOT_PATH ${TOOLCHAIN}/${COMPILER})
      EOF
      
          cmake -G"Unix Makefiles" \
                -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake \
                -DCMAKE_POSITION_INDEPENDENT_CODE=1 \
                -DCMAKE_INSTALL_PREFIX=${PREFIX} \
                -DWITH_JPEG8=1 \
                ${MY_SOURCE_DIR}
      
          make clean
          make
          make install
      
          #从当前arch_abi编译目录跳出,对应上面的cd ${BUILD_DIR},以便function多次执行
          cd ../../
      
          echo "-------------------$2 build end-------------------------"
      }
      
      # build armeabi
      build_bin arm armeabi arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV5_CFLAGS" arm
      
      #build armeabi-v7a
      build_bin arm armeabi-v7a arm-linux-androideabi arm-linux-androideabi "$ANDROID_ARMV7_CFLAGS" arm
      
      #build arm64-v8a
      build_bin arm64 arm64-v8a aarch64-linux-android aarch64-linux-android "$ANDROID_ARMV8_CFLAGS" aarch64
      
      #build x86
      build_bin x86 x86 x86 i686-linux-android "$ANDROID_X86_CFLAGS" i386
      
      #build x86_64
      build_bin x86_64 x86_64 x86_64 x86_64-linux-android "$ANDROID_X86_64_CFLAGS" x86_64
      复制代码
    2. 如果编译遇见 权限问题

      给它一个 可执行文件的权限 chmod +x build.sh

    3. 继续执行

    4. 编译完成

      这里我们发现 已经有我们需要的 静态库 .a 和 动态库 .so

    5. 在 AndroidStudio 中创建一个简单的项目 用于测试是否压缩成功

      1. 结构目录

      标红的都是重要的文件,include 头文件和 libs/armeabi-v7a 是我们刚刚编译出来的文件

      下面我们就来运行一下看看压缩效果

      1. 压缩主要代码

        jni 代码

        #include <jni.h>
        #include <string>
        #include "../include/jpeglib.h"
        #include <malloc.h>
        #include <android/bitmap.h>
        
        
        void write_JPEG_file(uint8_t *data, int w, int h, jint q, const char *path) {
        //    3.1、创建jpeg压缩对象
            jpeg_compress_struct jcs;
            //错误回调
            jpeg_error_mgr error;
            jcs.err = jpeg_std_error(&error);
            //创建压缩对象
            jpeg_create_compress(&jcs);
        //    3.2、指定存储文件  write binary
            FILE *f = fopen(path, "wb");
            jpeg_stdio_dest(&jcs, f);
        //    3.3、设置压缩参数
            jcs.image_width = w;
            jcs.image_height = h;
            //bgr
            jcs.input_components = 3;
            jcs.in_color_space = JCS_RGB;
            jpeg_set_defaults(&jcs);
            //开启哈夫曼功能
            jcs.optimize_coding = true;
            jpeg_set_quality(&jcs, q, 1);
        //    3.4、开始压缩
            jpeg_start_compress(&jcs, 1);
        //    3.5、循环写入每一行数据
            int row_stride = w * 3;//一行的字节数
            JSAMPROW row[1];
            while (jcs.next_scanline < jcs.image_height) {
                //取一行数据
                uint8_t *pixels = data + jcs.next_scanline * row_stride;
                row[0] = pixels;
                jpeg_write_scanlines(&jcs, row, 1);
            }
        //    3.6、压缩完成
            jpeg_finish_compress(&jcs);
        //    3.7、释放jpeg对象
            fclose(f);
            jpeg_destroy_compress(&jcs);
        }
        
        
        extern "C"
        JNIEXPORT void JNICALL
        Java_com_yk_libjpeg_1sample_libjpeg_JpegUtils_native_1Compress__Landroid_graphics_Bitmap_2ILjava_lang_String_2(
                JNIEnv *env, jclass type, jobject bitmap, jint q, jstring path_) {
            const char *path = env->GetStringUTFChars(path_, 0);
            //从bitmap获取argb数据
            AndroidBitmapInfo info;//info=new 对象();
            //获取里面的信息
            AndroidBitmap_getInfo(env, bitmap, &info);//  void method(list)
            //得到图片中的像素信息
            uint8_t *pixels;//uint8_t char    java   byte     *pixels可以当byte[]
            AndroidBitmap_lockPixels(env, bitmap, (void **) &pixels);
            //jpeg argb中去掉他的a ===>rgb
            int w = info.width;
            int h = info.height;
            int color;
            //开一块内存用来存入rgb信息
            uint8_t *data = (uint8_t *) malloc(w * h * 3);//data中可以存放图片的所有内容
            uint8_t *temp = data;
            uint8_t r, g, b;//byte
            //循环取图片的每一个像素
            for (int i = 0; i < h; i++) {
                for (int j = 0; j < w; j++) {
                    color = *(int *) pixels;//0-3字节  color4 个字节  一个点
                    //取出rgb
                    r = (color >> 16) & 0xFF;//    #00rrggbb  16  0000rr   8  00rrgg
                    g = (color >> 8) & 0xFF;
                    b = color & 0xFF;
                    //存放,以前的主流格式jpeg    bgr
                    *data = b;
                    *(data + 1) = g;
                    *(data + 2) = r;
                    data += 3;
                    //指针跳过4个字节
                    pixels += 4;
                }
            }
            //把得到的新的图片的信息存入一个新文件 中
            write_JPEG_file(temp, w, h, q, path);
        
            //释放内存
            free(temp);
            AndroidBitmap_unlockPixels(env, bitmap);
            env->ReleaseStringUTFChars(path_, path);
        }
        复制代码

        调用代码

        public class JpegUtils {
            static {
                System.loadLibrary("jpeg-yk");
            }
        
            /**
             * A native method that is implemented by the 'native-lib' native library,
             * which is packaged with this application.
             */
            public native  static void native_Compress(Bitmap bitmap, int q, String path);
        
        }
        复制代码

效果

  1. 开始压缩

        public void click(View view) {
            File input = new File(Environment.getExternalStorageDirectory(), "/girl.jpg");
            ImageView preImg = findViewById(R.id.pre);
            mNextImg = findViewById(R.id.next);
            inputBitmap = BitmapFactory.decodeFile(input.getAbsolutePath());
            preImg.setImageBitmap(inputBitmap);
           JpegUtils .native_Compress(inputBitmap, 10, Environment.getExternalStorageDirectory() + "/girl4.jpg");
            Toast.makeText(this, "执行完成", Toast.LENGTH_SHORT).show();
            String filePath = Environment.getExternalStorageDirectory() + "/girl4.jpg";
            mNextImg.setImageBitmap(BitmapFactory.decodeFile(filePath));
        }
    复制代码
  2. 动画效果

    压缩效果: 压缩质量在 10 的时候用压缩出来的质量也还是挺好了,只有周围有点点模糊,但是建议压缩质量在 30 -50 之间。

    压缩率: 大约压缩后的图片大小是原图的缩小 6 倍的样子。

资料

更多代码信息请移步 GitHub

提供 libjpeg 编译脚本

libjpeg 编译的源码和动态/静态库 在 dist 目录 提取码:b0cs

计划

图片优化计划出三篇文章

  1. libjpeg 编译及使用
  2. 长图巨图优化
  3. bitmap 内存管理 三级缓存
关注下面的标签,发现更多相似文章
评论