天天看點

淺析Android平台圖像壓縮方案

在介紹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: heighttargetDensity/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前後,兩張圖檔的對比.

壓縮前的圖檔

壓縮後的圖檔

從上述兩圖可以明顯圖檔品質的差别,另外再通過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平台提供了另一種尺寸壓縮方案

雙線性采樣

雙線性采樣采用雙線性插值算法,相比鄰近采樣簡單粗暴的選擇一個像素點代替其他像素點,雙線性采樣參考源像素相應位置周圍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);
複制代碼
           

壓縮效果

壓縮前

壓縮後

可以看出壓縮後的圖檔不會像鄰近采樣那般隻有純粹的一種顔色,而是參考了像素源周圍2x2個點的像素,并取其權重得到目标圖像。

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

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

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

雙立方/雙三次插值算法經常用于圖像或者視訊的縮放,它能比雙線性内插值算法保留更好的細節品質。

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

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

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

采樣效果 從低到高依次

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

Android平台圖像壓縮方案

QQ音樂團隊分享:Android中的圖檔壓縮技術詳解

也談圖檔壓縮

為什麼圖檔反複壓縮後會普遍會變綠而不是其他顔色

Android之優雅地加載大圖檔

記憶體占用/GPU渲染性能優化手記

另外

個人的github

閑暇之餘寫的故事