Android平臺影象壓縮方案

蘇策發表於2017-11-27

關於作者

郭孝星,程式設計師,吉他手,主要從事Android平臺基礎架構方面的工作,歡迎交流技術方面的問題,可以去我的Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。

文章目錄

  • 一 質量壓縮
    • 1.1 實現方法
    • 1.2 實現原理
  • 二 尺寸壓縮
    • 2.1 鄰近取樣
    • 2.2 雙線性取樣

本篇文章用來介紹Android平臺的影象壓縮方案以及影象編解碼的通識性理解,事實上Android平臺對影象的處理最終都交由底層實現,篇幅有限,我們這裡不會去過多的分析底層的細節實現細節,但是
我們會提一下底層的實現方案概覽,給向進一步擴充套件的同學提供一些思路。

在介紹影象壓縮方案之前,我們先要了解一下和壓縮相關的影象的基本知識,這也可以幫助我們理解Bitmap.java裡定義的一些變數的含義。

畫素密度

畫素密度指的是每英寸畫素數目,在Bitmap裡用mDensity/mTargetDensity,mDensity預設是裝置螢幕的畫素密度,mTargetDensity是圖片的目標畫素密度,在載入圖片時就是 drawable 目錄的畫素密度。

色彩模式

色彩模式是數字世界中表示顏色的一種演算法,在Bitmap裡用Config來表示。

  • ARGB_8888:每個畫素佔四個位元組,A、R、G、B 分量各佔8位,是 Android 的預設設定;
  • RGB_565:每個畫素佔兩個位元組,R分量佔5位,G分量佔6位,B分量佔5位;
  • ARGB_4444:每個畫素佔兩個位元組,A、R、G、B分量各佔4位,成像效果比較差;
  • Alpha_8: 只儲存透明度,共8位,1位元組;

另外提一點Bitmap計算大小的方法。

Bitamp 佔用記憶體大小 = 寬度畫素 x (inTargetDensity / inDensity) x 高度畫素 x (inTargetDensity / inDensity)x 一個畫素所佔的記憶體

在Bitmap裡有兩個獲取記憶體佔用大小的方法。

  • getByteCount():API12 加入,代表儲存 Bitmap 的畫素需要的最少記憶體。
  • getAllocationByteCount():API19 加入,代表在記憶體中為 Bitmap 分配的記憶體大小,代替了 getByteCount() 方法。

在不復用 Bitmap 時,getByteCount() 和 getAllocationByteCount 返回的結果是一樣的。在通過複用 Bitmap 來解碼圖片時,那麼 getByteCount() 表示新解碼圖片佔用記憶體的大
小,getAllocationByteCount() 表示被複用 Bitmap真實佔用的記憶體大小(即 mBuffer 的長度)。

除了以上這些概念,我們再提一下Bitmap.java裡的一些成員變數,這些變數大家在可能也經常遇到,要理解清楚。

  • private byte[] mBuffer:影象陣列,用來儲存影象,這個Java層的陣列實際上是在C++層建立的,下面會說明這個問題。
  • private final boolean mIsMutable:影象是否是可變的,這麼說有點抽象,它就像String與StringBuffer的關係一樣,String是不可修改的,StringBuffer是可以修改的。
  • private boolean mRecycled:影象是否已經被回收,影象的回收也是在C++層完成的。

瞭解完基本的概念,我們來分析壓縮影象的方法。

Android平臺壓縮影象的手段通常有兩種:

  • 質量壓縮
  • 尺寸壓縮

一 質量壓縮

1.1 實現方法

質量壓縮的關鍵在於Bitmap.compress()函式,該函式不會改變影象的大小,但是可以降低影象的質量,從而降低儲存大小,進而達到壓縮的目的。

compress(CompressFormat format, int quality, OutputStream stream)複製程式碼

它有三個引數

  • CompressFormat format:壓縮格式,它有JPEG、PNG、WEBP三種選擇,JPEG是有失真壓縮,PNG是無失真壓縮,壓縮後的影象大小不會變化(也就是沒有壓縮效果),WEBP是Google推出的
    影象格式,它相比JPEG會節省30%左右的空間,處於相容性和節省空間的綜合考慮,我們一般會選擇JPEG。
  • int quality:0~100可選,數值越大,質量越高,影象越大。
  • OutputStream stream:壓縮後影象的輸出流。

