>>分享Android开发相关的技术 书籍支持  卫琴直播  品书摘要  在线测试  资源下载  联系我们
发表一个新主题 开启一个新投票 回复文章 您是本文章第 19985 个阅读者 刷新本主题
 * 贴子主题:  Bitmap压缩原理解析与Android 7.0之前通过NDK使用libjpeg库高质量压缩图片 回复文章 点赞(0)  收藏  
作者:sunshine    发表时间:2020-03-08 13:10:25     消息  查看  搜索  好友  邮件  复制  引用

          

Bitmap压缩原理解析与Android 7.0之前通过NDK

使用libjpeg库高质量压缩图片

一、Bitmap压缩原理

     我们平常使用的bitmap.compress() 的内部实际上调用了如下native方法        

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

     在android源码的\frameworks\base\core\jni\android\graphics\Bitmap.cpp中我们发现nativeCompress这个方法实际对应的C++函数        

  static  bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap, int format,  int quality, object jstream, jbyteArray jstorage)  

      在上述方法中,然后是判断了编码的类型(PNG,JPEG,WEBP),然后真正调用编码的是这段                

          SkImageEncoder * encoder  = SkImageEncoder ::Create(fm);
         if ( NULL  != encoder) {
            success  = encoder ->encodeStream(strm,  *bitmap, quality);
            delete encoder;
        }

      可以看到这里使用了一个SkImageEncoder的编码器来对我们的图像进行了编码,这个编码器就是Skia引擎的编码器。

    什么是Skia引擎?Skia引擎是一个开源的C++二维图形库,目前由Google维护,在chrome浏览器和android系统中应用都很广泛。

    Android系统中的skia引擎是阉割的skia版本,对jpeg的处理基于libjpeg开源库,对png的处理则是基于libpng。  

早期的Android系统由于cpu吃紧。将libjpeg中的最优哈夫曼编码关闭了。直到7.0才打开。

6.0中SkImageDecoder_libjpeg.cpp源码        

           cinfo.input_gamma =  1;

     jpeg_ set_defaults(&cinfo);
     jpeg_ set_quality(&cinfo, quality, TRUE);

      7.0中SkImageDecoder_libjpeg.cpp源码        

          cinfo.input_gamma =  1;

     jpeg_set_defaults(&cinfo);

      // Tells libjpeg-turbo to compute optimal Huffman coding tables
      // for the image.  This improves compression at the cost of
      // slower encode performance.
     cinfo.optimize_coding =  TRUE;           //开启最优哈夫曼编码
     jpeg_set_quality(&cinfo, quality,  TRUE);

      开启了最优哈夫曼编码的话图片压缩的质量会明显提高,所以在7.0之前的系统中使用bitmap原始的压缩方式,在压缩比率较高的情况下,图片失真会十分严重。而开启了最优哈夫曼编码的话,不仅图片质量几乎和原图看不出差别,压缩后的图片大小也可以进一步降低。            

二、 7.0之前的系统中如何优化图片压缩

     我们无法改变android底层skia的压缩机制,也无法操作系统源码中的libjpeg库。这时候就需要使用NDK来调用C++原生库(libjpeg),在应用中通过调用自定义的native方法来压缩图片。            

步骤

     由于LibJpeg-turbo是用C语言编写的JPEG编解码库。我们直接到官网https://libjpeg-turbo.org/上下载它的源代码  -> 然后在Linux上编译成支持Android CPU架构的库 -> 然后在AndroidStduio中集成-> 最后自己调动libjpeg库进行编码。    

三、Linux中编译so库

     我用的是Ubuntu 64位虚拟机,直接在终端下输入                

wget https://github .com/libjpeg-turbo/libjpeg-turbo/archive/ 1.5 .3 .tar .gz

      下载libjpeg-turbo,下载完成后解压        

tar xvf  1.5 .3 .tar .gz

     如果你要在模拟器上运行,需要编译成x86架构的,除此以外还要安装另外一个工具NASM(armeabi不需要),        

wget http://www .nasm .us/pub/nasm/releasebuilds/ 2.13/nasm- 2.13 .tar .gz

     下载完解压然后编译nasm        

tar xvf nasm- 2.13 .tar .gz
cd nasm- 2.13
./configure
make install

     这里我在真机上运行,编译的arme-v7a版本,上述安装nasm的步骤可以省略。

         下面开始真正编译我们的libjpeg,实际上在该库的官网上也有很详细的编译步骤https://github.com/libjpeg-turbo/libjpeg-turbo/blob/master/BUILDING.md

首先,cd进解压后的libjpeg目录,        

  cd libjpeg-turbo- 1.5. 3

      然后使用autoconf生成Configure配置脚本                

