Android Bitmap(點陣圖)詳解

搖頭耶穌發表於2019-06-15

一、背景

在Android開發中,任何一個APP都離不開圖片的載入和顯示問題。這裡的圖片來源分為三種:專案圖片資原始檔(一般為res/drawable目錄下的圖片檔案)、手機本地圖片檔案、網路圖片資源等。圖片的顯示我們一般採用ImageView作為載體,通過ImageView的相應API即可設定其顯示的圖片內容。

我們知道:如果是需要展示專案中的圖片資原始檔,我們只需要呼叫ImageView的setImageResource(int id)方法並傳入該圖片資源的id(一般為R.drawable.xxx)即可。但是如果是需要展示手機本地的某張圖片或者網路上的某個圖片資源,又該怎麼辦呢?——問題A

為了回答問題A,我們先思考一個更深的問題B:Android中是如何將某一張圖片的內容載入到記憶體中繼而由ImageView顯示的呢?

我們知道:如果我們想通過TextView展示一個本地txt檔案的內容,我們只需要由該檔案建立幷包裝一個輸入流物件。通過該輸入流物件即可得到一個代表該檔案內容的字串物件,再將該字串物件交由TextView展示即可。換句話說,這個txt檔案的內容在記憶體中的表達形式就是這個字串物件。

類推一下,雖然圖片檔案也是檔案,但是我們顯然不可能對圖片檔案也採用這種方式:即通過該圖片建立幷包裝一個輸入流物件再獲取一個字串物件。畢竟無論如何我們都無法將某個圖片的內容表示為一個字串物件(細想一下就知道了,你能通過一段話100%準確地描述一張圖片嗎?顯然不現實)。那麼,這就引入了問題C:既然字串物件不行,那麼我們該以哪種物件來在記憶體中表示某個圖片的內容呢?答案就是:Bitmap物件!

二、基本概述

Bitmap,即點陣圖。它本質上就是一張圖片的內容在記憶體中的表達形式。那麼,Bitmap是通過什麼方式表示一張圖片的內容呢?

Bitmap原理:從純數學的角度,任何一個面都由無數個點組成。但是對於圖片而言,我們沒必要用無數個點來表示這個圖片,畢竟單獨一個微小的點人類肉眼是看不清的。換句話說,由於人類肉眼的能力有限,我們只需要將一張圖片表示為 有限但足夠多的點即可。點的數量不能無限,因為無限的點資訊量太大無法儲存;但是點的數量也必須足夠多,否則視覺上無法形成連貫性。這裡的點就是畫素。比如說,某個1080*640的圖片,這裡的畫素總數即為1080X640個。

將圖片內容表示為有限但足夠多的畫素的集合,這個“無限→有限”的思想極其迷人。所以,我們只需要將每個畫素的資訊儲存起來,就意味著將整個圖片的內容進行了表達。

畫素資訊:每個畫素的資訊,無非就是ARGB四個通道的值。其中,A代表透明度,RGB代表紅綠藍三種顏色通道值。每個通道的值範圍在0~255之間,即有256個值,剛好可以通過一個位元組(8bit)進行表示。所以,每個通道值由一個位元組表示,四個位元組表示一個畫素資訊,這似乎是最好的畫素資訊表示方案。

但是這裡忽略了兩個現實的需求問題:

①在實際需求中,我們真的需要這麼多數量的顏色嗎?上述方案是256X256X256種。有的時候,我們並不需要這麼豐富的顏色數量,所以可以適當減少表示每個顏色通道的bit位數。這麼做的好處是節省空間。也就是說,每個顏色通道都採用8bit來表示是代表全部顏色值的集合;而我們可以採用少於8bit的表示方式,儘管這會缺失一部分顏色值,但是隻要顏色夠用即可,並且這還可以節省記憶體空間。

②我們真的需要透明度值嗎?如果我們需要某個圖片作為背景或者圖示,這個圖片透明度A通道值是必要的。但是如果我們只是普通的圖片展示,比如拍攝的照片,透明度值毫無意義。細想一下,你希望你手機自拍的照片透明或者半透明嗎?hell no! 因此,透明度這個通道值是否有必要表示也是根據需求自由變化的。

具體每個畫素點儲存ARGB值的方案介紹,後面會詳細介紹。

總結:Bitmap物件本質是一張圖片的內容在記憶體中的表達形式。它將圖片的內容看做是由儲存資料的有限個畫素點組成;每個畫素點儲存該畫素點位置的ARGB值。每個畫素點的ARGB值確定下來,這張圖片的內容就相應地確定下來了。

現在回答一下問題A和問題B:Android就是將所有的圖片資源(無論是何種來源)的內容以Bitmap物件的形式載入到記憶體中,再通過ImageView的setImageBitmap(Bitmap b)方法即可展示該Bitmap物件所表示的圖片內容。

三、詳細介紹

1、Bitmap.Config

Config是Bitmap的一個列舉內部類,它表示的就是每個畫素點對ARGB通道值的儲存方案。取值有以下四種:

ARGB_8888:這種方案就是上面所說的每個通道值採8bit來表示,每個畫素點需要4位元組的記憶體空間來儲存資料。該方案圖片質量是最高的,但是佔用的記憶體也是最大的

ARGB_4444:這種方案每個通道都是4位,每個畫素佔用2個位元組,圖片的失真比較嚴重。一般不用這種方案。

RGB_565:這種方案RGB通道值分別佔5、6、5位,但是沒有儲存A通道值,所以不支援透明度。每個畫素點佔用2位元組,是ARGB_8888方案的一半。

ALPHA_8:這種方案不支援顏色值,只儲存透明度A通道值,使用場景特殊,比如設定遮蓋效果等。

比較分析:一般我們在ARGB_8888方式和RGB_565方式中進行選取:不需要設定透明度時,比如拍攝的照片等,RGB_565是個節省記憶體空間的不錯的選擇;既要設定透明度,對圖片質量要求又高,就用ARGB_8888。

 

2、Bitmap的壓縮儲存

Bitmap是圖片內容在記憶體中的表示形式,那麼如果想要將Bitmap物件進行持久化儲存為一張本地圖片,需要對Bitmap物件表示的內容進行壓縮儲存。根據不同的壓縮演算法可以得到不同的圖片壓縮格式(簡稱為圖片格式),比如GIF、JPEG、BMP、PNG和WebP等。這些圖片的(壓縮)格式可以通過圖片檔案的字尾名看出。

換句話說:Bitmap是圖片在記憶體中的表示,GIF、JPEG、BMP、PNG和WebP等格式圖片是持久化儲存後的圖片。記憶體中的Bitmap到磁碟上的GIF、JPEG、BMP、PNG和WebP等格式圖片經過了”壓縮”過程,磁碟上的GIF、JPEG、BMP、PNG和WebP等格式圖片到記憶體中的Bitmap經過了“解壓縮”的過程。

