问题

工作中遇到了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 methodif (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注册,代码如下所示,

1 static SkTRegistry<SkImageDecoder*, SkStream*> gDReg(sk_libjpeg_dfactory);
2 static SkTRegistry<SkImageDecoder::Format, SkStream*> gFormatReg(get_format_jpeg);
3 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 = 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 (cinfo.next_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 := 17include $(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];    //Rp1[1] = p2[1];    //Gp1[2] = p2[2];    //Bp1 += 3;p2 += 4;}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 = 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 (cinfo.next_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比较好。

参考文献

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

转载于:https://www.cnblogs.com/hrlnw/p/4403334.html

Android图片编码机制深度解析(Bitmap,Skia,libJpeg)相关推荐

  1. android nfc标签类型,Android NFC标签 开发深度解析 触碰的艺术

    原标题:Android NFC标签 开发深度解析 触碰的艺术 本文来自于CSDN博客,作者:郭朝,已获授权,版权归原作者所有,未经作者同意,请勿转载. 欢迎同有博客好文章的作者加微信(ID:tm_fo ...

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

    <div id="container">         <div id="header">     <div class=&qu ...

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

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

  4. android面试题(深度解析)

    1.activity的生命周期. 方法 描述 可被杀死 下一个 onCreate() 在activity第一次被创建的时候调用.这里是你做所有初始化设置的地方──创建视图.设置布局.绑定数据至列表等. ...

  5. Android图片编解码实现方案(Skia)

    首先给大家分享一个巨牛巨牛的人工智能教程,是我无意中发现的.教程不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵段子,像看小说一样,哈哈-我正在学习中,觉得太牛了,所以分享给大家!点这里可以跳转 ...

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

    这篇博客是在鸿洋的微信公众号看到的感觉收益非浅.于是转存于自己博客上以后可查阅. 本文由郭朝投稿. 郭朝的博客地址: http://blog.csdn.net/smartbetter 有几天没有更新博 ...

  7. Android异步消息处理机制 全解析

    Android异步消息处理机制主要是指Handler的运行机制以及Hanlder所附带的MessageQueue和Looper的工作过程. 本文将通过分析源码(api-28)的形式,全面解析Handl ...

  8. Android 图片缓存机制

    1.采用线程池  2.内存缓存+文件缓存  3.内存缓存中网上很多是采用SoftReference来防止堆溢出,这儿严格限制只能使用最大JVM内存的1/4  4.对下载的图片进行按比例缩放,以减少内存 ...

  9. Android实战开发Handler机制深度解析

    本文为自己多年来在Android实战开发过程中总结归纳的一些常见问题,现在分享出来希望对初学者有所帮助. 本文出自门心叼龙的博客,转载请注明出处: https://blog.csdn.net/gedu ...

最新文章

  1. C++核心编程(一)
  2. 开源:Angularjs示例--Sonar中项目使用语言分布图
  3. NanoPi NEO Air使用十:自己编写驱动来控制LED
  4. python手机版打了代码运行不了-android手机安装python并写代码运行
  5. spreedrest
  6. 简说设计模式——策略模式
  7. 后缀自动机Suffix Links的应用
  8. 【模拟信号】基于matlab标准调幅信号产生+解调【含Matlab源码 984期】
  9. android木马的制作方法,实现木马病毒的详细步骤
  10. blos硬盘启动台式计算机,最新戴尔台式机bios设置硬盘启动图解
  11. SpringBoot自动装配的魔力
  12. ui和ux的区别_UX和UI设计之间有什么区别?
  13. 日系插画学习笔记(十):色彩基础
  14. 让老公“寒心”的对话 (转)
  15. ubuntu如何安装Mac OS X主题
  16. 英语中的 姓氏/Surname
  17. BDW01手把手系列01:BDW01开发板基于TencentOS Tiny之helloworld!
  18. 中关村修电脑,被坑了
  19. csdn的粉丝老铁及技术小伙伴们拜年
  20. 圆转随意运鸿蒙,解析陈式太极拳的螺旋缠丝劲——陈式太极拳

热门文章

  1. Mr.J--Java接口实现
  2. Hive篇--搭建Hive集群
  3. Mo's Algorithm
  4. 自考--网络经济与企业管理--选择易考题
  5. Tips--解决BeatsX开机白灯闪三下无法连接问题(附拆机教程)
  6. 最详尽使用指南:超快上手Jupyter Notebook
  7. C语言求本金,求本金,试过了,调试了还是不行
  8. 力扣34-在排序数组中查找元素的第一个和最后一个位置(Java,二分,附思路)
  9. Linux传递位置参数,Linux-scripts-位置参数等特殊变量
  10. 基于HTTP请求头字段 User-Agent用户环境 开发多端应用