问题

工作中遇到了Android中有关图片压缩保存的问题,发现这个问题还挺深,而且网上资料比较有限,因此自己深入研究了一下,算是把这个问题自顶至下全部搞懂了,在此记录。

相关的几个问题如下:

1.Android系统是如何编码压缩保存图片的?

2.Skia库起到的作用?

3.libJpeg库起到的作用?

4.能不能自己调用Skia或libJpeg?

解答

一谈到Android上的图片压缩保存,基本都会想到android.graphics.Bitmap这个类,它提供了一个非常方便(事实上也只有这一个)的方法:

public boolean compress (Bitmap.CompressFormat format, int quality, OutputStream stream)

这个方法可以把当前的bitmap,根据参数提供的压缩格式(JPEG、PNG、WEBP)和压缩质量,将压缩好的数据输出到指定的输出流中。再跟进到这个函数中,发现如下代码,ok,又进入了神秘的native层,只能查看android的源码了

    public boolean compress(CompressFormat format, int quality, OutputStream stream) {
checkRecycled("Can't compress a recycled bitmap");
// do explicit check before calling the native method
if (stream == null) {
throw new NullPointerException();
}
if (quality < 0 || quality > 100) {
throw new IllegalArgumentException("quality must be 0..100");
}
return nativeCompress(mNativeBitmap, format.nativeInt, quality,
stream, new byte[WORKING_COMPRESS_STORAGE]);
}

在源码中的\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) 

ok,这时大致可以回答第二个问题了——Skia库起到的作用。上层的compress函数其实最终调用的就是Skia的Bitmap_compress函数,java这层基本上啥也没做,99%的工作都是在native中调用skia库中的函数完成的。再解释一下这个函数的各个参数。其中,前两个参数是JNI函数必带的,bitmap是SkBitmap类型指针,在创建该Bitmap时分配。Format是压缩格式,有JPEG、PNG和WEBP三种。quality是压缩质量,0-100的整数。jstream是从java层传过来的输出流,用来将压缩好的图片数据输出,Jstorage是用于native层压缩类和输出流之间传递数据的。

接下来继续分析一下Bitmap_compress函数的内部,代码很好理解,而且大部分我都加了注释,

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
int format, int quality,
jobject jstream, jbyteArray jstorage) {
SkImageEncoder::Type fm; //创建类型变量
//将java层类型变量转换成Skia的类型变量
switch (format) {
case kJPEG_JavaEncodeFormat:
fm = SkImageEncoder::kJPEG_Type;
break;
case kPNG_JavaEncodeFormat:
fm = SkImageEncoder::kPNG_Type;
break;
case kWEBP_JavaEncodeFormat:
fm = SkImageEncoder::kWEBP_Type;
break;
default:
return false;
}
//判断当前bitmap指针是否为空
bool success = false;
if (NULL != bitmap) {
SkAutoLockPixels alp(*bitmap); if (NULL == bitmap->getPixels()) {
return false;
} //创建SkWStream变量用于将压缩后的图片数据输出
SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
if (NULL == strm) {
return false;
}
//根据编码类型,创建SkImageEncoder变量,并调用encodeStream对bitmap
//指针指向的图片数据进行编码,完成后释放资源。
SkImageEncoder* encoder = SkImageEncoder::Create(fm);
if (NULL != encoder) {
success = encoder->encodeStream(strm, *bitmap, quality);
delete encoder;
}
delete strm;
}
return success;
}

如之前所说,该函数调用来skia的encodeStream函数来对图片进行压缩编码。接下来大致介绍一下skia库。

Skia 是一个 c++实现的代码库,在android 中以扩展库的形式存在,目录为external/skia/。总体来说skia是个相对简单的库,在android中提供了基本的画图和简单的编解码功能。另外,skia 同样可以挂接其他第3方编码解码库或者硬件编解码库,例如libpng和libjpeg。在Android中skia就是这么做的,\external\skia\src\images文件夹下面,有几个SkImageDecoder_xxx.cpp文件,他们都是继承自SkImageDecoder.cpp类,并利用第三方库对相应类型文件解码,最后再通过SkTRegistry注册,代码如下所示,

 static SkTRegistry<SkImageDecoder*, SkStream*> gDReg(sk_libjpeg_dfactory);
static SkTRegistry<SkImageDecoder::Format, SkStream*> gFormatReg(get_format_jpeg);
static SkTRegistry<SkImageEncoder*, SkImageEncoder::Type> gEReg(sk_libjpeg_efactory);

至此,第一个问题也得到了解答,Android编码保存图片就是通过Java层函数——Native层函数——Skia库函数——对应第三方库函数(例如libjpeg),这一层层调用做到的。Android真是做到了“善假于物也”。

