Android图⽚编码机制深度解析(Bitmap,Skia,libJpeg)
问题
⼯作中遇到了Android中有关图⽚压缩保存的问题,发现这个问题还挺深,⽽且⽹上资料⽐较有限,因此⾃⼰深⼊研究了⼀下,算是把这个问题⾃顶⾄下全部搞懂了,在此记录。
相关的⼏个问题如下:
1.Android系统是如何编码压缩保存图⽚的?
2.Skia库起到的作⽤?
3.libJpeg库起到的作⽤?
4.能不能⾃⼰调⽤Skia或libJpeg?
解答
⼀谈到Android上的图⽚压缩保存,基本都会想到aphics.Bitmap这个类,它提供了⼀个⾮常⽅便(事实上也只有这⼀个)的⽅法:
public boolean compress ( format, int quality, 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注册,代码如下所⽰,
1static SkTRegistry<SkImageDecoder*, SkStream*> gDReg(sk_libjpeg_dfactory);
2static SkTRegistry<SkImageDecoder::Format, SkStream*> gFormatReg(get_format_jpeg);
3static SkTRegistry<SkImageEncoder*, SkImageEncoder::Type> gEReg(sk_libjpeg_efactory);
⾄此,第⼀个问题也得到了解答,Android编码保存图⽚就是通过Java层函数——Native层函数——Skia库函数——对应第三⽅库函数(例如libjpeg),这⼀层层调⽤做到的。Android真是做到了“善假于物也”。
1.Android版本修改了内存管理⽅式,使⽤⾃⼰的⽅式。
2.Android版添加了把压缩数据输出到输出流的⽀持。
接下来讲⼀下libjpeg压缩图⽚的流程,这部分⽹上的资料就⾮常多了,因为libjpeg是个跨平台的开源库,只要有代码,不仅在Android系统,其他系统上依然可以编译出库。整个流程⾮常简单,直接上代码和注释
//声明⼀些在压缩时需要的变量,jerr⽤于错误控制
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
< = jpeg_std_error(&jerr);
jerr.output_message=android_output_message; //使⽤⾃定义的⽇志输出函数,不是必须的
<_exit=myjpeg_error_exit; //使⽤⾃定义的错误退出函数,不是必须的
jpeg_create_compress(&cinfo); //创建libjpeg的压缩结构体 cinfo.image_width = width; //设置被压缩图⽚的宽、⾼、通道数和⾊彩空间 cinfo.image_height = height;
cinfo.input_components = 3;
cinfo.in_color_space = JCS_RGB;
FILE * outfile; //创建⽂件变量⽤于指定压缩数据的输出⽬标
if ((outfile = fopen(imgPath, "wb")) == NULL) {
fprintf(stderr, "can't open %s\n", imgPath);
exit(1);
}
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 (_scanline < cinfo.image_height) {
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
row_pointer[0] = srcImg;
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
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 := 17
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[2048];
/* 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, 0);
图片下载站源码AndroidBitmapInfo bmpinfo = {0};
if (AndroidBitmap_getInfo(env, bmpObj, &bmpinfo) < 0)
{
LOGI("read failed");
return JNI_FALSE;
}
int width = bmpinfo.width;
int height =bmpinfo.height;
int widthStep = (width*3+3)/4*4;
if(bmpinfo.width <= 0 || bmpinfo.height <= 0 ||
bmpinfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
{
LOGI("format error");
return JNI_FALSE;
}
void* bmpFromJObject = NULL;
if (AndroidBitmap_lockPixels(env,bmpObj,(void**)&bmpFromJObject) < 0)
{
LOGI("lockPixels failed");
return JNI_FALSE;
}
unsigned char*imageData= (unsigned char*)malloc(sizeof(unsigned char)*(width*3+3)/4*4*height); unsigned char* pBuff = (unsigned char*)bmpFromJObject;
unsigned char* pImgData = imageData;
for (int y = 0; y < height; y++)
{
unsigned char* p1 = pImgData;
unsigned char* p2 = pBuff;
for (int x = 0; x < width; x++)
{
p1[0] = p2[0]; //R
p1[1] = p2[1]; //G
p1[2] = p2[2]; //B
p1 += 3;
p2 += 4;
}
pImgData +=widthStep;
pBuff += bmpinfo.stride;
}
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
< = jpeg_std_error(&jerr);
jerr.output_message=android_output_message;
<_exit=myjpeg_error_exit;
jpeg_create_compress(&cinfo);
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = 3;
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 (_scanline < cinfo.image_height) {
JSAMPROW row_pointer[1]; /* pointer to JSAMPLE row[s] */
row_pointer[0] = srcImg;
(void) jpeg_write_scanlines(&cinfo, row_pointer, 1);
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⽐较好。
参考⽂献
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论