性能优化之图片压缩性能优化

前文介绍了系统的Bitmap处理方式,那么在这一节中来说一说一个第三方的开源库,又来解决图片压缩的问题

话外题

Android使用Bitmap处理图片,处理出来的JPEG图片质量略差,那么为什么会这样呢?
这里有一个历史问题,当时skia开源引擎用来处理JPEG,Android也采用了这种引擎,然而对其做了阉割处理,也就是去掉了其中的哈夫曼算法,采用了定长编码算法,然而在解码的时候依旧使用了哈夫曼算法,这就使得处理后的图片变大了,这也是基于Android在当时的性能比较低采取的迫不得已做法,这个问题一直延续着
那么,图片的压缩该怎么做呢?也就是说不采用系统的Bitmap API,而是采用哈夫曼算法的压缩,在现在的Android机上,已经不像以前那样性能差了,可以支持哈夫曼算法了
关于霍夫曼编码参见:霍夫曼编码

jpeg开源库使用前准备

在这里,我们使用一个开源库进行图片的哈夫曼编码,用以改善Bitmap自身的不足
首先下载源代码:http://www.ijg.org/
我这里下载的是最新的版本:jpegsrc.v9c.tar.gz
接下来将其编译成so动态库,其套路和之前NDK差不多,首先看一看configure文件的帮助信息

1
./configure --help

然后编译,我这里设置一直存在问题,就参考了这篇文章:编译Android环境的libjpeg-turbo
脚本在编写的时候严格控制空格,可以有制表符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
···
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jni']
}
}
externalNativeBuild {
cmake {
cppFlags ""
}
ndk {
abiFilters "armeabi-v7a"
}
}
···

修改CMakeLists.txt,将so动态库和头文件添加至项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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方法类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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、释放资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
#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里做了下实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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质量的,对比大小如下:

Donate comment here