我們來寫個例子驗證一下。

File file = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
                            , "timo_compress_quality_100.jpg");
if (!file.exists()) {
    try {
        file.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timo);
BufferedOutputStream bos = null;
try {
    bos = new BufferedOutputStream(new FileOutputStream(file));
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
    bitmap.recycle();
} catch (FileNotFoundException e) {
    e.printStackTrace();
}finally {
    try {
        if(bos != null){
            bos.close();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}複製程式碼

quality = 100

1823x1076 1.16m

Android平臺影象壓縮方案

quality = 50

1823x1076 124.52k

Android平臺影象壓縮方案

quality = 0

1823x1076 35.80k

Android平臺影象壓縮方案

可以看到隨著quality的降低,影象質量發生了明顯的變化,但是影象的尺寸沒有發生變化。

1.2 實現原理

Android圖片的編碼是由Skia庫來完成的。

Skia是一個開源的二維圖形庫,提供各種常用的API,並可在多種軟硬體平臺上執行。谷歌Chrome瀏覽器、Chrome OS、安卓、火狐瀏覽器、火狐操作
系統以及其它許多產品都使用它作為圖形引擎。

Skia在external/skia包中,我們雖然在平時的開發中沒有直接用到Skia,但它對我們太重要了,它
是Android系統的重要組成部分,很多重要操作例如影象編解碼,Canvas繪製在底層都是通過Skia來完成的。它同樣被廣泛用於Google的其他產品中。

Skia在src/images包下定義了各種格式圖片的編解碼器。

kImageEncoder.cpp

  • SkJpegEncoder.cpp:JPEG解碼器
  • SkPngEncoder.cpp:PNG解碼器
  • SkWebpEncoder.cpp:WEBP解碼器

Skia本身提供了基本的畫圖和編解碼功能,它同時還掛載了其他第三方編解碼庫,例如:libpng.so、libjpeg.so、libgif.so、所以我們上面想要編碼成jpeg影象最終是由libjpeg來完成的。
上面也提到,我們做影象壓縮,一般選擇的JPEG,我們重點來看看JPEG的編解碼。

libjpeg是一個完全用C語言編寫的處理JPEG影象資料格式的自由庫。它包含一個JPEG編解碼器的演算法實現,以及用於處理JPEG資料的多種實用程式。

Android並非採用原生的libjpeg,而是做了一些修改,具體說來:

  • 修改了記憶體管理的方式
  • 增加了把壓縮資料輸出到輸出流的支援

libjpeg原始碼在external/jpeg包下,接下來我們具體看看JPEG壓縮的實現。

我們再來從上到下看看整個原始碼的實現流程。

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");
    }
    Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "Bitmap.compress");
    boolean result = nativeCompress(mNativePtr, format.nativeInt,
            quality, stream, new byte[WORKING_COMPRESS_STORAGE]);
    Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
    return result;
}複製程式碼

可以看到它在內部呼叫的是一個native方法nativeCompress(),這是定義在Bitmap.java裡的一個函式,它的實現在Bitmap.cpp

它最終呼叫的是Bitmap.cpp裡的Bitmap_compress()函式,我們來看看它的實現。

static bool Bitmap_compress(JNIEnv* env, jobject clazz, SkBitmap* bitmap,
                            int format, int quality,
                            jobject jstream, jbyteArray jstorage) {
    SkImageEncoder::Type fm;

    //根據編碼型別選擇SkImageEncoder
    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;
        }

        //根據編碼型別,建立對應的編碼器,對bitmap指標指向的影象資料進行壓縮並輸出到輸出流
        SkImageEncoder* encoder = SkImageEncoder::Create(fm);
        if (NULL != encoder) {
            //呼叫encodeStream進行編碼
            success = encoder->encodeStream(strm, *bitmap, quality);
            delete encoder;
        }
        delete strm;
    }
    return success;
}複製程式碼

