|
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,下载完成后解压
如果你要在模拟器上运行,需要编译成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目录,
然后使用autoconf生成Configure配置脚本
如果报错,提示你需要安装libtool工具
sudo apt-get install libtool |
完成后就可以编写我们的shell脚本了,在libjpeg-turbo-1.5.3目录下用vi编辑器新建一个脚本。
因为我有图形界面,所以我用图形界面的编辑器操作更方便。
然后安装官网上编译相应架构的库的提示,
将shell脚本,直接复制过来,然后修改相关配置。
比如,修改ndk路径
NDK_PATH =/home/lishuji/android -ndk -r14b |
注意你的虚拟机中需要下载了NDK,并且配置好环境变量。
修改最低的android版本
此外还要加上下面一句话,设置最终编译出的静态库和动态库存放的目录,我们就在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脚本。
最终生成的目录如下:
其中lib文件夹下就是生成的静态库和动态库,include中包含了我们需要的头文件。 四、AS中集成,编码 我们将上面那些复制到我们的本机上来,然后新建一个AS项目,勾选 Include C++ Support。创建完成后,将include文件夹和lib文件夹下的库复制到我们的cpp目录中。
最终的项目结构如下(这里我只用了一个.a静态库)
然后修改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方法压缩产生的图片区别。
可以看出我们使用native方法压缩的图片比使用bitmap原生方法小了10k,别小看这10k,当图片多了以后就是很大的流量,对性能优化来说,就优化了很多,而且压缩出来的图片质量也高于用bitmap的方法压缩出来的图片。这张图片原本比较清晰,可能看不出来较大差别,但是还是看的出来一些不同。如果你使用原本就比较模糊的图片,使用bitmap压缩的话,最终的效果可能惨不忍睹,
----------------------------
原文链接:https://blog.csdn.net/SakuraMashiro/article/details/79182239
程序猿的技术大观园:www.javathinker.net
[这个贴子最后由 flybird 在 2020-03-09 09:57:15 重新编辑]
|
|