autoreconf  -fiv

      如果报错,提示你需要安装libtool工具                

  sudo apt-get install libtool

     完成后就可以编写我们的shell脚本了,在libjpeg-turbo-1.5.3目录下用vi编辑器新建一个脚本。        

vi build .sh

     因为我有图形界面,所以我用图形界面的编辑器操作更方便。                

gedit build .sh

     然后安装官网上编译相应架构的库的提示,

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

         将shell脚本,直接复制过来,然后修改相关配置。

比如,修改ndk路径        

NDK_PATH =/home/lishuji/android -ndk -r14b

     注意你的虚拟机中需要下载了NDK,并且配置好环境变量。

         修改最低的android版本                

  ANDROID_VERSION=  "14"

     此外还要加上下面一句话,设置最终编译出的静态库和动态库存放的目录,我们就在libjpeg-turbo-1.5.3下新建一个android目录来存放。        

- -prefix= /home/lishuji /libjpeg-turbo-1.5.3/android

     最终的build.sh如下:                

  #!/bin/bash

# Set these variables to suit your needs
#指向ndk目录
NDK_PATH=/home/lishuji/android-ndk-r14b
#查看NDK目录下的 toolchains/x86-4.9/prebuilt
BUILD_PLATFORM= "linux-x86_64"
#查看toolchains/x86-xx xx是多少就写多少
TOOLCHAIN_VERSION= "4.9"
#查看platforms目录 as默认也是14
ANDROID_VERSION= "14"

# It should not be necessary to modify the rest
HOST=arm-linux-androideabi
SYSROOT= ${NDK_PATH}/platforms/android- ${ANDROID_VERSION}/arch-arm
ANDROID_CFLAGS= "-march=armv7-a -mfloat-abi=softfp -fprefetch-loop-arrays \
  -D__ANDROID_API__= ${ANDROID_VERSION} --sysroot= ${SYSROOT} \
  -isystem  ${NDK_PATH}/sysroot/usr/include \
  -isystem  ${NDK_PATH}/sysroot/usr/include/ ${HOST}"

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

./configure --host= ${HOST} \
  --prefix=/home/lishuji/libjpeg-turbo- 1.5. 3/android  \
  CFLAGS= " ${ANDROID_CFLAGS} -O3 -fPIE" \
  CPPFLAGS= " ${ANDROID_CFLAGS}" \
  LDFLAGS= " ${ANDROID_CFLAGS} -pie" --with-simd  ${1+"$@"}
make install

     注意:编译不同CPU架构下的build.sh文件不同,请参考官网的build.sh脚本。

         最终生成的目录如下:

         点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

         其中lib文件夹下就是生成的静态库和动态库,include中包含了我们需要的头文件。    

四、AS中集成,编码

     我们将上面那些复制到我们的本机上来,然后新建一个AS项目,勾选 Include C++ Support。创建完成后,将include文件夹和lib文件夹下的库复制到我们的cpp目录中。

         最终的项目结构如下(这里我只用了一个.a静态库)

点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

         然后修改CMakeList文件,        

  #添加静态库
add_library(jpeg STATIC IMPORTED)
#设置静态库地址
set_target_properties(jpeg PROPERTIES IMPORTED_LOCATION  ${CMAKE_SOURCE_DIR}/src/main/cpp/libs/libturbojpeg.a)
#包含头文件
include_directories(src/main/cpp/ include)

     链接        

target_link_libraries(
                       native -lib
                       jpeg
                       jnigraphics
                        log )

     修改build.gradle,指定abiFilters                

         externalNativeBuild {
            cmake {
                cppFlags  "-frtti -fexceptions"
                abiFilters  'armeabi-v7a'
            }
        }

     然后新建一个native方法,名叫nativeCompress,传入我们的bitmap,压缩质量(0-100),以及压缩后的图片路径。        

  public  native  void  nativeCompress(Bitmap bitmap ,  int q , String path );

     最后在native-lib.cpp中实现,主要思路也很明确,首先通过jni的方法从bitmap中获取rgba的数据,然后去除透明度,在C++中开辟一块内存来存放获得的rgb值,接着调用libjpeg中的方法来进行压缩,其中注意让optimize_coding = TRUE即可,具体做法和android源码中很相似,也可查看libjpeg库的examples,在我们编译出来的android/share/doc/libjpeg-turbo/example.c中,最后不要忘了释放相关内存。

         native-lib.cpp        

  #include <jni.h>
#include <string>
#include <android/bitmap.h>
#include <malloc.h>
#include <jpeglib.h>

