直接使用项目或直接复制 libs 中的 so 库到项目中即可(当前只构建了 armeabi), 需要其他 ABI 可检下项目另外使用 CMake 构建即可.
结果预览:
原图大小 5.99M~~ 我们把所有经过压缩的图片放到同等大小的情况后, 很明显, 采样压缩跟尺寸压缩都不是我们想要的结果, 而质量压缩跟 JNI 压缩我设置的质量压缩值都是 30,JNI 压缩出来只有 278KB, 直接质量压缩出来的有 484KB, 综合之后, JNI 才是综合最优的方式, 当然, 如果只是头像, 我们设置可以把配置值设置得更小, 图片就更小.
为什么 iPhone 手机图片的质量比 Android 的好?
首先了解两个图像处理库: libjpeg,Skia.
Skia: 图像处理引擎, Google 在 Android 系统上就是采用 Skia, 它是基于 libjpeg 的二次封装, Google 在很多其它产品也使用了这个库, 比如 Chorme,Firefox 等等.
libjpeg: 早期的图像处理引擎, 用于 PC 端.
官方文档可以看到 libjpeg.doc 这样一段话:
boolean optimize_coding TRUE causes the compressor to compute optimal Huffman coding tables for the image. This requires an extra pass over the data and therefore costs a good deal of space and time. The default is FALSE, which tells the compressor to use the supplied or default Huffman tables. In most cases optimal tables save only a few percent of file size compared to the default tables. Note that when this is TRUE, you need not supply Huffman tables at all, and any you do supply will be overwritten.
boolean optimize_coding:
参数为 TRUE 时, 图片压缩算法使用最优的哈夫曼编码表, 它需要额外传递数据, 因此会耗费 CPU 运算时间, 以及开辟很多临时内存空间. 参数为 FALSE 时, 使用默认的哈夫曼编码表. 在大多数情况, 使用最优哈夫曼编码表相比默认哈夫曼编码表, 能节省图像文件很大比例的大小. 为什么使用最优哈夫曼编码表可以节省图像文件很大的比例大小呢? 可以先了解下什么是哈夫曼树和哈夫曼编码
其次了解下 libjpeg 使用哈夫曼编码是对图片上的每个像素 (ARGB) 进行编码, 比如
ARGB(每个颜色通道取值范围 0-255)的编码分别是:
- A:001
- R:010
- G:011
- B:100
如果采用定长, 那一个图片下来一个人像素就是 001010011100...
如果我们使用可变长编码方式, 遍历, 再嵌套遍历, 再嵌套遍历每一个像素来获取前缀编码(没错, 这个运算过程很大, 而且临时变量内存也需要很大, 但相对于今天的手机 CPU 来说, so easy), 我们大致可以得到这样的编码:
- A:01
- R:10
- G:11
- B:100
那一个图片下来一个人像素就是 011011100...
编码长度的优化后, 接下来干的事就是计算权重以及每个颜色通道对应的编码的出现频次构建哈夫曼树了, 这里就参考博客里头的图片了哈.
哈夫曼树
看完博客之后, 可以知道最优哈夫曼编码其实是使用了可变长编码方式, 而默认的哈夫曼编码使用了定长编码方式, 因此需要更多的存储空间, 呈现出来的手机图片自然会大很大.
libjpeg 把 optimize_coding 参数默认设置为 FALSE 是因为 10 多年前的 Android 手机 CPU 跟内存都非常吃紧, 所以当年没有设置为 TRUE. 如今的手机 CPU 跟内存都 "起飞了",Goolge 的 Skia 图像处理引擎却还是使用 optimize_coding 的默认值 FALSE. 我们无法修改系统 Skia 的这个参数值, 所以只能默默忍受 size 很大的图像文件.
经过大量图像压缩测试结果, 得到两个结论:
1. 图片压缩到相同的质量, FALSE 所产出的图像文件大小是 TRUE 的 5-10 倍.
2. 图片压缩到相同的质量, Android 所产出的图像文件大小比 iOS 也是大 5-10 倍.
所以, 通过使用 libjpeg 编译自己的 native library 修改 optimize_coding 参数的值, 达图像质量相同, 所产出的图像却能节省 5-10 倍空间大小的效果.
实现的步骤:
1. 构建 libjpeg 的 so 库
到官方下载对应自己电脑系统类型的压缩包, 创建 Android 项目导入压缩包里头的 xx.h,xx.c 文件构建 so 库. bither/bither-Android-lib 已经做了这个工作, 因此我们只需直接拿他的 libjpegbither.so 即可.
2. 导入 libjpeg 的声明头文件, 因为步骤 1 的 libjpegbither.so 是对这些头文件的实现, 因此需要导入这些头文件.
3. 创建 CMake 脚本
- cmake_minimum_required(VERSION 3.4.1)
- add_library( effective-bitmap
- SHARED
- src/main/cpp/effective-bitmap.c )
- include_directories( src/main/cpp/jpeg/)
- add_library(jpegbither SHARED IMPORTED)
- set_target_properties(jpegbither
- PROPERTIES IMPORTED_LOCATION
- ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}/libjpegbither.so)
- find_library( log-lib
- log )
- find_library( jnigraphics-lib jnigraphics )
- target_link_libraries( effective-bitmap
- jpegbither
- ${log-lib}
- ${jnigraphics-lib})
4. 配置 gradle 关联 CMakeLists.txt 构建脚本, 以及指定产出的 ABI 的类型
- Android {
- // ...
- defaultConfig {
- // ...
- externalNativeBuild {
- cmake {
- cppFlags ""
- }
- }
- ndk {
- abiFilters 'armeabi'
- }
- }
- externalNativeBuild {
- cmake {
- path "CMakeLists.txt"
- }
- }
- }
5. 编写 C/C++ 代码
- int generateJPEG(BYTE* data, int w, int h, int quality, const char* outfilename, jboolean optimize) {
- int nComponent = 3;
- // jpeg 的结构体, 保存的比如宽, 高, 位深, 图片格式等信息
- struct jpeg_compress_struct jcs;
- 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;
- }
- 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;
- jcs.input_components = nComponent;
- if (nComponent == 1)
- jcs.in_color_space = JCS_GRAYSCALE;
- else
- 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;
- }
6. 构建 so 库
项目链接: https://github.com/zengfw/EffectiveBitmap
来源: http://www.tuicool.com/articles/IfENFr7