那麼,為什麼不直接將Bitmap物件進行持久化儲存而是要對Bitmap物件進行壓縮儲存呢?這麼做依據的思想是:當圖片持久化儲存在磁碟上時,我們應該儘可能以最小的體積來儲存同一張圖片的內容,這樣有利於節省磁碟空間;而當圖片載入到記憶體中以顯示的時候,應該將磁碟上壓縮儲存的圖片內容完整地展開。前者即為壓縮過程,目的是節省磁碟空間;後者即為解壓縮過程,目的是在記憶體中展示圖片的完整內容。

 

3、有失真壓縮和無失真壓縮

Bitmap壓縮儲存時的演算法有很多種,但是整體可分為兩類:有失真壓縮和無失真壓縮。

①有失真壓縮
有失真壓縮的基本依據是:人的眼睛對光線的敏感度遠高於對顏色的敏感度,光線對景物的作用比顏色的作用更為重要。有失真壓縮的原理是:保持顏色的逐漸變化,刪除影象中顏色的突然變化。生物學中的大量實驗證明,人類大腦會自發地利用與附近最接近的顏色來填補所丟失的顏色。有失真壓縮的具體實現方法就是刪除影象中景物邊緣的某些顏色部分。當在螢幕上看這幅圖時,大腦會利用在景物上看到的顏色填補所丟失的顏色部分。利用有失真壓縮技術,某些資料被有意地刪除了,並且在圖片重新載入至記憶體中時這些資料也不會還原,因此被稱為是“有損”的。有失真壓縮技術可以靈活地設定壓縮率。
無可否認,利用有失真壓縮技術可以在點陣圖持久化儲存的過程中大大地壓縮圖片的儲存大小,但是會影響影象質量,這一點在壓縮率很高時尤其明顯。所以需要選擇恰當的壓縮率。

②無失真壓縮
無失真壓縮的基本原理是:相同的顏色資訊只需儲存一次。具體過程是:首先會確定影象中哪些區域是相同的,哪些是不同的。包括了重複資料的區域就可以被壓縮,只需要記錄該區域的起始點即可。
從本質上看,無失真壓縮的方法通過刪除一些重複資料,也能在點陣圖持久化儲存的過程中減少要在磁碟上儲存的圖片大小。但是,如果將該圖片重新讀取到記憶體中,重複資料會被還原。因此,無失真壓縮的方法並不能減少圖片的記憶體佔用量,如果要減少圖片佔用記憶體的容量,就必須使用有失真壓縮方法。
無失真壓縮方法的優點是能夠比較好地儲存影象的質量,但是相對來說這種方法的壓縮率比較低。
對比分析:有失真壓縮壓縮率高而且可以靈活設定壓縮率,並且刪除的資料不可還原,因此可以減少圖片的記憶體佔用,但是對圖片質量會有一定程度的影響;無失真壓縮可以很好地儲存圖片質量,也能保證一定的壓縮率雖然沒有有失真壓縮那麼高,並且無失真壓縮刪除的資料在重新載入至記憶體時會被還原,因此不可以減少圖片的記憶體佔用。
 
4、位深與色深
我們知道了圖片在記憶體中和在磁碟上的兩種不同的表示形式:前者為Bitmap,後者為各種壓縮格式。這裡介紹一下位深與色深的概念:
①色深
色深指的是每一個畫素點用多少bit來儲存ARGB值,屬於圖片自身的一種屬性。色深可以用來衡量一張圖片的色彩處理能力(即色彩豐富程度)。
典型的色深是8-bit、16-bit、24-bit和32-bit等。
上述的Bitmap.Config引數的值指的就是色深。比如ARGB_8888方式的色深為32位,RGB_565方式的色深是16位。
 
②位深
位深指的是在對Bitmap進行壓縮儲存時儲存每個畫素所用的bit數,主要用於儲存。由於是“壓縮”儲存,所以位深一般小於或等於色深 。
舉個例子:某張圖片100畫素*100畫素 色深32位(ARGB_8888),儲存時位深度為24位,那麼: 
該圖片在記憶體中所佔大小為:100 * 100 * (32 / 8) Byte 
在檔案中所佔大小為 100 * 100 * ( 24/ 8 ) * 壓縮率 Byte
 
5、常見的壓縮格式
Bitmap的壓縮格式就是最終持久化儲存得到的圖片格式,一般由字尾名即可看出該圖片採用了何種壓縮方式。不同的壓縮方式的壓縮演算法不一樣。常見的主要有:
①Gif 
Gif是一種基於LZW演算法的無失真壓縮格式,其壓縮率一般在50%左右。Gif可插入多幀,從而實現動畫效果。因此Gif圖片分為靜態GIF和動畫GIF兩種GIF格式。
由於Gif以8位顏色壓縮儲存單個點陣圖,所以它最多隻能用256種顏色來表現物體,對於色彩複雜的物體它就力不從心了。因此Gif不適合用於色彩非常豐富的圖片的壓縮儲存,比如拍攝的真彩圖片等。
 
②BMP
BMP是標準圖形格式,它是包括Windows在內多種作業系統影象展現的終極形式。其本質就是Bitmap物件直接持久化儲存的點陣圖檔案格式,由於沒有進行壓縮儲存,因此體積非常大,故而不適合在網路上傳輸。同時也是因為這種格式是對Bitmap物件的直接儲存而沒有進行壓縮,因此我們在討論壓縮格式時往往忽略這一種。
 
③PNG
PNG格式本身的設計目的是替代GIF格式,所以它與GIF 有更多相似的地方。PNG格式也屬於無失真壓縮,其位深為32位,也就是說它支援所有的顏色型別。
同樣是無失真壓縮,PNG的壓縮率高於Gif格式,而且PNG支援的顏色數量也遠高於Gif,因此:如果是對靜態圖片進行無失真壓縮,優先使用PNG取代Gif,因為PNG壓縮率高、色彩好;但是PNG不支援動畫效果。所以Gif仍然有用武之地。
PNG缺點是:由於是無失真壓縮,因此PNG檔案的體積往往比較大。如果在專案中多處使用PNG圖片檔案,那麼在APP瘦身時需要對PNG檔案進行優化以減少APP體積大小。具體做法後面會詳細介紹。
 
