在介紹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
-
表示decodingBitmap 的 density,這個值一般跟圖檔放置的目錄有關(hdpi/xxhdpi)density
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
閑暇之餘寫的故事