可以看到該函式根據編碼格式選擇SkImageEncoder,從而建立對應的影象編碼器,最後
呼叫encodeStream(strm, *bitmap, quality)方法來完成編碼。通

上面的程式碼建立了SkJpegEncoder,並最終呼叫了它裡面的make()方法,如下所示:

std::unique_ptr<SkEncoder> SkJpegEncoder::Make(SkWStream* dst, const SkPixmap& src,
                                               const Options& options) {
    if (!SkPixmapIsValid(src, options.fBlendBehavior)) {
        return nullptr;
    }
    std::unique_ptr<SkJpegEncoderMgr> encoderMgr = SkJpegEncoderMgr::Make(dst);
    if (setjmp(encoderMgr->jmpBuf())) {
        return nullptr;
    }
    if (!encoderMgr->setParams(src.info(), options)) {
        return nullptr;
    }
    //設定壓縮質量
    jpeg_set_quality(encoderMgr->cinfo(), options.fQuality, TRUE);
    //開始壓縮
    jpeg_start_compress(encoderMgr->cinfo(), TRUE);
    sk_sp<SkData> icc = icc_from_color_space(src.info());
    if (icc) {
        // Create a contiguous block of memory with the icc signature followed by the profile.
        sk_sp<SkData> markerData =
                SkData::MakeUninitialized(kICCMarkerHeaderSize + icc->size());
        uint8_t* ptr = (uint8_t*) markerData->writable_data();
        memcpy(ptr, kICCSig, sizeof(kICCSig));
        ptr += sizeof(kICCSig);
        *ptr++ = 1; // This is the first marker.
        *ptr++ = 1; // Out of one total markers.
        memcpy(ptr, icc->data(), icc->size());
        jpeg_write_marker(encoderMgr->cinfo(), kICCMarker, markerData->bytes(), markerData->size());
    }
    return std::unique_ptr<SkJpegEncoder>(new SkJpegEncoder(std::move(encoderMgr), src));
}複製程式碼

上面就是整個影象壓縮的流程。

一般情況下,Android自帶的libjpeg就可以滿足日常的開發需求,如果業務對高質量和低儲存的需求比較大,可以考慮一下以下兩個庫:

  • libjpeg-turbo:增強版libjpeg,它是一種JPEG影象編解碼器,它使用SIMD指令(MMX,SSE2,NEON,AltiVec)來加速x86,x86-64,ARM和
    PowerPC系統上的基準JPEG壓縮和解壓縮。 在這樣的系統上,libjpeg-turbo的速度通常是libjpeg的2-6倍,其他的都是相等的。 在其他型別的系統上,依靠其高度優化的Huffman編碼例程,libjpeg-turbo仍然
    可以勝過libjpeg。 在許多情況下,libjpeg-turbo的效能與專有的高速JPEG編解碼器相媲美。
  • mozilla/mozjpeg:基於libjpeg-turbo.實現,保證不降低影象質量且相容主流編解碼器的情況下進行jpeg壓縮。

二 尺寸壓縮

尺寸壓縮本質上就是一個重新取樣的過程,放大影象稱為上取樣,縮小影象稱為下采樣,Android提供了兩種影象取樣方法,鄰近取樣和雙線性取樣。

2.1 鄰近取樣

鄰近取樣採用鄰近點插值演算法,用一個畫素點代替鄰近的畫素點,

它的實現程式碼大家也非常熟悉。

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 1;
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red, options);
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
        + "/timo_BitmapFactory_1.png";
ImageUtils.save(bitmap, savePath, Bitmap.CompressFormat.PNG);複製程式碼

inSampleSize = 1

Android平臺影象壓縮方案

inSampleSize = 32

Android平臺影象壓縮方案

可以看到這種方式的關鍵在於inSampleSize的選擇,它決定了壓縮後影象的大小。

inSampleSize代表了壓縮後的影象一個畫素點代表了原來的幾個畫素點,例如inSampleSize為4,則壓縮後的影象的寬高是原來的1/4,畫素點數是原來的1/16,inSampleSize
一般會選擇2的指數,如果不是2的指數,內部計算的時候也會像2的指數靠近。