④JPEG
JPEG是一種有失真壓縮格式,JPEG圖片以24位顏色壓縮儲存單個點陣圖。也就是說,JPEG不支援透明通道。JPEG也不支援多幀動畫。
因為是有失真壓縮,所以需要注意控制壓縮率以免圖片質量太差。
JPG和JPEG沒有區別,全名、正式副檔名是JPEG。但因DOS、Windows95等早期系統採用的8.3命名規則只支援最長3字元的副檔名,為了相容採用了.jpg。也因歷史習慣和相容性的考慮,.jpg目前更流行。
JPEG2000作為JPEG的升級版,其壓縮率比JPEG高約30%左右,同時支援有損和無失真壓縮。JPEG2000格式有一個極其重要的特徵在於它能實現漸進傳輸,即先傳輸影象的輪廓,然後逐步傳輸資料,不斷提高影象質量,讓影象由朦朧到清晰顯示。此外,JPEG2000還支援所謂的“感興趣區域”特性,也就是可以任意指定影像上感興趣區域的壓縮質量;另外,JPEG2000還可以選擇指定的部分先解壓縮來載入到記憶體中。JPEG2000和JPEG相比優勢明顯,且向下相容,因此可取代傳統的JPEG格式。
 
 ⑤WebP
WebP 是 Google 在 2010 年釋出的圖片格式,希望以更高的壓縮率替代 JPEG。它用 VP8 視訊幀內編碼作為其演算法基礎,取得了不錯的壓縮效果。WebP支援有損和無失真壓縮、支援完整的透明通道、也支援多幀動畫,並且沒有版權問題,是一種非常理想的圖片格式。WebP支援動圖,基本取代gif。
WebP不僅整合了PNG、JPEG和Gif的所有功能,而且相同質量的無失真壓縮WebP圖片體積比PNG小大約26%;如果是有失真壓縮,相同質量的WebP圖片體積比JPEG小25%-34%。
很多人會認為,既然WebP功能完善、壓縮率更高,那直接用WebP取代上述所有的圖片壓縮格式不就行了嗎?其實不然,WebP也有其缺點:我們知道JPEG是有失真壓縮而PNG是無失真壓縮,所以JPEG的壓縮率高於PNG;但是有失真壓縮的演算法決定了其壓縮時間一定是高於無失真壓縮的,也就是說JPEG的壓縮時間高於PNG。而WebP無論是無損還是有失真壓縮,壓縮率都分別高於PNG和JPEG;與其相對應的是其壓縮時間也比它們長的多。經測試,WebP圖片的編碼時間比JPEG長8倍。可以看出,時間和空間是一對矛盾;如果想要節省更多的空間,必然要付出額外的時間;如果想要節省時間,那麼必然要付出空間的代價。這取決於我們在實際中對於時空不同的需求程度來做出選擇。
不管怎麼說,WebP還是一種強大的、理想的圖片壓縮格式,並且藉由 Google 在網路世界的影響力,WebP 在幾年的時間內已經得到了廣泛的應用。看看你手機裡的 App:微博、微信、QQ、淘寶等等,每個 App 裡都有 WebP 的身影。
另外,WebP是Android4.0才引入的一種圖片壓縮格式,如果想要在Android4.0以前的版本支援WebP格式的圖片,那麼需要藉助於第三方庫來支援WebP格式圖片,例如:webp-android-backport函式庫,該開源專案在GitHub地址為:https://github.com/alexey-pelykh/webp-android-backport  當然考慮到一般的Android開發中只需要向下相容到Android4.0即可,所以也可以忽略這個問題。
 
目前來說,以上所述的五種壓縮格式,Android作業系統都提供了原生支援;但是在上層能直接呼叫的編碼方式只有 JPEG、PNG、WebP 這三種。具體的,可以檢視Bitmap類的列舉內部類CompressFormat類的列舉值來獲取上層能呼叫的圖片編碼方式。你會發現列舉值也是JPEG、PNG和WEBP三種。
如果我們想要在應用層使用Gif格式圖片,需要自行引入第三方函式庫來提供對Gif格式圖片的支援。不過一般我們用WebP取代Gif。
因此,我們只需要比較分析PNG、JPEG、WebP這三種壓縮格式即可。
 
比較分析:
①對於攝影類等真彩圖片:因為我們對這類色彩豐富的圖片的透明度沒有要求(一般預設為不透明),可以採用JPEG有失真壓縮格式,因為JPEG本身就不支援透明度,而且因為是有失真壓縮,所以儘管會犧牲一丟丟照片的質量但是可以大大減少體積。如果非要採用PNG格式,那麼首先因為PNG支援透明度通道,所以明明不必要的透明度值卻會被儲存;其次因為是無失真壓縮,所以壓縮率不會很高從而導致儲存的圖片非常大!綜上比較,建議採用JPEG格式,不要用PNG格式。
JPEG格式可以與Bitmap.Config引數值為RGB_565搭配使用,這是一個理想的設定。
 
②對於logo圖示、背景圖等圖片:這類圖片的特點是往往是有大塊的顏色相同的區域,這與無失真壓縮的思路不謀而合(即刪除重複資料)。而且這類圖片對透明度是有要求的,因此可以採用PNG無失真壓縮格式;儘管使用PNG格式會讓圖片有點大,但是可以在後續進行PNG圖片優化以對APP體積進行瘦身。如果非要採用JPEG格式,那麼由於有失真壓縮的原理(利用人腦的自動補全機制),可能會隨機地丟失一些線條導致最終的圖片完全不是想要的效果。綜上比較,建議使用PNG格式,不要用JPEG格式。
PNG格式可以與Bitmap.Config引數值為ARGB_8888搭配使用,這是一個理想的設定。
當然,以上兩種情況,我們都可以使用WebP取代PNG或JPEG,如果我們想要這麼做的話。如果你的專案中對空間的需求程度更高,你完全有理由這麼做。但是如果你對空間需求程度還OK,你也可以選擇分情況使用PNG或JPEG格式。
 
6、圖片優化
圖片優化屬於Android效能優化的一種,這裡主要是針對PNG圖片的大小進行優化,畢竟PNG這種無失真壓縮格式往往會導致圖片都比較大。除非你的專案已經全面支援了WebP格式,否則對PNG格式圖片的優化都會是你必須考慮的一點,這有利於減少APP的體積大小。
對PNG圖片進行優化的思想是:減少PNG圖片的體積,常用方式有:
①無失真壓縮工具ImageOptim
ImageOptim是一種無失真壓縮工具,所以你不用擔心利用該工具對PNG圖片進行壓縮後圖片質量會受影響。它的壓縮原理是:優化PNG壓縮引數,移除冗餘後設資料以及非必需的顏色配置檔案等,在不犧牲圖片質量的前提下,既減少了PNG圖片的大小,又提高了其載入的速度。
ImageOptim工具的網址為:https://imageoptim.com
 
②有失真壓縮工具ImageAlpha
ImageAlpha與ImageOptim是同一個作者,不過ImageAlpha屬於有失真壓縮,因此圖片質量會受到影響。所以使用ImageAlpha對PNG圖片進行壓縮後,必須讓設計師檢視一下優化後的PNG圖片,以免影響APP的視覺效果。但是ImageAlpha的優點是可以極大減少PNG圖片的體積大小。
ImageAlpha工具的網址為:https://pngmini.com
 
