淺析Android平臺影象壓縮方案

niknowzcd發表於2018-05-29

在介紹Android平臺的壓縮方案之前,先了解一下Bitmap的幾個主要概念。

畫素密度

畫素密度指的是每英寸畫素數目,在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的計算方式

memory=scaledWidth*scaledHeight*每個畫素所佔位元組數
複製程式碼

其中
scaledWidth : widthtargetDensity/density+0.5
scaledHeight: height
targetDensity/density+0.5

  • scaledWidth表示水平方向的畫素值,
  • width表示螢幕寬度,
  • targetDensity表示手機的畫素密度,這個值一般跟手機相關,
  • density表示decodingBitmap 的 density,這個值一般跟圖片放置的目錄有關(hdpi/xxhdpi)

scaledHeight同理

每個畫素所佔位元組數:這個值跟色彩模式相關,預設 ARGB_8888 則是4個位元組,

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

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

兩者的區別:

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

圖片壓縮方式

質量壓縮

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

這裡提到的影象的質量主要指的是圖片的色彩空間

一般影象的色彩空間為RGB,主要通過RGB三原色通道來描述圖片,其中又有ARGB格式,比起RGB多了一個透明度的通道。

Android下的質量壓縮主要通過下面這個函式來實現的。

bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
複製程式碼

三個引數

  • CompressFormat format:壓縮格式,它有JPEG、PNG、WEBP三種選擇,JPEG是有失真壓縮,PNG是無失真壓縮,WEBP是Google推出的影象格式.
  • int quality:0~100可選,數值越大,質量越高,影象越大。
  • OutputStream stream:壓縮後影象的輸出流。

其中PNG是無損格式的,壓縮效果不太理想,而WEBP會存在相容性的問題。出於相容性和效果來看,一般會選擇JPEG作為壓碎格式。

例項程式碼

// R.drawable.thumb 為 png 圖片
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.thumb);
try {
    //儲存壓縮圖片到本地
    File file = new File(Environment.getExternalStorageDirectory(), "aaa.jpg");
    if (!file.exists()) {
        file.createNewFile();
    }
    FileOutputStream fs = new FileOutputStream(file);
    bitmap.compress(Bitmap.CompressFormat.JPEG, 50, fs);
    Log.i(TAG, "onCreate: file.length " + file.length());
    fs.flush();
    fs.close();
} catch (FileNotFoundException e) {
    e.printStackTrace();
} catch (IOException e) {
    e.printStackTrace();
}
//檢視壓縮之後的 Bitmap 大小
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, outputStream);
byte[] bytes = outputStream.toByteArray();
Bitmap compress = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
Log.i(TAG, "onCreate: bitmap.size = " + bitmap.getByteCount() + "   compress.size = " + compress.getByteCount());
複製程式碼

我們再來看看quality引數被設定為50前後,兩張圖片的對比.
壓縮前的圖片

淺析Android平臺影象壓縮方案

壓縮後的圖片

淺析Android平臺影象壓縮方案

從上述兩圖可以明顯圖片質量的差別,另外再通過log列印檢視會壓縮前後圖片的所佔用的大小是一樣的。

bitmap.size = compress.size
複製程式碼

**Q:**這裡可能有人就會有疑惑,為什麼壓縮過後,兩張圖片的大小還會是一樣的呢?

**A:**因為圖片在記憶體中的儲存方式和檔案中的儲存方式是不一樣的。圖片壓縮只會影響檔案的大小,在這個例子中,壓縮過後存到磁碟的檔案大小會比壓縮之前的檔案大小減小很多。

記憶體中所佔的大小沒有變化是因為bitmap沒有變化的原因。

文章最開始提到Bitmap的計算方式

memory=scaledWidth*scaledHeight*每個畫素所佔位元組數
複製程式碼

因為是壓縮的質量,所有寬高都不變,而每個畫素所佔的位元組數跟色彩空間有關,預設是ARGB_8888.寬高不變,色彩空間不重新設定,那麼bitmap所佔的大小就不會發生改變。

說道這裡可能又會有個新疑問

**Q:**bitmap佔用的大小不變,那為什麼圖片質量下降了呢?這是因為圖片被壓縮過了啊!

**A:**首先要知道JPEG格式是有失真壓縮的,JPEG格式的圖片是不支援透明色彩的,這也是JPEG的大小會比PNG小很大,圖片質量會比PNG差的原因。 在經過了bitmap.compress()這個流程時,JPEG會捨去透明屬性.這樣存放到磁碟時的檔案大小就減小了.然後這個時候再通過BitmapFactory.decodeByteArray()把圖片載入回來時,載入的是捨去了透明通道的圖片,按理說應該採用 RGB_565或者RGB_888這樣的色彩空間載入,但是你沒有另外設定這個引數的話,載入的色彩格式會是預設ARGB_8888.圖片都沒有透明的色彩空間了,你再給它分配記憶體就只是浪費記憶體而已。

這也是為什麼壓縮前後,bitmap所佔的大小相同,圖片質量卻有所差距的原因。

補充一個有趣的事件,在早期的Android平臺下,對一張圖片進行多次質量壓縮,會得到一張變綠的圖片。詳情連結

補充一些Android下各格式圖片的儲存方式

WebP

Webp圖片格式是Google推出的一個支援alpha通道的有失真壓縮格式,據Google官方表明,同質量情況下Webp影象要比JPEG、PNG影象小25%~45%左右,在支援上Android4.0+版本提供原生支援,使用libwebp庫進行編解碼。

GIF

GIF影象最廣泛的應用是用於顯示動畫影象,它具備檔案小且支援alpha通道的優點,不過它是由8位進行表示每個畫素的色彩,僅支援256色,所以在對色彩要求比較高的場合不太適合。

