问题

工作中遇到了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的源码了

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

在源码中的\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函数的内部,代码很好理解,而且大部分我都加了注释,

  1. static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
  2. int format, int quality,
  3. jobject jstream, jbyteArray jstorage) {
  4. SkImageEncoder::Type fm; //创建类型变量
  5. //将java层类型变量转换成Skia的类型变量
  6. switch (format) {
  7. case kJPEG_JavaEncodeFormat:
  8. fm = SkImageEncoder::kJPEG_Type;
  9. break;
  10. case kPNG_JavaEncodeFormat:
  11. fm = SkImageEncoder::kPNG_Type;
  12. break;
  13. case kWEBP_JavaEncodeFormat:
  14. fm = SkImageEncoder::kWEBP_Type;
  15. break;
  16. default:
  17. return false;
  18. }
  19. //判断当前bitmap指针是否为空
  20. bool success = false;
  21. if (NULL != bitmap) {
  22. SkAutoLockPixels alp(*bitmap);
  23.  
  24. if (NULL == bitmap->getPixels()) {
  25. return false;
  26. }
  27.  
  28. //创建SkWStream变量用于将压缩后的图片数据输出
  29. SkWStream* strm = CreateJavaOutputStreamAdaptor(env, jstream, jstorage);
  30. if (NULL == strm) {
  31. return false;
  32. }
  33. //根据编码类型,创建SkImageEncoder变量,并调用encodeStream对bitmap
  34. //指针指向的图片数据进行编码,完成后释放资源。
  35. SkImageEncoder* encoder = SkImageEncoder::Create(fm);
  36. if (NULL != encoder) {
  37. success = encoder->encodeStream(strm, *bitmap, quality);
  38. delete encoder;
  39. }
  40. delete strm;
  41. }
  42. return success;
  43. }