③有失真壓縮TinyPNG
前面兩個工具是應用程式,TinyPNG是一個Web站點。你可以上傳原PNG圖片,它對PNG圖片壓縮後你就可以下載優化後的結果了。因為TinyPNG也是有失真壓縮,所以優缺點同②
TinyPNG的網址為:https://tinypng.com
以上方案都屬於對PNG圖片進行二次壓縮(有的是有損有的是無損),我們需要在圖片質量和圖片大小這對矛盾中根據實際情況進行選擇。
 
④PNG/JPEG轉換為WebP
如果不想對PNG圖片進行二次壓縮,可以考慮直接將其替換為WebP格式的圖片。另外,我們對JPEG格式的圖片也可以這麼替換。畢竟WebP無論是與PNG還是與JPEG格式想比,壓縮後體積大小都小很多。WebP轉換工具有:
智圖,這是一個圖片優化平臺,地址為:https://zhitu.isux.us
iSparta,這是一個針對PNG圖片的二次壓縮和格式轉換工具,地址為:https://isparta.github.io
 
⑤使用NinePatch格式的PNG圖
.9.png圖片格式簡稱為NinaPatch圖,本質上仍然是PNG格式圖片。不過它的優點是體積小、拉伸不變形,能夠很好地適配Android各種機型。我們可以利用Android Studio提供的功能,右鍵一張PNG圖片點選“create 9=Patch File”即可完成轉換。
總結:無論是二次壓縮還是格式轉換,無論是有損二次壓縮還是無損二次壓縮,我們都需要根據實際需求進行方案和工具的選擇。
 
我們已經知道了Android中圖片記憶體中的表示形式(Bitmap)和磁碟上的表示形式(各種壓縮格式),以及二者的關係(壓縮和解壓縮的過程)。下面具體看看Bitmap的使用方式和注意事項,畢竟磁碟上儲存的圖片終究還是要載入到記憶體中以Bitmap的形式進行展示的。
 
四、Bitmap的簡單使用
先看看Bitmap的簡單使用方式:
1、Bitmap的載入方法
Bitmap的工廠類BitmapFactory提供了四類靜態方法用於載入Bitmap物件:decodeFile、decodeResource、decodeStream、decodeByteArray。
分別代表從本地圖片檔案、專案資原始檔、流物件(可以是網路輸入流物件或本地檔案輸入流物件)、位元組序列中載入一個Bitmap物件。
之前講的圖片的三個來源,都可以找到對應的decodeXXXX方法來獲取該圖片對應的Bitmap物件。
舉個例子,假設需要通過網路請求一張圖片資源並展示:先處理該網路請求並得到返回結果的輸入流物件;依據該流物件呼叫decodeStream方法得到一個Bitmap物件,該物件即表示這張圖片的內容;最後通過ImageView的setImageBitmap()方法顯示該圖片即可。
 
2、Bitmap的壓縮儲存方法
Bitmap的壓縮儲存與Bitmap的載入是相反的過程,通過compress()方法來實現,該方法原型為:
compress(Bitmap.CompressFormat format, int quality, OutputStream stream)
format參數列示壓縮儲存的格式,可選為PNG、JPEG和WEBP;quality表示壓縮率,取值在0~100之間,100表示未壓縮,30表示壓縮為原大小的30%,但是該引數在format值為PNG時無效,因為PNG屬於無失真壓縮無法設定壓縮率;stream就是希望輸出到某個位置的輸出流物件,比如某個檔案的輸出流物件。
通過compress()方法可以將Bitmap按照指定的格式和壓縮率(非PNG格式時)壓縮儲存到指定的位置。
 
3、BitmapFactory.Options類
BitmapFactory是Bitmap的工廠類,通過BitmapFactory的靜態方法來建立Bitmap物件;BitmapFactory.Options類代表對Bitmap物件的屬性設定(配置)。一般情況下,我們呼叫decodeXXXX方法時不需要傳遞一個BitmapFactory.Options物件作為引數,因此此時是利用預設的配置資訊來建立Bitmap物件。如果需要對建立的Bitmap物件進行自定義的配置,那麼就需要給decodeXXXX方法傳遞一個BitmapFactory.Options物件,該物件包含了對Bitmap物件的配置資訊。
通過BitmapFactory.Options類的構造器建立BitmapFactory.Options物件。該物件包含的Bitmap配置資訊即為該物件的各屬性值,主要有:

介紹一下比較不好理解的屬性:

①inJustDecodeBounds:這個屬性表示是否只掃描輪廓,預設為false。如果該屬性為true,decodeXXXX方法不會返回一個Bitmap物件(即不會為Bitmap分配記憶體)而是返回null。那如果decodeXXXX方法不再分配記憶體以建立一個Bitmap物件,那麼還有什麼用呢?答案就是:掃描輪廓。

BitmapFactory.Options物件的outWidth和outHeight屬性分別代表Bitmap物件的寬和高,但是這兩個屬性在Bitmap物件未建立之前顯然預設為0,預設只有在Bitmap物件建立後才能被賦予正確的值。而當inJustDecodeBounds屬性為true,雖然不會分配記憶體建立Bitmap物件,但是會掃描輪廓來給outWidth和outHeight屬性賦值,就相當於繞過了Bitmap物件建立的這一步提前獲取到Bitmap物件的寬高值。那這個屬性到底有啥用呢?具體用處體現在Bitmap的取樣率計算中,後面會詳細介紹。

②inSample:這個表示Bitmap的取樣率,預設為1。比如說有一張圖片是2048畫素X1024畫素,那麼預設情況下該圖片載入到記憶體中的Bitmap物件尺寸也是2048畫素X1024畫素。如果採用的是ARGB_8888方式,那麼該Bitmap物件載入所消耗的記憶體為2048X1024X4/1024/1024=8M。這只是一張圖片消耗的記憶體,如果當前活動需要載入幾張甚至幾十張圖片,那麼會導致嚴重的OOM錯誤。

OOM錯誤:儘管Android裝置記憶體大小可能達到好幾個G(比如4G),但是Andorid中每個應用其執行記憶體都有一個閾值,超過這個閾值就會引發out of memory即OOM錯誤(記憶體溢位錯誤)。因為現在市場上流行的手機裝置其作業系統都是在Andori原生作業系統基礎上的擴充,所以不同的裝置環境中這個記憶體閾值不一樣。可以通過以下方法獲取到當前應用所分配的記憶體閾值大小,單位為位元組: Runtime.getRuntime().maxMemory();

