問題
工作中遇到了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