接下来分析第三方库中究竟是如何对位图(Bitmap)编码。由于工作中只涉及到了jpeg编码,因此我仅研究了Android中libjpeg中的编码方式,以及和标准libjpeg的区别。在\external\jpeg文件夹下面是google用于编译libjpeg.so库的代码和配置文件。需要注意的是,这份代码和libjpeg提供的标准版6b版本(http://sourceforge.net/projects/libjpeg/files/libjpeg/6b/)是不同的。我大致比较过两份代码的区别:主要是Android版修改/添加了一些额外的支持,

1.Android版本修改了内存管理方式,使用自己的方式。

2.Android版添加了把压缩数据输出到输出流的支持。

接下来讲一下libjpeg压缩图片的流程,这部分网上的资料就非常多了,因为libjpeg是个跨平台的开源库,只要有代码,不仅在Android系统,其他系统上依然可以编译出库。整个流程非常简单,直接上代码和注释

    //声明一些在压缩时需要的变量,jerr用于错误控制
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jerr.output_message=android_output_message; //使用自定义的日志输出函数,不是必须的
jerr.error_exit=myjpeg_error_exit; //使用自定义的错误退出函数,不是必须的
jpeg_create_compress(&cinfo); //创建libjpeg的压缩结构体 cinfo.image_width = width; //设置被压缩图片的宽、高、通道数和色彩空间
cinfo.image_height = height;
cinfo.input_components = ;
cinfo.in_color_space = JCS_RGB;
FILE * outfile; //创建文件变量用于指定压缩数据的输出目标
if ((outfile = fopen(imgPath, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", imgPath);
exit();
}
jpeg_set_defaults(&cinfo); //对cinfo做一些默认设置
jpeg_stdio_dest(&cinfo, outfile); //将之前的outfile作为输出目标
jpeg_set_quality(&cinfo,quality,TRUE); //设置压缩jpeg图片的质量
jpeg_start_compress(&cinfo, TRUE); //开始压缩
unsigned char * srcImg=(unsigned char *)imageData; //逐行的获取图像数据,进行压缩处理
while (cinfo.next_scanline < cinfo.image_height) {
JSAMPROW row_pointer[]; /* pointer to JSAMPLE row[s] */
row_pointer[] = srcImg;
(void) jpeg_write_scanlines(&cinfo, row_pointer, );
srcImg+=widthStep;
}
//压缩保存完毕,对使用到的变量进行销毁
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);

至此,第三个问题也可以回答了,真正干活(对图像进行编码压缩)的才是libjpeg。

最后,说一下最后一个问题。理论上,是可以自己调用skia和libjpeg库函数的。有两种方式,一种是通过自己获取源代码,编译出自己的skia或libjpeg库,然后使用。这种做法也是网上写的最多的,优点是自己可以随意改代码,想怎么编码怎么编码,灵活度比较大,缺点就是最后生成的动态链接库会比较大。第二种方法是通过调用系统自带的动态链接库来使用库函数,优点是只需要在编译自己的动态库时包括进头文件即可,最终生成的库很小,缺点是灵活度较低,而且skia和libjpeg随着Android版本和生产商不同,版本也会改变,容易出现链接失败,即调用库函数失败。具体怎么用完全看自己的需求了。自己编译skia和lijpeg的网上例子很多也很容易做,在此不做介绍了。我介绍一下如何使用系统的动态链接库,

1.下载一份android系统的源码,把\external\jpeg下的.h头文件都复制到一个目录下,我为了方便,直接放在了工程的jni目录下,注意不能用libjpeg官网上面的头文件,因为版本可能对不上。

2.编写Android.mk文件,需要注意的是LOCAL_LDLIBS :=里面一定要加上-ljpeg,下面是我的mk文件,一些编译选项都是摘抄Android源码里面的

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := AndroidJpegTest
LOCAL_SRC_FILES := AndroidJpegTest.cpp
LOCAL_LDLIBS :=-llog -ljpeg -ljnigraphics
LOCAL_C_INCLUDES := $(LOCAL_PATH)
LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays
LOCAL_CFLAGS += -DUSE_ANDROID_ASHMEM
LOCAL_CFLAGS += -DAVOID_TABLES
LOCAL_CFLAGS += -DANDROID_TILE_BASED_DECODE
LOCAL_SDK_VERSION := include $(BUILD_SHARED_LIBRARY)

3.编写自己的测试cpp文件,基本按照上面将的libjpeg使用流程调用即可,需要注意的是libjpeg接受的输入色彩空间没有RGBA,因此需要自己把bitmap的RGBA转换成RGB,Skia里面是直接从RGBA转成YUV的。我的测试代码如下,功能很简单,接收一个bitmap、一个保存路径和一个质量因子,按照要求把bitmap保存成jpg图片。

#ifdef __cplusplus
extern "C" {
#endif #include <jni.h>
#include <stdio.h>
#include <stdlib.h>
#include<android/bitmap.h>
#include<android/log.h>
#include"jpeglib.h" #define TAG "JPEGTEST"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__) static void myjpeg_error_exit(j_common_ptr jcs)
{
jpeg_error_mgr* error = (jpeg_error_mgr*)jcs->err;
(*error->output_message) (jcs);
jpeg_destroy(jcs);
exit(EXIT_FAILURE);
} static void android_output_message(j_common_ptr cinfo) {
char buffer[];
/* Create the message */
(*cinfo->err->format_message)(cinfo, buffer);
LOGI("%s", buffer);
} JNIEXPORT jint Java_com_example_yuvconv_NativeFunc_convert
(JNIEnv *env, jclass thiz, jobject bmpObj,jstring filepath,jint quality)
{
const char *imgPath = env->GetStringUTFChars(filepath, );
AndroidBitmapInfo bmpinfo = {};
if (AndroidBitmap_getInfo(env, bmpObj, &bmpinfo) < )
{
LOGI("read failed");
return JNI_FALSE;
} int width = bmpinfo.width;
int height =bmpinfo.height;
int widthStep = (width*+)/*;
if(bmpinfo.width <= || bmpinfo.height <= ||
bmpinfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
{
LOGI("format error");
return JNI_FALSE;
}
void* bmpFromJObject = NULL;
if (AndroidBitmap_lockPixels(env,bmpObj,(void**)&bmpFromJObject) < )
{
LOGI("lockPixels failed");
return JNI_FALSE;
}
unsigned char*imageData= (unsigned char*)malloc(sizeof(unsigned char)*(width*+)/**height);
unsigned char* pBuff = (unsigned char*)bmpFromJObject;
unsigned char* pImgData = imageData;
for (int y = ; y < height; y++)
{
unsigned char* p1 = pImgData;
unsigned char* p2 = pBuff;
for (int x = ; x < width; x++)
{
p1[] = p2[]; //R
p1[] = p2[]; //G
p1[] = p2[]; //B
p1 += ;
p2 += ;
}
pImgData +=widthStep;
pBuff += bmpinfo.stride;
}
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jerr.output_message=android_output_message;
jerr.error_exit=myjpeg_error_exit;
jpeg_create_compress(&cinfo); cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = ;
cinfo.in_color_space = JCS_RGB; FILE * outfile;
if ((outfile = fopen(imgPath, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", imgPath);
return JNI_FALSE;
} jpeg_set_defaults(&cinfo);
jpeg_stdio_dest(&cinfo, outfile);
jpeg_set_quality(&cinfo,quality,TRUE); jpeg_start_compress(&cinfo, TRUE);
unsigned char * srcImg=(unsigned char *)imageData;
while (cinfo.next_scanline < cinfo.image_height) {
JSAMPROW row_pointer[]; /* pointer to JSAMPLE row[s] */
row_pointer[] = srcImg;
(void) jpeg_write_scanlines(&cinfo, row_pointer, );
srcImg+=widthStep;
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
env->ReleaseStringUTFChars(filepath, imgPath);
return JNI_TRUE;
} #ifdef __cplusplus
}
#endif

4.编译的时候,会发现提示找不到libjpeg.so库,找一部手机,从system/lib下面把libjpeg.so抓出来,然后放在编译提示找不到库的那个目录下,我的目录是\android-ndk-r10d\toolchains\arm-linux-androideabi-4.8\prebuilt\windows-x86_64\lib\gcc\arm-linux-androideabi\4.8

5.重新编译,大功告成!把java的调用部分写好测试一下,没问题就ok了。可以看到,这样生成的so库只有10k多,比用libjpeg源码编译的几百k的库小很多。

调用系统的skia库也是类似的过程,不过skia变动的比较频繁,不建议这么使用,如果有需要还是用源码编译自己的libskia比较好。

参考文献

skia 文档:http://chromium-skia-gm.commondatastorage.googleapis.com/doxygen/doxygen/html/index.html

skia 源码解析 http://www.eoeandroid.com/thread-27841-1-1.html

使用系统自带libjpeg时问题 http://stackoverflow.com/questions/5208817/failing-to-link-against-libjpeg-so-in-jni-ndk-shared-library

Android图片编码机制深度解析(Bitmap,Skia,libJpeg)的更多相关文章

  1. [转]Android事件分发机制完全解析,带你从源码的角度彻底理解(上)

    Android事件分发机制 该篇文章出处:http://blog.csdn.net/guolin_blog/article/details/9097463 其实我一直准备写一篇关于Android事件分 ...

  2. 【转】Android事件分发机制完全解析,带你从源码的角度彻底理解(下)

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/9153761 记得在前面的文章中,我带大家一起从源码的角度分析了Android中Vi ...

  3. Android事件分发机制完全解析,带你从源码的角度彻底理解

    Android事件构成 在Android中,事件主要包括点按.长按.拖拽.滑动等,点按又包括单击和双击,另外还包括单指操作和多指操作.所有这些都构成了Android中的事件响应.总的来说,所有的事件都 ...

  4. android之handler机制深入解析

    一.android中需要另开线程处理耗时.网络的任务,但是有必须要在UI线程中修改组件.这样做是为了: ①只能在UI线程中修改组件,避免了多线程造成组件显示混乱 ②不使用加锁策略是为了提高性能,因为a ...

  5. Android异步消息处理机制完全解析,带你从源码的角度彻底理解(转)

    开始进入正题,我们都知道,Android UI是线程不安全的,如果在子线程中尝试进行UI操作,程序就有可能会崩溃.相信大家在日常的工作当中都会经常遇到这个问题,解决的方案应该也是早已烂熟于心,即创建一 ...

  6. 【转】Android异步消息处理机制完全解析,带你从源码的角度彻底理解

    原文网址:http://blog.csdn.net/guolin_blog/article/details/9991569 转载请注明出处:http://blog.csdn.net/guolin_bl ...

  7. Android NFC标签 开发深度解析 触碰的艺术

    有几天没有更新博客了,不过本篇却准备了许久,希望能带给每一位开发者最简单高效的学习方式.废话到此为止,下面开始正文. NFC(Near Field Communication,近场通信)是一种数据传输 ...

  8. Android Handler 消息机制原理解析

    前言 做过 Android 开发的童鞋都知道,不能在非主线程修改 UI 控件,因为 Android 规定只能在主线程中访问 UI ,如果在子线程中访问 UI ,那么程序就会抛出异常 android.v ...

  9. [学习总结]6、Android异步消息处理机制完全解析,带你从源码的角度彻底理解

    开始进入正题,我们都知道,Android UI是线程不安全的,如果在子线程中尝试进行UI操作,程序就有可能会崩溃.相信大家在日常的工作当中都会经常遇到这个问题,解决的方案应该也是早已烂熟于心,即创建一 ...

随机推荐

  1. MySQL5.7 利用keepalived来实现mysql双主高可用方案的详细过程

    Reference:  http://blog.csdn.net/mchdba/article/details/51377989 服务器准备 Keepalived:192.168.13.15 Keep ...

  2. Android背景颜色渐变

    ●使用XML的方式为背景添加渐变效果 1.在res/drawable文件夹里添加一个jbshape.xml文件,然后写入如下代码: <?xml version="1.0" e ...

  3. centos 配置subversion svn

    Subversion是一个自由,开源的版本控制系统. 1.安装: yum install subversion 2.配置 mkdir -p /var/svn/svnrepos #新建文件夹 svnad ...

  4. mysql处理varchar类型的between和and的时间问题少一天解决;

    select * from table where CJSJ day),"%Y-%m-%d") 数据库中的时间类型是varchar,传入的类型也是string 上述sql的意思是: ...

  5. [转]bootstrap的table插件动态加载表头

    原文地址:https://blog.csdn.net/abubu123/article/details/78060321 bootstrap的table属性已经很熟悉了,最近遇到一个问题,犹豫每个列表 ...

  6. [转]【MySQL】关于时间的查询,比如本月,本年,本季度

    原文地址:https://www.cnblogs.com/flydkPocketMagic/p/7089324.html -- mysql查询本季度 -- 今天 select * from ticke ...

  7. sublime Text2下安装php code sniffer插件

    为了跟团队保持开发规范的一致性,需要安装sublime Text2的php code sniffer插件,之前是用的phpfmt插件,发现两个规范还是有点不一样,需要再安装php code sniff ...

  8. mac OS X中升级php5.5至php5.6 or php7

    在做php项目中,提示“Warning. PHP 5.6 or newer is required. Please, upgrade your local PHP installation.” 通过p ...

  9. WPF DataTomplate中Command无效

    问题:在DataTomplate中添加一个Button,Button添加Command,但是Command生效. 原因:ItemTemplate的DataContext指代不明,需要改为父类的Data ...

  10. C语言 · 数组排序去重

    算法训练 数组排序去重   时间限制:1.0s   内存限制:512.0MB        问题描述 输入10个整数组成的序列,要求对其进行升序排序,并去掉重复元素. 输入格式 10个整数. 输出格式 ...