關於inSampleSize的計算,Luban提供了很好的思路,作者也給出了演算法思路。

演算法思路

1. 判斷影象比例值,是否處於以下區間內;
  - [1, 0.5625)    即影象處於 [1:1 ~ 9:16) 比例範圍內
  - [0.5625, 0.5)  即影象處於 [9:16 ~ 1:2) 比例範圍內
  - [0.5, 0)       即影象處於 [1:2 ~ 1:∞) 比例範圍內
2. 判斷影象最長邊是否過邊界值;
  - [1, 0.5625)   邊界值為:1664 * n(n=1), 4990 * n(n=2), 1280 * pow(2, n-1)(n≥3)
  - [0.5625, 0.5) 邊界值為:1280 * pow(2, n-1)(n≥1)
  - [0.5, 0)      邊界值為:1280 * pow(2, n-1)(n≥1)
3. 計算壓縮影象實際邊長值,以第2步計算結果為準,超過某個邊界值則:width / pow(2, n-1),height/pow(2, n-1)
4. 計算壓縮影象的實際檔案大小,以第2、3步結果為準,影象比例越大則檔案越大。  
    size = (newW * newH) / (width * height) * m;
  - [1, 0.5625) 則 width & height 對應 1664,4990,1280 * n(n≥3),m 對應 150,300,300;
  - [0.5625, 0.5) 則 width = 1440,height = 2560, m = 200;
  - [0.5, 0) 則 width = 1280,height = 1280 / scale,m = 500;注:scale為比例值
5. 判斷第4步的size是否過小
  - [1, 0.5625) 則最小 size 對應 60,60,100
  - [0.5625, 0.5) 則最小 size 都為 100
  - [0.5, 0) 則最小 size 都為 100
6. 將前面求到的值壓縮影象 width, height, size 傳入壓縮流程,壓縮影象直到滿足以上數值複製程式碼

具體實現

private int computeSize() {
    int mSampleSize;

    mSourceWidth = mSourceWidth % 2 == 1 ? mSourceWidth + 1 : mSourceWidth;
    mSourceHeight = mSourceHeight % 2 == 1 ? mSourceHeight + 1 : mSourceHeight;

    mSourceWidth = mSourceWidth > mSourceHeight ? mSourceHeight : mSourceWidth;
    mSourceHeight = mSourceWidth > mSourceHeight ? mSourceWidth : mSourceHeight;

    double scale = ((double) mSourceWidth / mSourceHeight);

    if (scale <= 1 && scale > 0.5625) {
      if (mSourceHeight < 1664) {
        mSampleSize = 1;
      } else if (mSourceHeight >= 1664 && mSourceHeight < 4990) {
        mSampleSize = 2;
      } else if (mSourceHeight >= 4990 && mSourceHeight < 10240) {
        mSampleSize = 4;
      } else {
        mSampleSize = mSourceHeight / 1280 == 0 ? 1 : mSourceHeight / 1280;
      }
    } else if (scale <= 0.5625 && scale > 0.5) {
      mSampleSize = mSourceHeight / 1280 == 0 ? 1 : mSourceHeight / 1280;
    } else {
      mSampleSize = (int) Math.ceil(mSourceHeight / (1280.0 / scale));
    }

    return mSampleSize;
}複製程式碼

核心思想就是通過對原圖寬高的比較計算出合適的取樣值。

同樣的我們也來看看這種方式的底層實現原理,BitmapFactory裡有很多decode方法,它們最終呼叫的是native方法。

private static native Bitmap nativeDecodeStream(InputStream is, byte[] storage,
        Rect padding, Options opts);
private static native Bitmap nativeDecodeFileDescriptor(FileDescriptor fd,
        Rect padding, Options opts);
private static native Bitmap nativeDecodeAsset(long nativeAsset, Rect padding, Options opts);
private static native Bitmap nativeDecodeByteArray(byte[] data, int offset,
        int length, Options opts);複製程式碼

這些native方法在BitmapFactory.cpp裡實現,這些方法最終呼叫的是doDecode()方法

