Android圖片編碼機制深度解析(Bitmap,Skia,libJpeg)

問題

工作中遇到了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函數必帶的,bitmapSkBitmap類型指針,在創建該Bitmap時分配。Format是壓縮格式,有JPEGPNGWEBP三種。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

發佈了29 篇原創文章 · 獲贊 18 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章