儘管我們確實可以通過設定來修改這個閾值大小以提高應用的最大分配記憶體(具體方式是在在Manifest中設定android.largeHeap="true"),但是需要注意的是:記憶體是一種很寶貴的資源,不加考慮地無腦給每個應用提高最大分配記憶體是一個糟糕的選擇。因為手機總記憶體相比較每個應用預設的最大分配記憶體雖然高很多,但是手機中的應用數量是非常多的,每個應用都修改其執行記憶體閾值為幾百MB甚至一個G,這很嚴重影響手機效能!另外,如果應用的最大分配記憶體很高,這意味著其垃圾回收工作也會變得更加耗時,這也會影響應用和手機的效能。所以,這個方案需要慎重考慮不能濫用。

關於這個方案的理解可以參考一位大神的解釋:“在一些特殊的情景下,你可以通過在manifest的application標籤下新增largeHeap=true的屬性來為應用宣告一個更大的heap空間。然後,你可以通過getLargeMemoryClass()來獲取到這個更大的heap size閾值。然而,宣告得到更大Heap閾值的本意是為了一小部分會消耗大量RAM的應用(例如一個大圖片的編輯應用)。不要輕易的因為你需要使用更多的記憶體而去請求一個大的Heap Size。只有當你清楚的知道哪裡會使用大量的記憶體並且知道為什麼這些記憶體必須被保留時才去使用large heap。因此請謹慎使用large heap屬性。使用額外的記憶體空間會影響系統整體的使用者體驗,並且會使得每次gc的執行時間更長。在任務切換時,系統的效能會大打折扣。另外, large heap並不一定能夠獲取到更大的heap。在某些有嚴格限制的機器上,large heap的大小和通常的heap size是一樣的。因此即使你申請了large heap,你還是應該通過執行getMemoryClass()來檢查實際獲取到的heap大小。”

綜上,我們已經知道了Bitmap的載入是一個很耗記憶體的操作,特別是在大點陣圖的情況下。這很容易引發OOM錯誤,而我們又不能輕易地通過修改或提供應用的記憶體閾值來避免這個錯誤。那麼我們該怎麼做呢?答案就是:利用這裡所說的取樣率屬性來建立一個原Bitmap的子取樣版本。這也是官方推薦的對於大點陣圖載入的OOM問題的解決方案。其具體思想為:比如還是那張尺寸為2048畫素X1024畫素圖片,在inSample值預設為1的情況下,我們現在已經知道它載入到記憶體中預設是一個2048畫素X1024畫素大點陣圖了。我們可以將inSample設定為2,那麼該圖片載入到記憶體中的點陣圖寬高都會變成原寬高的1/2,即1024畫素X512畫素。進一步,如果inSample值設定為4,那麼點陣圖尺寸會變成512畫素X256畫素,這個時候該點陣圖所消耗的記憶體(假設還是ARGB_8888方式)為512X256X4/1024/1024=0.5M,可以看出從8M到0.5M,這極大的節省了記憶體資源從而避免了OOM錯誤。

切記:官方對於inSample值的要求是,必須為2的冪,比如2、4、8...等整數值。

這裡會有兩個疑問:第一:通過設定inSample屬性值來建立一個原大點陣圖的子取樣版本的方式來降低記憶體消耗,聽不上確實很不錯。但是這不會導致圖片嚴重失真嗎?畢竟你丟失了那麼多畫素點,這意味著你丟失了很多顏色資訊。對這個疑問的解釋是:儘管在取樣的過程確實會丟失很多畫素點,但是原點陣圖的尺寸也在減小,其畫素密度是不變的。比如說如果inSample值為2,那麼子取樣版本的畫素點數量是原來的1/4,但是子取樣版本的顯示尺寸(區域面積)也會變成原來的1/4,這樣的話畫素密碼是不變的因此圖片不用擔心嚴重失真問題。第二:inSample值如何選取才是最佳?這其實取決於ImageView的尺寸,具體取樣率的計算方式後面會詳細介紹。

③inPreferredConfig:該屬性指定Bitmap的色深值,該屬性型別為Bitmap.Config值。

例如你可以指定某圖片載入為Bitmap物件的色深模式為ARGB_8888,即:options.inPreferredConfig=Bitmap.Config.ARGB_8888;

④isMutable:該屬性表示通過decodeXXXX方法建立的Bitmap物件其代表的圖片內容是否允許被外部修改,比如利用Canvas重新繪製其內容等。預設為false,即不允許被外部操作修改。

利用這些屬性定製BitmapFactory.Options物件,從而靈活地按照自己的需求配置建立的Bitmap物件。

 

五、Bitmap的進階使用

1、高效地載入大點陣圖

上面剛說了大點陣圖載入時的OOM問題,解決方式是通過inSample屬性建立一個原點陣圖的子取樣版本以減低記憶體。那麼這裡的取樣率inSample值如何選取最好呢?這裡我們利用官方推薦的取樣率最佳計算方式:基本步驟就是:①獲取點陣圖原尺寸 ②獲取ImageView即最終圖片顯示的尺寸  ③依據兩種尺寸計算取樣率(或縮放比例)。

public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // 點陣圖的原寬高通過options物件獲取
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;
     //當要顯示的目標大小和影象的實際大小比較接近時,會產生沒必要的取樣,先除以2再判斷以防止過度取樣
        while ((halfHeight / inSampleSize) >= reqHeight
                && (halfWidth / inSampleSize) >= reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

 

依據上面的最佳取樣率計算方法,進一步可以封裝出利用最佳取樣率建立子取樣版本再建立點陣圖物件的方法,這裡以從專案圖片資原始檔載入Bitmap物件為例:

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
   //因為inJustDecodeBounds為true,所以不會建立Bitmap物件只會掃描輪廓從而給options物件的寬高屬性賦值
    BitmapFactory.decodeResource(res, resId, options);

    // 計算最佳取樣率
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 記得將inJustDecodeBounds屬性設定回false值
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}

 

2、Bitmap載入時的非同步問題

由於圖片的來源有三種,如果是專案圖片資原始檔的載入,一般採取了子取樣版本載入方案後不會導致ANR問題,畢竟每張圖載入消耗的記憶體不會很大了。但是對於本地圖片檔案和網路圖片資源,由於分別涉及到檔案讀取和網路請求,所以屬於耗時操作。為了避免ANR的產生,必須將圖片載入為Bitmap物件的過程放入工作執行緒中;獲取到Bitmap物件後再回到UI執行緒設定ImageView的顯示。舉個例子,如果採用AsyncTask作為我們的非同步處理方案,那麼程式碼如下:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
         private final ImageView iv;
         private int id = 0;
 
         public BitmapWorkerTask(ImageView imageView) {
             iv = imageView;
          }
 
         // Decode image in background.
         @Override
         protected Bitmap doInBackground(Integer... params) {
             id = params[0];
            //假設ImageView尺寸為500X500,為了方便還是以專案資原始檔的載入方式為例,因為這可以複用上面封裝的方法
             return decodeSampledBitmapFromResource(getResources(), id, 500, 500);
        }
 
         @Override
         protected void onPostExecute(Bitmap bitmap) {
             iv.setImageBitmap(bitmap);
         }
     }