static jobject doDecode(JNIEnv* env, SkStream* stream, jobject padding,
        jobject options, bool allowPurgeable, bool forcePurgeable = false,
        bool applyScale = false, float scale = 1.0f) {
    int sampleSize = 1;
    //影象解碼模式,這裡是畫素點模式
    SkImageDecoder::Mode mode = SkImageDecoder::kDecodePixels_Mode;
    //引數初始化
    SkBitmap::Config prefConfig = SkBitmap::kARGB_8888_Config;
    bool doDither = true;
    bool isMutable = false;
    bool willScale = applyScale && scale != 1.0f;
    bool isPurgeable = !willScale &&
            (forcePurgeable || (allowPurgeable && optionsPurgeable(env, options)));
    bool preferQualityOverSpeed = false;

    //javaBitmap物件
    jobject javaBitmap = NULL;
    //對options裡的引數進行初始化
    if (options != NULL) {
        sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
        if (optionsJustBounds(env, options)) {
            mode = SkImageDecoder::kDecodeBounds_Mode;
        }
        // initialize these, in case we fail later on
        env->SetIntField(options, gOptions_widthFieldID, -1);
        env->SetIntField(options, gOptions_heightFieldID, -1);
        env->SetObjectField(options, gOptions_mimeFieldID, 0);
        jobject jconfig = env->GetObjectField(options, gOptions_configFieldID);
        prefConfig = GraphicsJNI::getNativeBitmapConfig(env, jconfig);
        isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
        doDither = env->GetBooleanField(options, gOptions_ditherFieldID);
        preferQualityOverSpeed = env->GetBooleanField(options,
                gOptions_preferQualityOverSpeedFieldID);
        javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
    }
    if (willScale && javaBitmap != NULL) {
        return nullObjectReturn("Cannot pre-scale a reused bitmap");
    }

    //建立影象解碼器,並設定從Java層傳遞過來的引數,例如sampleSize、doDither等
    SkImageDecoder* decoder = SkImageDecoder::Factory(stream);
    if (decoder == NULL) {
        return nullObjectReturn("SkImageDecoder::Factory returned null");
    }
    decoder->setSampleSize(sampleSize);
    decoder->setDitherImage(doDither);
    decoder->setPreferQualityOverSpeed(preferQualityOverSpeed);
    NinePatchPeeker peeker(decoder);
    //Java的畫素分配器
    JavaPixelAllocator javaAllocator(env);
    SkBitmap* bitmap;
    if (javaBitmap == NULL) {
        bitmap = new SkBitmap;
    } else {
        if (sampleSize != 1) {
            return nullObjectReturn("SkImageDecoder: Cannot reuse bitmap with sampleSize != 1");
        }
        bitmap = (SkBitmap*) env->GetIntField(javaBitmap, gBitmap_nativeBitmapFieldID);
        // config of supplied bitmap overrules config set in options
        prefConfig = bitmap->getConfig();
    }
    SkAutoTDelete<SkImageDecoder> add(decoder);
    SkAutoTDelete<SkBitmap> adb(bitmap, javaBitmap == NULL);
    decoder->setPeeker(&peeker);
    if (!isPurgeable) {
        decoder->setAllocator(&javaAllocator);
    }
    AutoDecoderCancel adc(options, decoder);
    // To fix the race condition in case "requestCancelDecode"
    // happens earlier than AutoDecoderCancel object is added
    // to the gAutoDecoderCancelMutex linked list.
    if (options != NULL && env->GetBooleanField(options, gOptions_mCancelID)) {
        return nullObjectReturn("gOptions_mCancelID");
    }
    SkImageDecoder::Mode decodeMode = mode;
    if (isPurgeable) {
        decodeMode = SkImageDecoder::kDecodeBounds_Mode;
    }

    //解碼
    SkBitmap* decoded;
    if (willScale) {
        decoded = new SkBitmap;
    } else {
        decoded = bitmap;
    }
    SkAutoTDelete<SkBitmap> adb2(willScale ? decoded : NULL);
    if (!decoder->decode(stream, decoded, prefConfig, decodeMode, javaBitmap != NULL)) {
        return nullObjectReturn("decoder->decode returned false");
    }

    //縮放操作
    int scaledWidth = decoded->width();
    int scaledHeight = decoded->height();
    if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
        scaledWidth = int(scaledWidth * scale + 0.5f);
        scaledHeight = int(scaledHeight * scale + 0.5f);
    }
    // 更新選項引數
    if (options != NULL) {
        env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
        env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
        env->SetObjectField(options, gOptions_mimeFieldID,
                getMimeTypeString(env, decoder->getFormat()));
    }

    //處於justBounds模式,不再建立Bitmap物件,直接返回,這個很熟悉吧,對應了
    //options.inJustDecodeBounds = true,直解析大小,不實際載入影象
    if (mode == SkImageDecoder::kDecodeBounds_Mode) {
        return NULL;
    }
    jbyteArray ninePatchChunk = NULL;
    if (peeker.fPatchIsValid) {
        if (willScale) {
            scaleNinePatchChunk(peeker.fPatch, scale);
        }
        size_t ninePatchArraySize = peeker.fPatch->serializedSize();
        ninePatchChunk = env->NewByteArray(ninePatchArraySize);
        if (ninePatchChunk == NULL) {
            return nullObjectReturn("ninePatchChunk == null");
        }
        jbyte* array = (jbyte*) env->GetPrimitiveArrayCritical(ninePatchChunk, NULL);
        if (array == NULL) {
            return nullObjectReturn("primitive array == null");
        }
        peeker.fPatch->serialize(array);
        env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
    }
    // detach bitmap from its autodeleter, since we want to own it now
    adb.detach();

    //處理縮放
    if (willScale) {
        // This is weird so let me explain: we could use the scale parameter
        // directly, but for historical reasons this is how the corresponding
        // Dalvik code has always behaved. We simply recreate the behavior here.
        // The result is slightly different from simply using scale because of
        // the 0.5f rounding bias applied when computing the target image size
        const float sx = scaledWidth / float(decoded->width());
        const float sy = scaledHeight / float(decoded->height());
        bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
        bitmap->allocPixels(&javaAllocator, NULL);
        bitmap->eraseColor(0);
        SkPaint paint;
        paint.setFilterBitmap(true);
        SkCanvas canvas(*bitmap);
        canvas.scale(sx, sy);
        canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
    }

    //處理影象的邊距
    if (padding) {
        if (peeker.fPatchIsValid) {
            GraphicsJNI::set_jrect(env, padding,
                    peeker.fPatch->paddingLeft, peeker.fPatch->paddingTop,
                    peeker.fPatch->paddingRight, peeker.fPatch->paddingBottom);
        } else {
            GraphicsJNI::set_jrect(env, padding, -1, -1, -1, -1);
        }
    }
    SkPixelRef* pr;
    if (isPurgeable) {
        pr = installPixelRef(bitmap, stream, sampleSize, doDither);
    } else {
        // if we get here, we're in kDecodePixels_Mode and will therefore
        // already have a pixelref installed.
        pr = bitmap->pixelRef();
    }
    if (!isMutable) {
        // promise we will never change our pixels (great for sharing and pictures)
        pr->setImmutable();
    }
    if (javaBitmap != NULL) {
        // If a java bitmap was passed in for reuse, pass it back
        return javaBitmap;
    }
    // 建立Bitmap物件並返回
    return GraphicsJNI::createBitmap(env, bitmap, javaAllocator.getStorageObj(),
            isMutable, ninePatchChunk);
}複製程式碼