Stream

圖片的儲存形式從File轉到記憶體中時,圖片內容以位元組方式儲存在Stream中,此時所佔的記憶體大小為File檔案大小。

Bitmap

在Android中,任何圖片資源的顯示物件都是通過bitmap來顯示的,除了xml資源則是通過Canvas來繪製的,所以,對於某些純色或者規則類的影象,可以通過xml進行描述或Canvas來繪製,這樣所佔用的記憶體比通過bitmap來顯示將少幾個等級。

Bitmap與Drawable的聯絡

關於Bitmap和Drawable的關係,可以看官方的解釋,Drawable是一個抽象的概念,來描述某些具備可繪製的的物件,它是一個抽象類,而Bitmap是一個最簡單的Drawable實體物件,Bitmap並不繼承於Drawable,它們之間建立關聯最終是通過BitmapDrawable物件,該物件會把具體的Bitmap例項物件渲染到Canvas上。Drawable更注重描述的是某繪製的行為,而Bitmap則是注重儲存著影象的畫素資訊。

Bitmap儲存空間

隨著版本的變化以及儲存空間的變化,Bitmap的儲存空間主要有三個地方

Native Memory
Android2.3以下版本,bitmap畫素資料儲存在native記憶體中,釋放記憶體需主動呼叫recycle()方法

Dalvik Heap
Android3.0+版本,在Android2.3版本引入了併發的垃圾回收器後,在3.0以後的版本bitmap的畫素資料則儲存在虛擬機器堆中,不需要主動呼叫recycle()來回收記憶體,gc會主動回收

Ashmem
匿名共享記憶體空間,說到這個,就會聯想起大名鼎鼎的Fresco圖片庫,它巧妙的利用了這一空間來進行Bitmap物件的儲存,對於Ashmem空間,首先想到的是與App程式空間是隔離且互不影響的,這點在Android4.4以下版本是這樣的,在Android4.4+後版本,Ashmem空間將會包含在App所佔用的記憶體空間中。看Fresco原始碼也可以看出,對於4.4+版本,對於Bitmap的解碼使用了另外的解碼器。在Android4.4以下版本如何使用Ashmem進行bitmap的儲存呢?通過DecodeOptions:

options.inPurgeable = true;
options.inInputShareable = true;
複製程式碼

以及通過MemoryFile可將圖片的位元組資料儲存在Ashmem中。

尺寸壓縮

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

鄰近取樣

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

BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Bitmap compress = BitmapFactory.decodeFile("/sdcard/test.png", options);
複製程式碼

其中options.inSampleSize的值代表著壓縮後一個畫素點代替原來的幾個畫素點,比如options.inSampleSize=2,一個畫素點會代替原來的2個畫素點,注意這裡的2個畫素點僅僅指水平方向或者豎直方向上的。即原來2x2的畫素,壓縮後僅使用一個畫素點來代替。

網上找了張圖

壓縮前的圖片

淺析Android平臺影象壓縮方案

壓縮後的圖片

淺析Android平臺影象壓縮方案

壓縮前紅綠相間的圖片,經過壓縮後,完全變成了綠色.這時因為鄰近點插值演算法直接選擇其中一個畫素作為生成畫素,另外一個畫素直接拋棄,這樣才會造成圖片變成純綠色的情況。

考慮到鄰近取樣的方法有些暴力,Android平臺提供了另一種尺寸壓縮方案

雙線性取樣

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

使用例項

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Bitmap compress = Bitmap.createScaledBitmap(bitmap, bitmap.getWidth()/2, bitmap.getHeight()/2, true);
複製程式碼

或者

Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/test.png");
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
bm = Bitmap.createBitmap(bitmap, 0, 0, bit.getWidth(), bit.getHeight(), matrix, true);
複製程式碼

壓縮效果

壓縮前

淺析Android平臺影象壓縮方案

壓縮後

淺析Android平臺影象壓縮方案

可以看出壓縮後的圖片不會像鄰近取樣那般只有純粹的一種顏色,而是參考了畫素源周圍2x2個點的畫素,並取其權重得到目標影象。

雙線性取樣相比鄰近取樣而言,圖片的保真度會高些,但壓縮的速率不及前者,因為前者不需要計算直接選擇了其中一個畫素作為生成畫素。

雙立方/雙三次取樣 (Android原生不支援)

雙立方/雙三次取樣使用的是雙立方/雙三次插值演算法。雙立方/雙三次插值演算法參考了源畫素某點周圍 4x4 個畫素。

雙立方/雙三次插值演算法經常用於影象或者視訊的縮放,它能比雙線性內插值演算法保留更好的細節質量。

雙立方/雙三次插值演算法在平時的軟體中是很常用的一種圖片處理演算法,但是這個演算法有一個缺點就是計算量會相對比較大,是前三種演算法中計算量最大的,軟體 photoshop 中的圖片縮放功能使用的就是這個演算法。

Lanczos 取樣 (原生不支援)###

Lanczos 取樣和 Lanczos 過濾是 Lanczos 演算法的兩種常見應用,它可以用作低通濾波器或者用於平滑地在取樣之間插入數字訊號,Lanczos 取樣一般用來增加數字訊號的取樣率,或者間隔取樣來降低取樣率。

取樣效果 從低到高依次

鄰近取樣--雙線性取樣--雙立方/雙三次取樣--Lanczos 取樣

Android平臺影象壓縮方案
QQ音樂團隊分享:Android中的圖片壓縮技術詳解
也談圖片壓縮
為什麼圖片反覆壓縮後會普遍會變綠而不是其他顏色
Android之優雅地載入大圖片
記憶體佔用/GPU渲染效能優化手記

另外

個人的github
閒暇之餘寫的故事

相關文章