//使用libjpeg库进行压缩
void write_JPEG_file(uint8_t *data,  int w,  int h, jint q,  const  char *path) {

     //第一步.创建jpeg压缩对象
    jpeg_compress_struct jcs;
     //错误回调
    jpeg_error_mgr error;
    jcs.err = jpeg_std_error(&error);
     //创建压缩对象
    jpeg_create_compress(&jcs);

     //第二步.指定存储文件
    FILE *f = fopen(path, "wb");
    jpeg_stdio_dest(&jcs,f);

     //第三步.设置压缩参数
    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);

     //第三步.开始压缩
    jpeg_start_compress(&jcs, 1);

     //第四步.循环写入每一行数据
     int row_stride = w* 3;
    JSAMPROW  row[ 1];
     //next_scanline 一行数据开头的位置
     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);
    }

     //第五步.压缩完成,释放jpeg对象
    jpeg_finish_compress(&jcs);
    fclose(f);
    jpeg_destroy_compress(&jcs);

}
extern  "C"
JNIEXPORT  void JNICALL
Java_com_libjpeg_1test2_MainActivity_nativeCompress(JNIEnv *env, jobject instance, jobject bitmap,
                                                    jint q, jstring path_) {
     const  char *path = env->GetStringUTFChars(path_,  0);
     //从bitmap获取argb数据
    AndroidBitmapInfo info ;
     //获得bitmap的信息
    AndroidBitmap_getInfo(env,bitmap,&info);

    uint8_t *pixels;
    AndroidBitmap_lockPixels(env, bitmap, ( void **) &pixels);

     //argb , 去掉透明度
     int w = info.width;
     int h = info.height;
     int color;
     //开辟一块内存存放rgb值
    uint8_t* data = (uint8_t *)  malloc(w * h *  3);
    uint8_t* temp = data;
    uint8_t r, g , b;
     for(  int i =  0 ; i < h ; i++ ){
         for(  int j =  0 ; j < w ; j ++ ){
            color = *( int*)pixels;
             //操作argb
             //取红色
            r = (color >>  16) &  0xFF;
            g = (color >>  8 ) &  0xFF;
            b = color &  0xFF ;
             //将rgb值放入data数组中,注意libjpeg的顺序是bgr
            *data = b ;
            *(data+ 1) = g ;
            *(data+ 2) = r ;
             //指针移动三位
            data +=  3 ;
             //指针后移4个字节,指向下一个rgba像素
            pixels +=  4;
        }
    }

    write_JPEG_file(temp,w,h,q,path);

         //释放内存
    AndroidBitmap_unlockPixels(env,bitmap);
     free(data);
    env->ReleaseStringUTFChars(path_, path);
}

     注意:我压缩的是32位真彩图,也就是带透明度的,如果用24位真彩图会报错,解决方法修改相关获取rgb值的代码即可。

         最后我们可以对比自己使用libjpeg库(开启了哈夫曼)和使用bitmap原始的compress方法压缩产生的图片区别。

         点击在新窗口中浏览原图
CTRL+鼠标滚轮放大或缩小

         可以看出我们使用native方法压缩的图片比使用bitmap原生方法小了10k,别小看这10k,当图片多了以后就是很大的流量,对性能优化来说,就优化了很多,而且压缩出来的图片质量也高于用bitmap的方法压缩出来的图片。这张图片原本比较清晰,可能看不出来较大差别,但是还是看的出来一些不同。如果你使用原本就比较模糊的图片,使用bitmap压缩的话,最终的效果可能惨不忍睹,
                                    
                                                                    
----------------------------
原文链接:https://blog.csdn.net/SakuraMashiro/article/details/79182239

程序猿的技术大观园:www.javathinker.net



[这个贴子最后由 flybird 在 2020-03-09 09:57:15 重新编辑]
  Java面向对象编程-->集合(下)
  JavaWeb开发-->Web运作原理(Ⅰ)
  JSP与Hibernate开发-->持久化层的映射类型
  Java网络编程-->XML数据处理
  精通Spring-->绑定CSS样式
  Vue3开发-->Vue CLI脚手架工具
  Android 控件布局实现卡片效果,阴影效果
  Android自动化测试之Robotium学习
  Android之TabHost
  android异步更新UI
  Android实用测试方法之Monkey与MonkeyRunner
  Android网络开发-请求队列-性能提升解决方案
  Android UI学习 - Tab的学习和使用
  在Window中下载Android源代码的步骤
  Android Gallery实现循环显示图像
  Android制作Tabs界面的常用方法
  Android线程处理简述
  Android 解码播放GIF图像
  Android数据存储之Content Providers
  自定义ViewGroup和FrameLayout实现轮播图(包括底部小圆点)
  Android炫酷菜单
  更多...
 IPIP: 已设置保密
楼主      
1页 1条记录 当前第1
发表一个新主题 开启一个新投票 回复文章


中文版权所有: JavaThinker技术网站 Copyright 2016-2026 沪ICP备16029593号-2
荟萃Java程序员智慧的结晶,分享交流Java前沿技术。  联系我们
如有技术文章涉及侵权,请与本站管理员联系。