我們發現在最後呼叫了createBitmap()方法來建立Bitmap物件,這個方法在Graphics.cpp裡定義的,我們來看看它是如何建立Bitmap的。

jobject GraphicsJNI::createBitmap(JNIEnv* env, SkBitmap* bitmap, jbyteArray buffer,
                                  bool isMutable, jbyteArray ninepatch, int density)
{
    SkASSERT(bitmap);
    SkASSERT(bitmap->pixelRef());
    //呼叫Java方法,建立一個物件
    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            static_cast<jint>(reinterpret_cast<uintptr_t>(bitmap)),
            buffer, isMutable, ninepatch, density);
    hasException(env); // For the side effect of logging.
    //返回Bitmap物件
    return obj;
}複製程式碼

可以看到最終C++層呼叫JNI方法建立了Java層的Bitmap物件,至此,整個BitmapFactory的解碼流程我們就分析完了。

2.2 雙線性取樣

雙線性取樣採用雙線性插值演算法,相比鄰近取樣簡單粗暴的選擇一個畫素點代替其他畫素點,雙線性取樣參考源畫素相應位置周圍2x2個點的值,根據相對位置取對應的權重,經過計算得到目標影象。

它的實現方式也很簡單

Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.blue_red);
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
Bitmap sclaedBitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth()/2, bitmap.getHeight()/2, matrix, true);
String savePath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + "/timo_BitmapFactory_1.png";
ImageUtils.save(bitmap, savePath, Bitmap.CompressFormat.PNG);複製程式碼

