問題
工作中遇到了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注冊,代碼如下所示,
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 := 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; 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