如之前所说,该函数调用来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系统,其他系统上依然可以编译出库。整个流程非常简单,直接上代码和注释

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

  1. LOCAL_PATH := $(call my-dir)
  2.  
  3. include $(CLEAR_VARS)
  4. LOCAL_MODULE := AndroidJpegTest
  5. LOCAL_SRC_FILES := AndroidJpegTest.cpp
  6. LOCAL_LDLIBS :=-llog -ljpeg -ljnigraphics
  7. LOCAL_C_INCLUDES := $(LOCAL_PATH)
  8. LOCAL_CFLAGS += -O3 -fstrict-aliasing -fprefetch-loop-arrays
  9. LOCAL_CFLAGS += -DUSE_ANDROID_ASHMEM
  10. LOCAL_CFLAGS += -DAVOID_TABLES
  11. LOCAL_CFLAGS += -DANDROID_TILE_BASED_DECODE
  12. LOCAL_SDK_VERSION :=
  13.  
  14. include $(BUILD_SHARED_LIBRARY)

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

  1. #ifdef __cplusplus
  2. extern "C" {
  3. #endif
  4.  
  5. #include <jni.h>
  6. #include <stdio.h>
  7. #include <stdlib.h>
  8. #include<android/bitmap.h>
  9. #include<android/log.h>
  10. #include"jpeglib.h"
  11.  
  12. #define TAG "JPEGTEST"
  13. #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__)
  14.  
  15. static void myjpeg_error_exit(j_common_ptr jcs)
  16. {
  17. jpeg_error_mgr* error = (jpeg_error_mgr*)jcs->err;
  18. (*error->output_message) (jcs);
  19. jpeg_destroy(jcs);
  20. exit(EXIT_FAILURE);
  21. }
  22.  
  23. static void android_output_message(j_common_ptr cinfo) {
  24. char buffer[];
  25. /* Create the message */
  26. (*cinfo->err->format_message)(cinfo, buffer);
  27. LOGI("%s", buffer);
  28. }
  29.  
  30. JNIEXPORT jint Java_com_example_yuvconv_NativeFunc_convert
  31. (JNIEnv *env, jclass thiz, jobject bmpObj,jstring filepath,jint quality)
  32. {
  33. const char *imgPath = env->GetStringUTFChars(filepath, );
  34. AndroidBitmapInfo bmpinfo = {};
  35. if (AndroidBitmap_getInfo(env, bmpObj, &bmpinfo) < )
  36. {
  37. LOGI("read failed");
  38. return JNI_FALSE;
  39. }
  40.  
  41. int width = bmpinfo.width;
  42. int height =bmpinfo.height;
  43. int widthStep = (width*+)/*;
  44. if(bmpinfo.width <= || bmpinfo.height <= ||
  45. bmpinfo.format != ANDROID_BITMAP_FORMAT_RGBA_8888)
  46. {
  47. LOGI("format error");
  48. return JNI_FALSE;
  49. }
  50. void* bmpFromJObject = NULL;
  51. if (AndroidBitmap_lockPixels(env,bmpObj,(void**)&bmpFromJObject) < )
  52. {
  53. LOGI("lockPixels failed");
  54. return JNI_FALSE;
  55. }
  56. unsigned char*imageData= (unsigned char*)malloc(sizeof(unsigned char)*(width*+)/**height);
  57. unsigned char* pBuff = (unsigned char*)bmpFromJObject;
  58. unsigned char* pImgData = imageData;
  59. for (int y = ; y < height; y++)
  60. {
  61. unsigned char* p1 = pImgData;
  62. unsigned char* p2 = pBuff;
  63. for (int x = ; x < width; x++)
  64. {
  65. p1[] = p2[]; //R
  66. p1[] = p2[]; //G
  67. p1[] = p2[]; //B
  68. p1 += ;
  69. p2 += ;
  70. }
  71. pImgData +=widthStep;
  72. pBuff += bmpinfo.stride;
  73. }
  74. struct jpeg_compress_struct cinfo;
  75. struct jpeg_error_mgr jerr;
  76. cinfo.err = jpeg_std_error(&jerr);
  77. jerr.output_message=android_output_message;
  78. jerr.error_exit=myjpeg_error_exit;
  79. jpeg_create_compress(&cinfo);
  80.  
  81. cinfo.image_width = width;
  82. cinfo.image_height = height;
  83. cinfo.input_components = ;
  84. cinfo.in_color_space = JCS_RGB;
  85.  
  86. FILE * outfile;
  87. if ((outfile = fopen(imgPath, "wb")) == NULL) {
  88. fprintf(stderr, "can't open %s\n", imgPath);
  89. return JNI_FALSE;
  90. }
  91.  
  92. jpeg_set_defaults(&cinfo);
  93. jpeg_stdio_dest(&cinfo, outfile);
  94. jpeg_set_quality(&cinfo,quality,TRUE);
  95.  
  96. jpeg_start_compress(&cinfo, TRUE);
  97. unsigned char * srcImg=(unsigned char *)imageData;
  98. while (cinfo.next_scanline < cinfo.image_height) {
  99. JSAMPROW row_pointer[]; /* pointer to JSAMPLE row[s] */
  100. row_pointer[] = srcImg;
  101. (void) jpeg_write_scanlines(&cinfo, row_pointer, );
  102. srcImg+=widthStep;
  103. }
  104. jpeg_finish_compress(&cinfo);
  105. jpeg_destroy_compress(&cinfo);
  106. env->ReleaseStringUTFChars(filepath, imgPath);
  107. return JNI_TRUE;
  108. }
  109.  
  110. #ifdef __cplusplus
  111. }
  112. #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. 从 shell 眼中看世界

    (字符) 展开每一次你输入一个命令,然后按下 enter 键,在 bash 执行你的命令之前, bash 会对输入的字符完成几个步骤处理.我们已经知道两三个案例,怎样一个简单的字符序列,例如 “*”, ...

  2. [转]Java中BigDecimal的使用

    原文地址:https://blog.csdn.net/cen_s/article/details/76472834 在日常开发中我们经常会碰到小数计算,而小数直接计算的话会出现一些小小的错误,如下 S ...

  3. TCC分布式事务

    https://github.com/changmingxie/tcc-transaction

  4. mysql hive sql 进阶

    场景: 说明.1.上面的数据是经过规整的数据,step是连续的,这个可以通过row_number实现.连续是必要的一个条件因为在计算第二个查询条件时依赖这个顺序,如果step不是数字字段可以截取然后转 ...

  5. nginx封ip,禁用IP段的设置说明

    nginx的ngx_http_access_module 模块可以封配置内的ip或者ip段,语法如下: deny IP; deny subnet; allow IP; allow subnet; # ...

  6. 【微信小程序】Page页面跳转(路由/返回)并传参

    页面跳转的方法参考官方文档: https://mp.weixin.qq.com/debug/wxadoc/dev/framework/app-service/route.html 问题:使用wx.na ...

  7. <Effective Django>读书笔记

    In Django parlance, a project is the final product, and it assembles one or more applications togeth ...

  8. EhCache 配置信息

    How to Size Caches 官方文档:http://ehcache.org/documentation/configuration/cache-size [maxEntriesLocalHe ...

  9. Aspose Linux下字体找不到报错

    http://www.aspose.com/docs/display/cellsnet/Smart+Markers http://www.aspose.com/docs/display/cellsja ...

  10. CAS (6) —— Nginx代理模式下浏览器访问CAS服务器网络顺序图详解

    CAS (6) -- Nginx代理模式下浏览器访问CAS服务器网络顺序图详解 tomcat版本: tomcat-8.0.29 jdk版本: jdk1.8.0_65 nginx版本: nginx-1. ...