這種方式的關鍵在於Bitmap.createBitmap(Bitmap source, int x, int y, int width, int height, Matrix m, boolean filter)方法。

這個方法有七個引數:

  • Bitmap source:源影象
  • int x:目標影象第一個畫素的x座標
  • int y:目標影象第一個畫素的y座標
  • int width:目標影象的寬度(畫素點個數)
  • int height:目標影象的高度(畫素點個數)
  • Matrix m:變換矩陣
  • boolean filter:是否開啟過濾

我們來看看它的實現。

  public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height,
            Matrix m, boolean filter) {

        //引數校驗
        ...

        int neww = width;
        int newh = height;
        Canvas canvas = new Canvas();
        Bitmap bitmap;
        Paint paint;

        Rect srcR = new Rect(x, y, x + width, y + height);
        RectF dstR = new RectF(0, 0, width, height);

        //選擇影象的編碼格式,和源影象保持一致
        Config newConfig = Config.ARGB_8888;
        final Config config = source.getConfig();
        // GIF files generate null configs, assume ARGB_8888
        if (config != null) {
            switch (config) {
                case RGB_565:
                    newConfig = Config.RGB_565;
                    break;
                case ALPHA_8:
                    newConfig = Config.ALPHA_8;
                    break;
                //noinspection deprecation
                case ARGB_4444:
                case ARGB_8888:
                default:
                    newConfig = Config.ARGB_8888;
                    break;
            }
        }

        if (m == null || m.isIdentity()) {
            bitmap = createBitmap(neww, newh, newConfig, source.hasAlpha());
            paint = null;   // not needed
        } else {
            final boolean transformed = !m.rectStaysRect();

            //通過Matrix變換獲取新的影象寬高
            RectF deviceR = new RectF();
            m.mapRect(deviceR, dstR);

            neww = Math.round(deviceR.width());
            newh = Math.round(deviceR.height());

            //傳入影象引數到底層,建立愛女Bitmap物件
            bitmap = createBitmap(neww, newh, transformed ? Config.ARGB_8888 : newConfig,
                    transformed || source.hasAlpha());

            canvas.translate(-deviceR.left, -deviceR.top);
            canvas.concat(m);

            paint = new Paint();
            paint.setFilterBitmap(filter);
            if (transformed) {
                paint.setAntiAlias(true);
            }
        }

        // The new bitmap was created from a known bitmap source so assume that
        // they use the same density
        bitmap.mDensity = source.mDensity;
        bitmap.setHasAlpha(source.hasAlpha());
        bitmap.setPremultiplied(source.mRequestPremultiplied);

        canvas.setBitmap(bitmap);
        canvas.drawBitmap(source, srcR, dstR, paint);
        canvas.setBitmap(null);

        return bitmap;
    }複製程式碼

可以看到這個方法又呼叫了它的同名方法createBitmap(neww, newh, transformed ? Config.ARGB_8888 : newConfig,transformed || source.hasAlpha())
該方法當然也是藉由底層的native方法實現Bitmap的建立。

