1、背景android
在Android開發中,任何一個APP都離不開圖片的加載和顯示問題。這裏的圖片來源分爲三種:項目圖片資源文件(通常爲res/drawable目錄下的圖片文件)、手機本地圖片文件、網絡圖片資源等。圖片的顯示咱們通常採用ImageView做爲載體,經過ImageView的相應API便可設置其顯示的圖片內容。c++
咱們知道:若是是須要展現項目中的圖片資源文件,咱們只須要調用ImageView的setImageResource(int id)方法並傳入該圖片資源的id(通常爲R.drawable.xxx)便可。可是若是是須要展現手機本地的某張圖片或者網絡上的某個圖片資源,又該怎麼辦呢?——問題Agit
爲了回答問題A,咱們先思考一個更深的問題B:Android中是如何將某一張圖片的內容加載到內存中繼而由ImageView顯示的呢?github
咱們知道:若是咱們想經過TextView展現一個本地txt文件的內容,咱們只須要由該文件建立幷包裝一個輸入流對象。經過該輸入流對象便可獲得一個表明該文件內容的字符串對象,再將該字符串對象交由TextView展現便可。換句話說,這個txt文件的內容在內存中的表達形式就是這個字符串對象。web
類推一下,雖然圖片文件也是文件,可是咱們顯然不可能對圖片文件也採用這種方式:即經過該圖片創建幷包裝一個輸入流對象再獲取一個字符串對象。畢竟不管如何咱們都沒法將某個圖片的內容表示爲一個字符串對象(細想一下就知道了,你能經過一段話100%準確地描述一張圖片嗎?顯然不現實)。那麼,這就引入了問題C:既然字符串對象不行,那麼咱們該以哪一種對象來在內存中表示某個圖片的內容呢?答案就是:Bitmap對象!算法
2、基本概述緩存
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對象所表示的圖片內容。
3、詳細介紹
一、Bitmap.Config
Config是Bitmap的一個枚舉內部類,它表示的就是每一個像素點對ARGB通道值的存儲方案。取值有如下四種:
ARGB_8888:這種方案就是上面所說的每一個通道值採8bit來表示,每一個像素點須要4字節的內存空間來存儲數據。該方案圖片質量是最高的,可是佔用的內存也是最大的
ARGB_4444:這種方案每一個通道都是4位,每一個像素佔用2個字節,圖片的失真比較嚴重。通常不用這種方案。
RGB_565:這種方案RGB通道值分別佔五、六、5位,可是沒有存儲A通道值,因此不支持透明度。每一個像素點佔用2字節,是ARGB_8888方案的一半。
ALPHA_8:這種方案不支持顏色值,只存儲透明度A通道值,使用場景特殊,好比設置遮蓋效果等。
比較分析:通常咱們在ARGB_8888方式和RGB_565方式中進行選取:不須要設置透明度時,好比拍攝的照片等,RGB_565是個節省內存空間的不錯的選擇;既要設置透明度,對圖片質量要求又高,就用ARGB_8888。
二、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對象進行壓縮存儲呢?這麼作依據的思想是:當圖片持久化保存在磁盤上時,咱們應該儘量以最小的體積來保存同一張圖片的內容,這樣有利於節省磁盤空間;而當圖片加載到內存中以顯示的時候,應該將磁盤上壓縮存儲的圖片內容完整地展開。前者即爲壓縮過程,目的是節省磁盤空間;後者即爲解壓縮過程,目的是在內存中展現圖片的完整內容。
三、有損壓縮和無損壓縮
Bitmap壓縮存儲時的算法有不少種,可是總體可分爲兩類:有損壓縮和無損壓縮。
compress(Bitmap.CompressFormat format, int quality, OutputStream stream)
介紹一下比較很差理解的屬性:
①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的冪,好比二、四、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對象。
5、Bitmap的進階使用
一、高效地加載大位圖
上面剛說了大位圖加載時的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); }
二、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錯誤了。
三、列表加載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時取出標籤值進行比對是否一致:若是一致意味着沒有發生滑動,正常加載圖片;若是不同意味着發生了滑動,取消加載。
四、Android中的Bitmap緩存策略
若是隻是加載若干張圖片,上述的Bitmap使用方式已經絕對夠用了;可是若是在應用中須要頻繁地加載大量的圖片,特別是有些圖片會被重複加載時,這個時候利用緩存策略能夠很好地提升圖片的加載速度。好比說有幾張圖片被重複加載的頻率很高,那麼能夠在緩存中保留這幾張圖片的Bitmap對象;後續若是須要加載這些圖片,則不須要花費不少時間去從新在網絡上獲取並加載這些圖片的Bitmap對象,只須要直接向緩存中獲取以前保留下來的Bitmap對象便可。
Android中對Bitmap的緩存策略分爲兩種:
在實際使用中,咱們不須要強行二選一,能夠兩者都使用,畢竟各有優點。因此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對象 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中。
五、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這個屬性的時候 必定要當心當心再當心。
6、開源框架
咱們如今已經知道了,Android圖片加載的知識點和注意事項實在太多了:單個的位圖加載咱們要考慮Bitmap加載的OOM問題、異步處理問題和內存泄露問題;列表加載位圖要考慮顯示錯亂問題;頻繁大量的位圖加載時咱們要考慮二級緩存策略;咱們還有考慮不一樣版本下的Bitmap內存管理問題,在這部分最後咱們介紹了Bitmap內存複用方式,咱們須要當心使用這種方式。
那麼,能不能有一種方式讓咱們省去這麼多繁瑣的細節,方便咱們對圖片進行加載呢?答案就是:利用已有的成熟的圖片加載和緩存開源框架!好比square公司的Picasso框架、Google公司的Glide框架和Facebook公司的Fresco框架等。特別是Fresco框架,提供了三級緩存策略,很是的專業。根據APP對圖片顯示和緩存的需求從低到高排序,咱們能夠採用的方案依次爲:Bitmapfun、Picasso、Android-Universal-Image-Loader、Glide、Fresco。
這些框架能夠方便咱們實現對網絡圖片的加載和緩存操做。具體再也不贅述。