版權聲明
簡介隨著目前設備像素的不斷提高,基本隨便一張照片就是M級別的大小,對於如此大的圖片,不管是在內存空間、帶寬資源和伺服器數據空間上都是非常耗費的,特別是在移動端,由圖片引起的OOM和圖片上傳質量過大等問題我想大家都遇到過,所以對於圖片內存佔用上和物理空間佔用上進行壓縮很有必要。
在Android上,我們使用到的圖片格式無非這五種:PNG、JPEG、Webp、SVG、GIF。其中GIF的位深為8位,所以文件通常比較小而且支持alpha通道以及動畫,Webp在等質量的大小上和等大小的清晰度上都占極大優勢,而SVG矢量圖是由xml文件進行描述的,可以適配於任何解析度的設備而保證圖像不失真,Google的官方視頻中也提到可用這兩種格式進行某些場景下替換PNG或JPEG圖像,這不但能節約帶寬資源還能提高圖片載入速度。
所以圖片壓縮主要是對PNG和JPEG這兩種格式,關於圖片的壓縮,有無損壓縮和有損壓縮兩種方式,這兩種壓縮方式區別如下:
無損壓縮:通過對冗餘數據的存儲方式進行優化,該方式不會丟失文件內容,壓縮率受冗餘度的影響,所以壓縮率較低
有損壓縮:通過丟失不會對文件造成太大影響的數據來達到壓縮效果,所以壓縮率較高
其中PNG是無損壓縮格式圖片,JPEG是有損壓縮格式圖片,所以對應的也有各自的壓縮演算法。
在Android系統中,png的壓縮是使用libpng進行壓縮,場景有兩個:編譯階段以及api層調用方式進行壓縮。
其中在編譯階段通過aapt打包工具會對drawable目錄下png圖片進行壓縮,壓縮率大約在40%以下,如果我們對編譯后的apk進行解壓,可以發現解壓后drawable目錄下的png圖片比原先的變小了。但是,也有例外,
NinePatch(.9)
圖片經過編譯反而變大,因為對於
.9
圖片在編譯過程中aapt會對它額外進行處理,使得
.9
圖片會增加2~3個不同類型的
Chunk
塊(
注:api層調用方式進行壓縮不會對
.9
進行額外處理)。
jpeg的壓縮是用libjpeg(7.0後有變化,後面另外說)進行壓縮,場景只有在api層進行調用方式進行壓縮。下面將主要圍繞圖片的壓縮原理、壓縮策略以及在Android上的運用進行講解。
色彩空間在此之前,我們先了解一下ColorSpace(色彩空間)。通常來說,我們看一張彩色圖片,會有幾千甚至上萬種顏色構成,色彩空間就是用來表示圖片所構成的色彩範圍。對於圖片最常用的是使用三原色組成的RGB色彩空間,它最大可表示2^24(16777216)種顏色。
其它的色彩空間還有YUV、CMYK、YCCK等,其中YUV在視頻的開發方面會經常涉及到,它主要用於表示彩色視頻中彩色圖像的顏色空間,因為它節約帶寬,每個像素位深最大不超過12位,最小為6位,在此不過多描述,了解即可。
上面說到RGB色彩空間,它根據每個分量的所佔位數不同又可以分為這兩種:RGB_565、RGB_888,其中帶alpha通道的有這兩種:ARGB_4444、ARGB_8888,區別如下:
RGB_565:每個像素佔兩個位元組,R分量佔5位,G分量佔6位,B分量佔5位,最多能表示2^16(65536)中顏色
RGB_888:每個像素佔三個位元組,R、G、B分量各佔8位,最多能表示2^24(16777216)中顏色
ARGB_4444:每個像素佔兩個位元組,A、R、G、B分量各佔4位,最多能表示2^12(4096)中顏色,成像效果比較差,所以Google給了它一個Deprecated,並且v4.4+后如果使用了它會自動轉成用ARGB_8888
ARGB_8888:每個像素佔四個位元組,A、R、G、B分量各佔8位,最多能表示2^24(16777216)中顏色,其中前面8位alpha(0~255)通道表示每個像素點的透明度
圖片的存儲形式,主要以以下三種形式存在:File、Stream、Bitmap。其中在Android上File主要有PNG、JPEG、XML(VectorDrawable)、Webp和GIF這五種類型格式進行存儲,下面分別對這三種存儲形式以及壓縮方式進行分析。
PNG是一種無損壓縮的圖像存儲格式,正由於它使用的是無損壓縮演算法進行壓縮,所以相同像素寬高的圖像保存為PNG在文件大小上比JPEG往往要大的多,一般是JPEG大小的幾倍左右。由於無損壓縮不會丟失圖像數據,並且支持alpha通道而且完整的保存了圖像數據且無鋸齒,所以一般應用在PS素材或圖標上,這就為什麼不管Android和iOS圖標都是使用的png格式。
PNG圖像根據每個像素位數的不同,可分為三種格式:PNG8、PNG24、PNG32。PNG8隻支持256色,有索引色透明和Alpha透明兩種方式,索引色透明只能簡單的指明一個像素點為透明還是不透明,Alpha透明則支持像素點的透明度,PNG24支持全色1670萬色,只支持不透明,PNG32支持全色1670萬色,在PNG24基礎上增加了8位的alpha分量,支持Alpha透明,目前大部分PNG圖片使用的格式大都為PNG32。
PNG數據結構
其中一個最簡單的PNG圖像應該至少包含PNG文件的簽名
Signature
IHDR
IDAT
以及圖像結束數據塊
IEND
。這三個的數據塊叫做關鍵數據塊,是每一個PNG文件必須包含的,否則PNG文件將無法正常顯示,另外還有
輔助數據塊
,如:
PLTE
(調色板數據塊,僅用於索引PNG)、
tEXt
(文本信息數據塊),這些輔助數據塊是可選的,用於額外表示一個PNG文件的內容。
這些Chunk都由四部分內容組成:
含義如下:
Length:佔4個位元組,表示該Chunk中Data域的長度
Type:佔4個位元組,表示該Chunk的類型,如:IHDR、IDAT等
Data:佔n個位元組,存儲著該Chunk的數據
CRC:佔4個位元組,循環冗餘校驗碼
下面主要說下這三個關鍵數據塊,它們表示的含義如下:
Signature:佔8個位元組,用於表示該文件是一個PNG文件,內容固定
IHDR:佔25位元組,其中Data域佔13個位元組,用於表示圖像的基本信息,如圖像的寬高與位深等,並且它永遠都是第一個數據塊
IDAT:佔n個位元組,用於表示圖像的數據信息,它存儲真實的圖像數據,在一個PNG文件中,該數據塊出現的數量為>=1
IEND:佔12個位元組,用於表示數據塊內容已結束,永遠都是最後一個數據塊,內容固定
如下,我們查看一張最簡單的PNG文件結構:
可以看到這張PNG圖像只包含了
Signature
、
IHDR
、一個
IDAT
以及結束數據塊
IEND
,可以說是最簡單的PNG圖像。
前8個位元組:
89 50 4E 47 0D 0A 1A 0A
描述的為ASCII字元
.PNG
表示該文件為PNG文件。
後續的25個位元組:
00 00 00 0D 49 48 44 52 00 00 00 30 00 00 00 30 08 06 00 00 00 57 02 F9 87
描述的是
IHDR
頭數據塊,其中前4個位元組
00 00 00 0D
表示該數據塊Data域的長度為13位元組,然後是4個位元組
49 48 44 52
描述的該數據塊類型,對應的ASCII字元為
IHDR
,接下來是數據塊真正存儲的數據Data域,最後是4個位元組的CRC校驗碼。關於
IHDR
數據塊的Data,主要有四個我們比較關心的數據:圖像寬高、色深以及顏色類型。其中寬和高各佔4個位元組,位深和顏色類型佔1個位元組,對應位元組為:
00 00 00 30 00 00 00 30 08 06
從中可以得知該PNG圖像寬和高都為
00 00 00 30
(48px),色深為8bit,顏色類型為6(6代錶帶alpha通道的彩色圖像)。
更多細節可以參考W3C對於PNG的標準定義:
NinePatch(.9)圖片是Android上一個可動態伸縮的PNG圖片,原理是在PNG圖像基礎上添加額外的1像素的邊框來描述動態伸縮與內容填充的區域,然後在編譯打包時通過aapt工具對.9圖進行額外處理。
具體是提取所添加的1像素邊框的信息,這些信息會通過額外類型
npTc
和
npOl
的
Chunk
數據塊保存在PNG文件中,當在圖片載入時,會在判斷該圖片是否為.9圖來選擇性的構造一個
NinePatchDrawable
還是
BitmapDrawable
對象,即是一個可對內容區域進行動態伸縮的Drawable,判斷是否為.9圖以及構造一個代碼為:
byte npChunk = bitmap.getNinePatchChunk; if (npChunk != null && NinePatch.isNinePatchChunk(npChunk)) { NinePatchDrawable npDrawable = new NinePatchDrawable(getResources, bitmap, npChunk, new Rect, null); //... }
一個.9圖片可以由自帶的draw9patch工具進行製作,製作後文件的後綴為
.9.png
PNG壓縮
關於對PNG圖片的壓縮,Android默認使用的是libpng庫進行PNG圖片的壓縮,場景有兩個地方:aapt打包時和bitmap.compress時。
所以對於Android中PNG的壓縮或想獲取更好的壓縮率,我們有兩種做法:
屏蔽在aapt打包時默認的libpng的壓縮,我們自己使用第三方壓縮工具進行png圖像的壓縮
對於api層面,使用自己編譯的lib庫替換系統的api進行png的壓縮
對於一些第三方png壓縮工具,有:
通常來說如果我們不滿足於在aapt打包時進行的png圖片壓縮,我們可以通過上面的工具進行png的壓縮,此時,必須屏蔽aapt打包時的壓縮。
我們可以通過gradle的
aaptOptions
配置來屏蔽aapt打包時對png進行壓縮,進而使用我們自己壓縮的png圖片,通過以下配置:
android { aaptOptions { cruncherEnabled false } }
其中這不會屏蔽對.9圖片的處理,所以不影響.9圖的使用。
JPEG
JPEG是一種有損壓縮的圖像存儲格式,不支持alpha通道,由於它具有高壓縮比,在壓縮過程中把重複的數據和無關緊要的數據會選擇性的丟失,所以如果不需要用到alpha通道,那麼大都圖片格式都用該格式。
JPEG數據結構
一張JPEG圖片的數據結構大致如下:
JPEG文件主要是由多個
segment
段組成,每個
segment
又由
標識碼
和
壓縮數據
組成,標識碼由兩個位元組組成,第一個為固定值
0xFF
,而區分每個標識碼的類型主要由第二個進行區分,下面介紹一下常用的標識碼:
FFD8:表示圖像的開始,段名為
SOI
FFE0:表示JFIF數據塊,段名為
APP0
FFC0:表示圖像幀開始,段名為
SOFO
FFC4:表示Huffman表,段名為
DHT
FFDA:表示從上往下開始掃描圖像,段名為
SOS
FFD9:表示圖像結束,段名為
EOI
更多JPEG格式細節可以看JPEG Wiki:
JPEG壓縮
對於JPEG圖片的壓縮,文章開頭說到了Android默認使用的是libjpeg庫進行壓縮的,不過在Android7.0+發生了一點點變化,主要是做了兩點優化
內部使用的JPEG壓縮庫改為
libjpeg-turbo
,這是一個基於libjpeg
的渦輪增壓庫,主要的一特點就是速度比libjpeg
快使用
Huffman
編碼替代Arithmetic
編碼
上面第二點為主要優化點,有興趣的同學可以用一台Android7.0+手機以及7.0以下版本的手機,壓縮相同一張圖片,會發現在相同質量下Android7.0+機子上的壓縮后的圖片大小比7.0以下的要小。
矢量圖是通過一系列的xml標籤進行描述圖像的行為信息,通過xml文件進行存儲,使得它文件比一般PNG、JPEG更小,同時具備高度的伸縮性且不失真,但在Android上的矢量圖(VectorDrawable)描述標籤和廣義上的SVG矢量圖有些差別,在Android具體的對矢量圖進行描述的標籤主要是
vector
與
path
,其中
path
的格式和定義是一樣的,矢量圖的行為內容描述也主要在該標籤中,在支持上:
WebPWebp圖片格式是Google推出的一個支持alpha通道的有損壓縮格式,據Google官方表明,同質量情況下Webp圖像要比JPEG、PNG圖像小25%~45%左右,在支持上Android4.0+版本提供原生支持,使用
libwebp
庫進行編解碼。
GIFGIF圖像最廣泛的應用是用於顯示動畫圖像,它具備文件小且支持alpha通道的優點,不過它是由8位進行表示每個像素的色彩,僅支持256色,所以在對色彩要求比較高的場合不太適合。
Stream
圖片的存儲形式從File轉到內存中時,圖片內容以位元組方式存儲在Stream中,此時所佔的內存大小為File文件大小。
Bitmap
在Android中,任何圖片資源的顯示對象都是通過bitmap來顯示的,除了xml資源則是通過Canvas來繪製的,所以,對於某些純色或者規則類的圖像,可以通過xml進行描述或Canvas來繪製,這樣所佔用的內存比通過bitmap來顯示將少幾個等級。
Bitmap所佔用內存的計算:
pixelWidth * pixelHeight * bytesPerPixel
即Bitmap的寬x高x每像素所佔位元組數,所以相同一個Bitmap對象,對於每像素佔2位元組的RGB_565色彩空間所佔內存是每像素4位元組的ARGB_8888佔用內存的一半。
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中。
對於圖片的壓縮方式,針對內存空間來說主要有兩個類型:
減少虛擬機堆中所佔用的內存大小
減少在硬碟中所佔用的物理內存大小
下面主要說說這兩種類型的壓縮方式
降低色彩位數
所謂的降低色彩位數,就是降低RGB各個分量的位數,如ARGB_8888到RGB_565,每個像素所佔位元組從4個減小到2個位元組,對應的內存大小也節約了一半。
對於每個像素的R、G、B分量的轉換過程,即一個分量佔用8bit需要轉換為5bit(6bit),或者是一個分量佔用5bit(6bit)需要轉換為8bit,這兩個過程叫:壓縮、補償。
壓縮
壓縮是各個分量位數的降低
以RGB_888到RGB_565為例:
RGB_888每個分量佔8位,每個像素的位深為24bit,如下:
R7 R6 R5 R4 R3 R2 R1 R0 | G7 G6 G5 G4 G3 G2 G1 G0 | B7 B6 B5 B4 B3 B2 B1 B0
RGB_565每個分量分別佔5位、6位、5位,每個像素位深為16bit,對於各分量的壓縮,避免不了丟失一些精度,為使得精度丟失最小,我們只需取其高位即可,RGB_888到RGB_565的轉換后最終每個分量值為:
R7 R7 R5 R4 R3 | G7 G6 G5 G4 G3 G2 | B7 B6 B5 B4 B3
補償
補償是各個分量位數的增加
以RGB_565到RGB_888為例:
RGB_565每個分量分別佔5位、6位、5位,每個像素位深為16bit,如下:
R4 R3 R2 R1 R0 | G5 G4 G3 G2 G1 G0 | B4 B3 B2 B1 B0
RGB_888每個分量佔8位,每個像素的位深為24bit,對於各個分量的補償,常用的做法是,用原分量的值進行填充,剩下的用原分量的低位進行循環補償。
RGB_565到RGB_888的轉換后最終每個分量的值為:
R4 R3 R2 R1 R0 R2 R1 R0 | G5 G4 G3 G2 G1 G0 G1 G0 | B4 B3 B2 B1 B0 B2 B1 B0
尺寸壓縮
尺寸壓縮主要是縮減bitmap的大小,當載入一張大圖片時,進行合適的尺寸的壓縮是減小內存佔用的很有效的方法。
SamplingSize
採樣率壓縮,這基本是人人皆知的辦法,在圖片decode階段,先獲取其寬高然後進行判斷是否符合我們預期的,否則進行一定比例的縮放,代碼為:
public Bitmap decode(String filePath) { BitmapFactory.Options options = new BitmapFactory.Options; options.inJustDecodeBounds = true; BitmapFactory.decodeFile(filePath, options); int outWidth = options.outWidth; int outHeight = options.outHeight; options.inSampleSize = computeSampleSize(outWidth, outHeight); return BitmapFactory.decodeFile(filePath, options); }
這種方式採樣率只能支持2的冪次方的值進行縮放,所以一般decode出來的bitmap大小往往不是我們預期的大小,有可能大很多也有可能小很多
Matrix
Matrix矩陣變換,可以對bitmap進行非常多的操作,其中一項是對bitmap進行等比縮放,這種方式可以精確的縮放到符合我們預期的bitmap大小,代碼如下:
int bitmapWidth = bitmap.getWidth; int bitmapHeight = bitmap.getHeight; Matrix matrix = new Matrix; float rate = computeScaleRate(bitmapWidth, bitmapHeight); matrix.postScale(rate, rate); Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmapWidth, bitmapHeight, matrix, true);
設備屏幕密度與drawable關係
我們知道在不同屏幕密度的設備下,會選擇最適合該設備的資源目錄進行資源的載入,如對於屏幕dpi為480的則會最優選擇
xxhdpi
目錄,關於解析度與dpi以及屏幕密度對應關係為:
如果沒有對應的目錄,則會使用默認目錄。假如有一設備dpi為480,並且沒有xxdpi目錄下沒有該圖片資源,那麼在進行圖片資源decode時候,會把圖片適配到對應的屏幕解析度,進行放大或縮小,縮放比例是:
寬、高 * (當前設備dpi/目標資源目錄對應的dpi)
對於獲取設備的dpi以及density可由Resources靜態方法方便獲取:
DisplayMetrics displayMetrics = Resources.getSystem.getDisplayMetrics; int densityDpi = displayMetrics.densityDpi; int density = displayMetrics.density; int width = displayMetrics.widthPixels; int height = displayMetrics.heightPixels;
一些建議
對於某些高清無碼比較大的圖片,如一些背景或者引導圖等,可由第三方壓縮工具進行壓縮後放入assets目錄中,避免可能在drawable目錄下載入引起的放大導致消耗內存過大或縮小導致圖片過小等問題。
質量壓縮
對於質量壓縮,也就是圖片的所佔物理內存的大小,主要是通過一些lib庫進行壓縮,如Android默認的
bitmap.compress
,可選擇PNG與JPEG等進行壓縮,如果不滿足於內置的lib壓縮庫效果,可自己選擇替換系統api進行壓縮。在上述JPEG這節中說的Android底層所用libjpeg庫在7.0+版本變化,主要是進行了JPEG圖片壓縮的優化,所以為了彌補在Android7.0以下對JPEG壓縮的質量問題以及對bitmap壓縮進行合理適配,特此寫了一個開源庫
Tiny,對於JPEG的壓縮選擇和Android7.0后一樣的庫
libjpeg-turbo
庫進行壓縮,同時也開啟了
Huffman
編碼。
Tiny地址:
下面是使用Tiny圖片壓縮庫進行壓縮與微信朋友圈的效果對比示例:
Reference
Image compression for Android developers - Google I/O 2016:https://www.youtube.com/watch?v=r_LpCi6DQME&index=2&list=PLWz5rJ2EKKc8jQTUYvIfqA9lMvSGQWtte
Android Support Library 23.2:
WebP Compression Techniques in Detail:
Managing Bitmap Memory:
活動推薦:
由InfoQ主辦的第二屆GMTC全球移動技術大會即將來襲!大會將於6月9-10日在北京舉行。本屆大會,我們將探討智能時代的大前端,2017年都有哪些值得關注的大前端趨勢和實踐?掃描二維碼或點擊閱讀原文進入大會官網,現在報名享8折優惠!