private static native Bitmap nativeCreate(int[] colors, int offset,
                                              int stride, int width, int height,
                                              int nativeConfig, boolean mutable);複製程式碼

這個方法對應著Bitmap.cpp裡的Bitmap_creator()方法。

static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              int offset, int stride, int width, int height,
                              SkBitmap::Config config, jboolean isMutable) {
    if (NULL != jColors) {
        size_t n = env->GetArrayLength(jColors);
        if (n < SkAbs32(stride) * (size_t)height) {
            doThrowAIOOBE(env);
            return NULL;
        }
    }

    //SkBitmap物件
    SkBitmap bitmap;

    //設定影象配置資訊
    bitmap.setConfig(config, width, height);

    //建立影象陣列,這裡對應著Bitmap.java裡的mBuffers
    jbyteArray buff = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
    if (NULL == buff) {
        return NULL;
    }

    if (jColors != NULL) {
        GraphicsJNI::SetPixels(env, jColors, offset, stride,
                               0, 0, width, height, bitmap);
    }

    //建立Bitmap物件,並返回
    return GraphicsJNI::createBitmap(env, new SkBitmap(bitmap), buff, isMutable, NULL);
}複製程式碼

可以看到上面呼叫allocateJavaPixelRef()方法來建立影象陣列,該方法在Graphics.cpp裡定義的。

jbyteArray GraphicsJNI::allocateJavaPixelRef(JNIEnv* env, SkBitmap* bitmap,
                                             SkColorTable* ctable) {
    Sk64 size64 = bitmap->getSize64();
    if (size64.isNeg() || !size64.is32()) {
        jniThrowException(env, "java/lang/IllegalArgumentException",
                          "bitmap size exceeds 32bits");
        return NULL;
    }
    size_t size = size64.get32();
    //呼叫Java層的方法建立一個Java陣列
    jbyteArray arrayObj = env->NewByteArray(size);
    if (arrayObj) {
        // TODO: make this work without jniGetNonMovableArrayElements
       //獲取陣列地址
        jbyte* addr = jniGetNonMovableArrayElements(&env->functions, arrayObj);
        if (addr) {
            SkPixelRef* pr = new AndroidPixelRef(env, (void*) addr, size, arrayObj, ctable);
            bitmap->setPixelRef(pr)->unref();
            // since we're already allocated, we lockPixels right away
            // HeapAllocator behaves this way too
            bitmap->lockPixels();
        }
    }
    return arrayObj;
}複製程式碼

建立完成影象陣列後,就接著呼叫createBitmap()建立Java層的Bitmap物件,這個我們在上面已經說過,自此Bitmap.createBitmap()方法的實現流程我們也分析完了。

以上便是Android原生支援的兩種取樣方式,如果這些並不能滿足你的業務需求,可以考慮以下兩種方式。

  • 雙立方/雙三次取樣:雙立方/雙三次取樣使用的是雙立方/雙三次插值演算法。鄰近點插值演算法的目標畫素值由源圖上單個畫素決定,雙線性內插值演算法由源畫素某點周圍 2x2 個畫素點按一定權重獲得,而雙立
    方/雙三次插值演算法更進一步參考了源畫素某點周圍 4x4 個畫素。這個演算法在 Android 中並沒有原生支援,如果需要使用,可以通過手動編寫演算法或者引用第三方演算法庫,這個演算法在 ffmpeg 中已經給到了支援,
    具體的實現在 libswscale/swscale.c 檔案中:FFmpeg Scaler Documentation。
  • Lanczos 取樣:Lanczos 取樣和 Lanczos 過濾是 Lanczos 演算法的兩種常見應用,它可以用作低通濾波器或者用於平滑地在取樣之間插入數字訊號,Lanczos 取樣一般用來增加數字訊號的取樣率,或者間隔
    取樣來降低取樣率。

好了,以上就是關於Android平臺處理影象壓縮的全部內容,下一篇文章我們來分析視訊壓縮的實現方案。另外phoenix專案完整的實現了圖片與視訊的壓縮,其中圖片的壓縮就是用的上文提到的
Luban的演算法實現,大家在做專案的時候可以做個參考。

相關文章