該方案中,doInBackground方法執行在子執行緒,用來處理 ”圖片檔案讀取操作+Bitmap物件的高效載入操作” 或 ”網路請求圖片資源操作+Bimap物件的高效載入操作”等兩種情形下的耗時操作。onPostExecute方法執行在UI執行緒,用於設定ImageView的顯示內容。看上去這個方案很完美,但是有一個很隱晦的嚴重問題:

由當前活動啟動了BitmapWorkerTask任務後:當我們退出當前活動時,由於非同步任務只依賴於UI執行緒所以BitmapWorkerTask任務會繼續執行。正常的操作是遍歷當前活動例項的物件圖來釋放各物件的記憶體以銷燬該活動,但是由於當前活動例項的ImageView引用被BitmapWorkerTask物件持有,而且還是強引用關係。這會導致Activity例項無法被銷燬,引發記憶體洩露問題。記憶體洩露問題會進一步導致記憶體溢位錯誤。

為了解決這個問題,我們只需要讓BitmapWorkerTask類持有ImageView的弱引用即可。這樣當活動退出時,BitmapWorkerTask物件由於持有的是ImageView的弱引用,所以ImageView物件會被回收,繼而Activity例項得到銷燬,從而避免了記憶體洩露問題。具體修改後的程式碼如下:

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
        private final WeakReference<ImageView> imageViewReference;
        private int data = 0;

        public BitmapWorkerTask(ImageView imageView) {
            // 用弱引用來關聯這個imageview!弱引用是避免android 在各種callback回撥裡發生記憶體洩露的最佳方法!
            //而軟引用則是做快取的最佳方法 兩者不要搞混了!
            imageViewReference = new WeakReference<ImageView>(imageView);
        }

        // Decode image in background.
        @Override
        protected Bitmap doInBackground(Integer... params) {
            data = params[0];
            return decodeSampledBitmapFromResource(getResources(), data, 100, 100);
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            //當後臺執行緒結束後 先看看ImageView物件是否被回收:如果被回收就什麼也不做,等著系統回收他的資源
            //如果ImageView物件沒被回收的話,設定其顯示內容即可
            if (imageViewReference != null && bitmap != null) {
                final ImageView imageView = imageViewReference.get();
                if (imageView != null) {
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    }

 

擴充:①WeakReference是弱引用,其中儲存的物件例項可以被GC回收掉。這個類通常用於在某處儲存物件引用,而又不干擾該物件被GC回收,可以用於避免記憶體洩露。②SoftReference是軟引用,它儲存的物件例項,不會被GC輕易回收,除非JVM即將OutOfMemory,否則不會被GC回收。這個特性使得它非常適合用於設計Cache快取。快取可以省去重複載入的操作,而且快取屬於記憶體因此讀取資料非常快,所以我們自然不希望快取內容被GC輕易地回收掉;但是因為快取本質上就是一種記憶體資源,所以在記憶體緊張時我們需要能釋放一部分快取空間來避免OOM錯誤。綜上,軟引用非常適合用於設計快取Cache。但是,這只是早些時候的快取設計思想,比如在Android2.3版本之前。在Android2.3版本之後,JVM的垃圾收集器開始更積極地回收軟引用物件,這使得原本的快取設計思想失效了。因為如果使用軟引用來實現快取,那麼動不動快取物件就被GC回收掉實在是無法接受。所以,Android2.3之後對於快取的設計使用的是強引用關係(也就是普通物件引用關係)。很多人會問這樣不會由於強引用的快取物件無法被回收從而導致OOM錯誤嗎?確實會這樣,但是我們只需要給快取設定一個合理的閾值就好了。將快取大小控制在這個閾值範圍內,就不會引發OOM錯誤了。

 

3、列表載入Bitmap時的圖片顯示錯亂問題

 我們已經知道了如何高效地載入點陣圖以避免OOM錯誤,還知道了如何合理地利用非同步機制來避免Bitmap載入時的ANR問題和記憶體洩露問題。現在考慮另一種常見的Bitmap載入問題:當我們使用列表,如ListView、GridView和RecyclerView等來載入多個Bitmap時,可能會產生圖片顯示錯亂的問題。先看一下該問題產生的原因。以ListView為例:

①ListView為了提高列表展示內容在滾動時的流暢性,使用了一種item複用機制,即:在螢幕中顯示的每個ListView的item對應的佈局只有在第一次的時候被載入,然後快取在convertView裡面,之後滑動改變ListView時呼叫的getView就會複用快取在converView中的佈局和控制元件,所以可以使得ListView變得流暢(因為不用重複載入佈局)。

②每個Item中的ImageView載入圖片時往往都是非同步操作,比如在子執行緒中進行圖片資源的網路請求再載入為一個Bitmap物件最後回到UI執行緒設定該item的ImageView的顯示內容。

③ 聽上去①是一種非常合理有效的提高列表展示流暢性的機制,②看起來也是圖片載入時很常見的一個非同步操作啊。其實①和②本身都沒有問題,但是①+②+使用者滑動列表=圖片顯示錯亂!具體而言:當我們在其中一個itemA載入圖片A的時候,由於載入過程是非同步操作需要耗費一定的時間,那麼有可能圖片A未被載入完該itemA就“滾出去了”,這個itemA可能被當做快取應用到另一個列表項itemB中,這個時候剛好圖片A載入完成顯示在itemB中(因為ImageView物件在快取中被複用了),原本itemB該顯示圖片B,現在顯示圖片A。這只是最簡單的一種情況,當滑動頻繁時這種圖片顯示錯亂問題會愈加嚴重,甚至讓人毫無頭緒。

那麼如何解決這種圖片顯示錯亂問題呢?解決思路其實非常簡單:在圖片A被載入到ImageView之前做一個判斷,判斷該ImageView物件是否還是對應的是itemA,如果是則將圖片載入到ImageView當中;如果不是則放棄載入(因為itemB已經啟動了圖片B的載入,所以不用擔心控制元件出現空白的情況)。

那麼新的問題出現了,如何判斷ImageView物件對應的item已經改變了?我們可以採取下面的方式:

①在每次getView的複用佈局控制元件時,對會被複用的控制元件設定一個標籤(在這裡就是對ImageView設定標籤)。標籤內容必須可以標識不同的item!這裡使用圖片的url作為標籤內容,然後再非同步載入圖片。

②在圖片下載完成後要載入到ImageView之前做判斷,判斷該ImageView的標籤內容是否和圖片的url一樣:如果一樣說明ImageView沒有被複用,可以將圖片載入到ImageView當中;如果不一樣,說明ListView發生了滑動,導致其他item呼叫了getView從而將該ImageView的標籤改變,此時放棄圖片的載入(儘管圖片已經被下載成功了)。

總結:解決ListView非同步載入Bitmap時的圖片錯亂問題的方式是:為被複用的控制元件物件(即ImageView物件)設定標籤來標識item,非同步任務結束後要將圖片載入到ImageView時取出標籤值進行比對是否一致:如果一致意味著沒有發生滑動,正常載入圖片;如果不一樣意味著發生了滑動,取消載入。

 

4、Android中的Bitmap快取策略

如果只是載入若干張圖片,上述的Bitmap使用方式已經絕對夠用了;但是如果在應用中需要頻繁地載入大量的圖片,特別是有些圖片會被重複載入時,這個時候利用快取策略可以很好地提高圖片的載入速度。比如說有幾張圖片被重複載入的頻率很高,那麼可以在快取中保留這幾張圖片的Bitmap物件;後續如果需要載入這些圖片,則不需要花費很多時間去重新在網路上獲取並載入這些圖片的Bitmap物件,只需要直接向快取中獲取之前保留下來的Bitmap物件即可。

Android中對Bitmap的快取策略分為兩種:

  • 記憶體快取:影象儲存在裝置記憶體中,因此訪問速度非常快。事實上,比影象解碼過程要快得多,所以將影象儲存在這裡是讓app更快更穩定的一個好主意。記憶體快取的唯一缺點是:它只存活於app的生命週期,這意味著一旦app被Android作業系統記憶體管理器關閉或殺死(全部或部分),那麼儲存在那裡的所有影象都將丟失。由於記憶體快取本質上就是一種記憶體資源,所以切記:記憶體快取必須設定一個最大可用的記憶體量。否則可能會導致臭名昭著的outOfMemoryError。
  • 磁碟快取:影象儲存在裝置的物理儲存器上(磁碟)。磁碟快取本質上就是裝置SD卡上的某個目錄。只要app不被解除安裝,其磁碟快取可以一直安全地儲存圖片,只要有足夠的磁碟空間即可。缺點是,磁碟讀取和寫入操作可能會很慢,而且總是比訪問記憶體快取慢。由於這個原因,因此所有的磁碟操作必須在工作執行緒執行,UI執行緒之外。否則,app會凍結,並導致ANR警報。

在實際使用中,我們不需要強行二選一,可以二者都使用,畢竟各有優勢。所以Android中完整的圖片快取策略為:先嚐試在記憶體快取中查詢Bitmap物件,如果有直接載入使用;如果沒有,再嘗試在磁碟快取中查詢圖片檔案是否存在,如果有將其載入至記憶體使用;如果還是沒有,則老老實實傳送網路請求獲取圖片資源並載入使用。需要注意的是,後面兩種情況下的操作都必須使用非同步機制以避免ANR的發生。

Android中通過LruCache實現記憶體快取,通過DiskLruCache實現磁碟快取,它們採用的都是LRU(Least Recently Used)最近最少使用演算法來移除快取中的最近不常訪問的內容(變相地保留了最近經常訪問的內容)。

①記憶體快取LruCache

LruCache原理:LruCache底層是使用LinkedHashMap來實現的,所以LruCache也是一個泛型類。在圖片快取中,其鍵型別是字串,值型別為Bitmap。利用LinkedHashMap的accessOrder屬性可以實現LRU演算法。accessOrder屬性決定了LinkedHashMap的連結串列順序:accessOrder為true則以訪問順序維護連結串列,即被訪問過的元素會安排到連結串列的尾部;accessorder為false則以插入的順序維護連結串列。

而LruCache利用的正是accessOrder為true的LinkedHashMap來實現LRU演算法的。具體表現為:

1° put:通過LinkedHashMap的put方法來實現元素的插入,插入的過程還是要先尋找有沒有相同的key的資料,如果有則替換掉舊值,並且將該節點移到連結串列的尾部。這可以保證最近經常訪問的內容集中儲存在連結串列尾部,最近不常訪問的記憶體集中儲存在連結串列頭部位置。在插入後如果快取大小超過了設定的最大快取大小(閾值),則將LinkedHashMap頭部的節點(最近不常訪問的內容)刪除,直到size小於maxSize。

2° get:通過LinkedHashMap的get方法來實現元素的訪問,由於accessOrder為true,因此被訪問到的元素會被調整到連結串列的尾部,因此不常被訪問的元素就會留到連結串列的頭部,當觸發清理快取時不常被訪問的元素就會被刪除,這裡是實現LRU最關鍵的地方。

3° remove:通過LinkedHashMap的remove方法來實現元素的移除。

3° size:LruCache中很重要的兩個成員變數size和maxSize,因為清理快取的是在size>maxSize時觸發的,因此在初始化的時候要傳入maxSize定義快取的大小,然後重寫sizeOf方法,因為LruCache是通過sizeOf方法來計算每個元素的大小。這裡我們是使用LruCache來快取圖片,所以sizeOf方法需要計算Bitmap的大小並返回。

LruCache對其快取物件採用的是強引用關係,採用maxSize來控制快取空間大小以避免OOM錯誤。而且LruCache類在Android SDK中已經提供了,在實際使用中我們只需要完成以下幾步即可:

  • 設計LruCache的最大快取大小:一般是通過計算當前可用的記憶體大小繼而來獲取到應該設定的快取大小
  • 建立LruCache物件:傳入最大快取大小的引數,同時重寫sizeOf方法來設定存在LruCache裡的每個物件的大小
  • 封裝對LruCache的資料訪問和新增操作並對外提供介面以供呼叫

具體程式碼參考如下:

//初始化LruCache物件
public void initLruCache()
{
    //獲取當前程式的可用記憶體,轉換成KB單位
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    //分配快取的大小
    int maxSize = maxMemory / 8;
    //建立LruCache物件並重寫sizeOf方法
    lruCache = new LruCache<String, Bitmap>(maxSize)
        {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                // TODO Auto-generated method stub
                return value.getWidth() * value.getHeight() / 1024;
            }
        };
}

/**
 * 封裝將圖片存入快取的方法
 * @param key 圖片的url轉化成的key
 * @param bitmap物件
 */
private void addBitmapToMemoryCache(String key, Bitmap bitmap)
{
    if(getBitmapFromMemoryCache(key) == null)
    {
        mLruCache.put(key, bitmap);
    }
}

//封裝從LruCache中訪問資料的方法
private Bitmap getBitmapFromMemoryCache(String key)
{
    return mLruCache.get(key);
}

/**
 * 因為外界一般獲取到的是url而不是key,因此為了方便再做一層封裝
 * @param url http url
 * @return bitmap
 */
private Bitmap loadBitmapFromMemoryCache(String url)
{
    final String key = hashKeyFromUrl(url);
    return getBitmapFromMemoryCache(key);
}

 

②磁碟快取DiskLruCache

由於DiskLruCache並不屬於Android SDK的一部分,需要自行設計。與LruCache實現LRU演算法的思路基本上是一致的,但是有很多不一樣的地方:LruCache是記憶體快取,其鍵對應的值型別直接為Bitmap;而DiskLruCache是磁碟快取,所以其鍵對應的值型別應該是一個代表圖片檔案的類。其次,前者訪問或新增元素時,查詢成功可以直接使用該Bitmap物件;後者訪問或新增元素時,查詢到指定圖片檔案後還需要通過檔案的讀取和Bitmap的載入過程才能使用。另外,前者是在記憶體中的資料讀寫操作所以不需要非同步;後者涉及到檔案操作必須開啟子執行緒實現非同步處理。

具體DiskLruCache的設計方案和使用方式可以參考這篇部落格:https://www.jianshu.com/p/765640fe474a

有了LruCache類和DiskLruCache類,可以實現完整的Android圖片二級快取策略:在具體的圖片載入時:先嚐試在LruCache中查詢Bitmap物件,如果有直接拿來使用。如果沒有再嘗試在DiskLruCache中查詢圖片檔案,如果有將其載入為Bitmap物件再使用,並將其新增至LruCache中;如果沒有查詢到指定的圖片檔案,則傳送網路請求獲取圖片資源並載入為Bitmap物件再使用,並將其新增DiskLruCache中。

 

5、Bitmap記憶體管理

Android裝置的記憶體包括本機Native記憶體和Dalvik(類似於JVM虛擬機器)堆記憶體兩部分。在Android 2.3.3(API級別10)及更低版本中,點陣圖的支援畫素資料儲存在Native記憶體中。它與點陣圖本身是分開的,Bitmap物件本身儲存在Dalvik堆中。Native記憶體中的畫素資料不會以可預測的方式釋放,可能導致應用程式短暫超出其記憶體限制並崩潰。從Android 3.0(API級別11)到Android 7.1(API級別25),畫素資料與相關Bitmap物件一起儲存在Dalvik堆上,一起交由Dalvik虛擬機器的垃圾收集器來進行回收,因此比較安全。

①在Android2.3.3版本之前:

在Bitmap物件不再使用並希望將其銷燬時,Bitmap物件自身由於儲存在Dalvik堆中,所以其自身會由GC自動回收;但是由於Bitmap的畫素資料儲存在native記憶體中,所以必須由開發者手動呼叫Bitmap的recycle()方法來回收這些畫素資料佔用的記憶體空間。

 

②在Android2.3.3版本之後:

由於Bitmap物件和其畫素資料一起儲存在Dalvik堆上,所以在其需要回收時只要將Bitmap引用置為null 就行了,不需要如此麻煩的手動釋放記憶體操作。

當然,一般我們在實際開發中往往向下相容到Android4.0版本,所以你懂得。

 

③在Android3.0以後的版本,還提供了一個很好用的引數,叫options.inBitmap。如果你使用了這個屬性,那麼在呼叫decodeXXXX方法時會直接複用 inBitmap 所引用的那塊記憶體。大家都知道,很多時候ui卡頓是因為gc 操作過多而造成的。使用這個屬效能避免頻繁的記憶體的申請和釋放。帶來的好處就是gc操作的數量減少,這樣cpu會有更多的時間執行ui執行緒,介面會流暢很多,同時還能節省大量記憶體。簡單地說,就是記憶體空間被各個Bitmap物件複用以避免頻繁的記憶體申請和釋放操作。

需要注意的是,如果要使用這個屬性,必須將BitmapFactory.Options的isMutable屬性值設定為true,否則無法使用這個屬性。

具體使用方式參考如下程式碼:

final BitmapFactory.Options options = new BitmapFactory.Options();
        //size必須為1 否則是使用inBitmap屬性會報異常
        options.inSampleSize = 1;
        //這個屬性一定要在用在src Bitmap decode的時候 不然你再使用哪個inBitmap屬性去decode時候會在c++層面報異常
        //BitmapFactory: Unable to reuse an immutable bitmap as an image decoder target.
        options.inMutable = true;
        inBitmap2 = BitmapFactory.decodeFile(path1,options);
        iv.setImageBitmap(inBitmap2);
        //將inBitmap屬性代表的引用指向inBitmap2物件所在的記憶體空間,即可複用這塊記憶體區域
        options.inBitmap = inBitmap2;
        //由於啟用了inBitmap屬性,所以後續的Bitmap載入不會申請新的記憶體空間而是直接複用inBitmap屬性值指向的記憶體空間
        iv2.setImageBitmap(BitmapFactory.decodeFile(path2,options));
        iv3.setImageBitmap(BitmapFactory.decodeFile(path3,options));
        iv4.setImageBitmap(BitmapFactory.decodeFile(path4,options));

 

補充:Android4.4以前,你要使用這個屬性,那麼要求複用記憶體空間的Bitmap物件大小必須一樣;但是Android4.4 以後只要求後續複用記憶體空間的Bitmap物件大小比inBitmap指向的記憶體空間要小就可以使用這個屬性了。另外,如果你不同的imageview 使用的scaletype 不同,但是你這些不同的imageview的bitmap在載入是如果都是引用的同一個inBitmap的話,

這些圖片會相互影響。綜上,使用inBitmap這個屬性的時候 一定要小心小心再小心。

 

六、開源框架

我們現在已經知道了,Android圖片載入的知識點和注意事項實在太多了:單個的點陣圖載入我們要考慮Bitmap載入的OOM問題、非同步處理問題和記憶體洩露問題;列表載入點陣圖要考慮顯示錯亂問題;頻繁大量的點陣圖載入時我們要考慮二級快取策略;我們還有考慮不同版本下的Bitmap記憶體管理問題,在這部分最後我們介紹了Bitmap記憶體複用方式,我們需要小心使用這種方式。

那麼,能不能有一種方式讓我們省去這麼多繁瑣的細節,方便我們對圖片進行載入呢?答案就是:利用已有的成熟的圖片載入和快取開源框架!比如square公司的Picasso框架、Google公司的Glide框架和Facebook公司的Fresco框架等。特別是Fresco框架,提供了三級快取策略,非常的專業。根據APP對圖片顯示和快取的需求從低到高排序,我們可以採用的方案依次為:Bitmapfun、Picasso、Android-Universal-Image-Loader、Glide、Fresco。

這些框架可以方便我們實現對網路圖片的載入和快取操作。具體不再贅述。

 

 

相關文章