我目前所从事的工作是做公司社交类APP的消息服务模块,其中有一个需求就是压缩传输的图片的体积,因为现在一般的手机拍摄的照片体积都在2M左右,所以想办法减小传输过程中的体积是非常有必要的。
一般的处理过程是这样:设定一个固定的分辨率大小,比如960*960,图片解码成bitmap对象后,如果图片的宽或者高超出了960,那么就对宽和高进行等比例缩放,使得长的那一边刚好等于960,然后再进行jpeg编码。
经过这个过程的处理,一般2M左右的图片就会变为100K左右,这个已经是一个不错的结果了,但是既然交给我们平台架构中心来做这件事,肯定要做出一点不同的效果出来。
我的研究成果如下:
Android平台所使用的jpeg编解码库其实是一个叫IJG的组织开发的一个C库,Android的图形库SKIA对其进行了封装,但是SKIA对jpeg库进行封装的时候没有将其中的两个选项暴露出来:optimize encoding选项和progressive选项,而这两个选项都可以减小编码后的体积,并且progressive选项产生的progressive jpeg的用户体验是好于baseline jpeg的用户体验的(目前还没有找到在ImageView上渐进地显示progressive jpeg的方法),所以我要做的工作是使用NDK移植jpeg编解码库,然后暴露这两个选项出来,让业务来调用我的接口。
第一步:使用Android NDK移植jpeg-turbo库
jpeg-turbo库是对jpeg库的一个优化,此处移植jpeg-turbo库
jpeg-turbo库下载链接:http://download.youkuaiyun.com/detail/lihuapinghust/8220993
将下载的zip包解压到android工程的jni目录,在工程的Properties->Builders中新建一个Builder,设置如下:
并且将新建的builder放在第一个位置,然后build,就可以编译so库了
第二部,设计接口
因为我们只在编码的时候做优化,所以我们就要求业务使用Android原生的解码和缩放工具得到要编码的bitmap,然后我们再把bitmap的字节数据拷贝出来,传给我们的接口进行编码操作,所以jni接口部分的代码如下
void write_to_RGB(char * dst, const void* srcRow, int width) {
const uint32_t* src = (const uint32_t*)srcRow;
while (--width >= 0) {
uint32_t c = *src++;
dst[0] = SkGetPackedR32(c);
dst[1] = SkGetPackedG32(c);
dst[2] = SkGetPackedB32(c);
dst += 3;
}
}
JNIEXPORT jint JNICALL Java_com_skynet_compressor_image_NativeJpeg_doJpegCompressBitmap
(JNIEnv * env, jclass cls, jbyteArray byteArray, jint width, jint height, jstring out_file_path, jint quality) {
uint8_t *buf = (uint8_t *)(*env)->GetByteArrayElements(env, byteArray, 0);
jlong capacity = (*env)->GetArrayLength(env, byteArray);
char * c_out_file_path = (char *) (*env)->GetStringUTFChars(env, out_file_path, NULL);
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE * output_file;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
/* Add some application-specific error messages (from cderror.h) */
jerr.addon_message_table = cdjpeg_message_table;
jerr.first_addon_message = JMSG_FIRSTADDONCODE;
jerr.last_addon_message = JMSG_LASTADDONCODE;
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
cinfo.optimize_coding = TRUE;
cinfo.input_gamma = 1;
cinfo.dct_method = JDCT_IFAST;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_simple_progression(&cinfo);
if ((output_file = fopen(c_out_file_path, "wb")) == NULL) {
__android_log_print(ANDROID_LOG_INFO, "native-method", "open outfile failed");
return -1;
}
jpeg_stdio_dest(&cinfo, output_file);
jpeg_start_compress(&cinfo, TRUE);
uint8_t* oneRowP = (uint8_t*)malloc(width * 3);
const void* srcRow = (const void *)buf;
while (cinfo.next_scanline < cinfo.image_height) {
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
write_to_RGB(oneRowP, srcRow, width);
row_pointer[0] = oneRowP;
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
srcRow = (const void*)((const char*)srcRow + width * 4);
}
free(oneRowP);
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
fclose(output_file);
return 0;
}
代码是参考jpeg库本身代码和Android平台源码写的,
然后在Java层声明jni方法:
public static native int doJpegCompressBitmap(byte[] bytes, int width,
int height, String outFilePath, int quality);
然后java层就可以使用我们的接口进行jpeg编码了
得到的文件大小会比使用Androdi原生的编码方法得到的文件小6%左右,虽然提升性能不是特别明显,但是在以用户体验为中心的互联网时代,即使是1K的流量也要为用户节省