日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Bitmap的加载和Cache

發布時間:2024/3/13 编程问答 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Bitmap的加载和Cache 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

本章的主題是Bitmap的加載和Cache,主要包含三個方面的內容。首先講述如何有效地加載一個Bitmap,這是一個很有意義的話題,由于Bitmap的特殊性以及Android對單個應用所施加的內存限制,比如16MB,這導致Bitmap加載的適合很容易出現內存溢出。下面這個異常信息在開發種應該經常遇到:

因此如何高效的加載Bitmap是一個很重要也很容易被開發者或忽視的問題。 接著介紹Android中常用的緩存策略,緩存策略是一種通用的思想,可以用在很多場景中,但是實際開發中經常需要用Bitmap緩存。通過緩存策略,我們不需要每次都從網絡上請求圖片或者中設備中加載圖片,這樣就極大地提高了圖片加載效率以及產品的用戶體驗。目前比較常用的緩存策略是LruCache和DiskLruCache,其中 LruCache常用用作內存緩存,而Disk LruCache常用用作存儲緩存。Lru是Least Recently Used的所需,即使用最少使用算法,這中算法的核心思想為:當緩存快慢時,會淘汰最近最少使用的緩存目標,很顯然Lru算法的思想時很容易被接受的。 最后本章會介紹如何優化列表的卡頓現象,ListView和GridView由于要加載大量的子視圖,當用戶款蘇滑動時就很容易出現卡頓的現象,因此本章最后針對這個問題將會一一給出一些優化建議。

為了更好地介紹上述三個主題,本章提供了一個示例程序,該程序會嘗試從網絡加載大量圖片,并在GridView中現實,可以發現這個程序具有很強的使用性,并且技術細節完全覆蓋了本章的三個主題:圖片加載、緩存策略、列表的滑動流程性,通過這個示例程序讀者可以很好地理解本章地全部內容并能夠在實際中靈活應用。

1.Bitmap的高效加載

在介紹Bitmap的高效加載之前,先說一下如何加載一個Bitmap,Bitmap在Android中指的是一張圖片,可以是png格式也可以是jpg等其他常見格式。那么如何加載一個圖片呢?BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分別用于支持文件系統、資源、輸入流以及字節數組中加載出一個Bitmap對象,其中decodeFile和decodeResource又間接調用了decodeStream方法,這四類方法最終在Android底層實現的,對應著BitmapFactory類的幾個native方法。

如何高效地加載Bitmap呢?其核心思想也很簡單,那就是采用BitmapFactory.Options來加載所需尺寸那么大,這個時候把整個圖片加載進來后設給ImageView,這顯然是沒有必要的,因為ImageView并沒有辦法現顯示原始的圖片。通過 BitmapFactory.Options就可以按照一定的采樣率來加載縮小后的圖片,將縮小后的圖片在ImageView中顯示,這樣就會降低內存占用從而一定程度上避免OOM,提高了Bitmap加載時的性能。 BitmapFactory提供的加載圖片的四類方法都支持 BitmapFactory.Options參數,通過它們就可以很方面的對一個圖片進行采樣縮放。 通過BitmapFactory.Options來縮放圖片,主要是用到了它的inSampleSize參數,即采樣率。當inSampleSize為1時,采樣后的圖片大小為圖片的原始大小;當inSampleSize大于1時,比如為2,那么采樣后的圖片其寬高為原圖大小的1/2,而像素為原圖的1/4,其占有內存大小也為原圖的1/4.拿一張1024*1024像素的圖片來說,假定采用ARGB8888格式存儲,它占有的內存為1024*1024*4,即4MB,如果inSampleSize為2,那么采樣后的圖片其內存占用只有512*512*4,即1MB。可以發現采樣率inSampleSize必須大于1的正數圖片才會有縮小的效果,并且采樣率同時作用于寬高,這將導致縮放后的圖片大小以采樣率的2次方形式遞減,即縮放比例為1/(inSampleSize的2次方),比如inSampleSize為4時,那么縮放比率為1/16.有一種特殊情況,那就是當inSampleSize小于1時,其作用相當于1,即無縮放效果。另外最新的官方文檔中指出,inSampleSize的取值應該總是為2的指數,比如1、2、4、8、16等等。如果外界傳遞給系統的inSampleSize不為2的指數,那么系統會向下取整并選擇一個最接近的2的指數來代替,比如3,系統會選擇2來代替,但是經過驗證發現這個結論并非在所有的Android版本上都成立,因此把它當成一個開發建議即可。

考慮以下實際情況,比如ImageView的大小是100*100像素,而圖片的原始大小為200*200,那么只需要將采樣率 inSampleSize設為2即可。但是如果圖片大小為200*300呢?這個時候采樣率還應該選擇2,這樣縮放后額大小為100*150像素,仍然是適合ImageView的,如果采樣率為3,那么縮放后的圖片大小就會小于ImageView所期望的大小,這樣圖片就會被拉伸從而導致模糊。 通過采樣率即可有效地加載圖片,那么到底如何獲取采樣率呢?獲取采樣率也很簡單,遵循如下流程:

(1)將 BitmapFactory.Options的inJustDecodeBounds參數設為true并加載圖片。 (2)從BitmapFactory.Options中取出圖片的原始寬高,它們對應于outWidth和outHeight參數。 (3)根據采樣率的規則并結合目標View的所需大小計算出采樣率inSampleSize. (4)將 BitmapFactory.Options的inJustDecodeBounds參數設為false,然后重寫加載圖片。 經過上面4個步驟,加載出的圖片最終是縮放后的圖片,當然也有可能不需要縮放。這里說明以下inJustDecodeBounds參數,當此參數設為true時,BitmapFactory只會解析圖片的原始寬高信息,并不會去真正的加載圖片,所以這個操作是輕量級的。另外需要注意的是,這個適合BitmapFactory獲取的圖片的寬高信息和圖片的位置以及程序運行的設備有關,比如一張圖片放在不同的drawable目錄下或者運行在不同屏幕密度的設備上,這都可能導致BitmapFactory獲取到不同的結果,之所以會出現這個現象,這和Android的資源加載機制有關。 將上面的四個流程用程序來實現,就產生了下面的代碼: public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int rewHeight){final BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(res,resId,options);options.inSampleSize = calculateInSampleSize(options, reqWidth, rewHeight);options.inJustDecodeBounds = true;return BitmapFactory.decodeResource(res,resId,options);}public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int rewHeight){final int height = options.outHeight;final int width = options.outWidth;int inSampleSize = 1;if (height > rewHeight || width > reqWidth) {final int halfHeight = height/2;final int haleWidth = width/2;while ((halfHeight / inSampleSize) >= rewHeight&& (haleWidth/inSampleSize) >= reqWidth){inSampleSize *= 2;}}return inSampleSize;}

有了上面的兩個方法,實際使用的適合就很簡單了,比如ImageView所期望的圖片大小為100*100像素,這個時候就可以通過如下方式高效地加載并顯示圖片:
mImageView.setImageBitmap(BitmapUtil.decodeSampledBitmapFromResource(getResources(),R.mipmap.ic_launcher,100,100));

除了BitmapFactory地decodeResource方法,其他三個decode系統地方法也是支持采樣加載地,并且處理方式也是類似的,但是decodeStream方法稍微有點特殊,這個會在后續內容中詳細介紹。通過本節的介紹,讀者應該能很好地掌握這種高效地加載圖片的方法了。

2. Android中的緩存策略

緩存策略在Android中有著廣泛的應用場景,尤其在圖片加載這個場景下,緩存策略就變得更為重要。考慮一種場景:有一批網絡圖片,需要下載后再用戶界面上予以顯示,這個場景再PC環境下是很簡單的,直接把所有的圖片下載到本地再顯示即可,但是放到移動設備上就不一樣了。不管是Android還是IOS設備,流量對于客戶來說都是一種寶貴的資源,由于流量是收費的,所以在應用開發中并不能過多地消耗用戶的流量,否則這個應用肯定不能被用戶所接受。再加上目前國內公共場所的wifi的普及率并不算高,因此用戶在很多情況下手機上都是用的移動網絡而非wifi,因此必須提供一種解決方案來解決流量的消耗問題。

如何避免多多的流量消耗呢?那就是本節所要討論的主題:緩存。當程序第一次網絡加載圖片后,就將其緩存到存儲設備上,這樣下次使用這張圖片就不用再從網絡上獲取了,這樣就為用戶節省了流量。很多時候為了提高應用的用戶體驗,往往還會把圖片放在內存中再緩存一份,這樣當應用打算從網絡上請求一張圖片時,程序會首先從內存中去獲取,如果內存中沒有那就從存儲設備中去獲取。如果存儲設備中也沒有,那就從網絡上下載這張圖片。因為從內存中加載圖片比從存儲設備中加載圖片要快,所以這樣及提高了程序的效率又為用戶節約了不必要的流量開銷。上述的緩存策略不僅僅適用于圖片,也適用于其他文件類型。 說到緩存策略,其實并沒有統一的標準。一般來說,緩存策略主要包含緩存的添加、獲取和刪除這三類操作。如何添加和獲取這個比較好理解,那為什么還要刪除緩存呢?這是因為不管時內存緩存還是存儲設備緩存,它們的緩存大小都是有限制的,因為內存和諸如SD卡之類的存儲設備都是有容量限制的,因此再使用緩存時總是要為緩存指定一個最大的容量。如果當緩存容量滿了,但是程序還需要向其添加緩存,這個時候該怎么辦?這就需要刪除一些舊的緩存并添加新的緩存,如何定義緩存的新舊這就是一種策略,不同的策略就對應著不同的緩存算法,比如可以簡單地根據文件的最后修改時間來定義緩存的新舊,當緩存滿時就將最后修改時間較早的緩存移除,這就是一種緩存算法,但是這種算法并不算很完美。

目前最常用的一種緩存算法是LRU,LRU是近期最少使用算法,它的核心思想是當緩存滿時,會優先淘汰哪些近期最少使用的緩存對象。采樣LRU算法的緩存有兩種:LruCache和DiskLruCache, LruCache用于實現內存緩存,而DiskLruCache則充當存儲設備緩存,通過這二者的完美結合,就可以很方便地實現一個具有很高使用價值地ImageLoader。本節首先會介紹LruCache和DiskLruCache,然后利用LruCache和DiskLruCache來實現一個優秀地ImageLoader,并且提供一個使用ImageLoader來從網絡下載并展示圖片的例子,在這個例子種體現了ImageLoader以及大批量網絡圖片加載所設計的大量技術點。

2.1LruCache

LruCache是Android 3.1所提供的一個緩存類,通過support-v4兼容包到早期的Android版本,目前Android 2.2以下的用戶量以及很少了,因此我們開發的應用兼容到Android 2.2就已經足夠了。為了能夠兼容Android 2.2版本,在使用LruCache時建議采用support-v4兼容包種的LruCache,而不是直接使用Android 3.1提供的LruCache。 LruCache是一個泛型類,它內部采用了一個LinkedHashMap以及強引用的方式存儲外界的緩存對象,其提供了get和put方法來完成緩存的獲取和添加操作,當緩存滿時,LruCache會移除較早使用的緩存對象,然后再添加新的緩存對象。這里要明白強引用、軟引用和弱引用的區別,如下所示。 強引用:直接的對象引用。

軟引用:當一個對象只有軟引用存在時,系統內存不足時此對象會被gc回收。 弱引用: 當一個對象只有軟引用存在時,此對象會隨時被gc回收。

另外 LruCache是線程安全的。LruCache的實現比較簡單,讀者可以參考它的源碼,這里僅介紹如何使用LruCache來實現內存緩存。仍然拿圖片緩存的來舉例子,下面的代碼展示了LruCache的典型的初始化過程: int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024);int cacheSize = maxMemory /8;mMemoryCache = new LruCache<String, Bitmap>(cacheSize){@Overrideprotected int sizeOf(String key, Bitmap bitmap) {return bitmap.getRowBytes() * bitmap.getHeight() / 1024;}};

在上面的代碼種,只需要提供緩存的總容量大小并重寫sizeof方法即可。sizeOf方法的作用是計算緩存對象的大小,這里的大小的單位需要和總容量的單位移植。對于上面的示例代碼來說,總容量大小為當前進程可以內存的1/8,單位為KB,而sizeOf方法則完成了Bitmap對象的大小計算。很明顯,之所以除以1024也是為了將其單位轉換為KB。一些特殊情況下,還需要重寫 LruCache的entryRemoved方法, LruCache移除就緩存時會調用 entryRemoved方法,因此可以在entryRemoved種完成一些資源回收工作。 除了 LruCache的創建外,還有緩存的獲取和添加,這也很簡單,從 LruCache獲取一個緩存對象,如下所示。
mMemoryCache.get(key);

LruCache中添加一個緩存對象,如下所示。
mMemoryCache.put(key,bitmap);

LruCache還支持刪除操作,通過remove方法即可刪除一個指定的緩存對象。可以看到 LruCache的實現以及使用都非常簡單,雖然簡單,但是仍不影響它具有強大的功能。

2.2 DiskLruCache

Disk LruCache用于實現存儲設備緩存,即磁盤緩存,它通過將緩存對象寫入文件系統從而實現緩存的效果。DiskLruCache得到了Android官方文檔的推薦,但它不屬于Android SDK的一部分,它的源碼請讀者自行獲取。 下面分別從 Disk LruCache的創建、緩存查找和緩存添加這三個方面來解釋 Disk LruCache的使用方式。 1.DiskLruCache的創建 Disk LruCache并不能通過構造方法來創建,它提供了open方法用于創建自身,如下所示。 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)throws IOException

open方法有4個參數,其中第一個參數表示磁盤緩存在文件系統中的存儲路徑。緩存路徑可以選擇SD卡上的緩存目錄,具體是指/sdcard/Android/data/package_name/cache目錄,其中package_name表示當前應用的包名,當應用被卸載后,此目錄一并被刪除。當然也可以選擇SD卡上的其他指定目錄,還可以選擇data下的當前應用的目錄,具體可以根據需要靈活設定。這里給出一個建議:如果應該卸載后就希望刪除緩存文件,那么就選擇SD卡上的緩存目錄,如果希望保留緩存數據,那就應該選擇SD卡上的其他特定目錄。 第二個參數表示應用的版本號,一般設為1即可。當版本號發送改變時 Disk LruCache會情況之前所有的緩存文件,而這個特性在實際開發中作用不大,很多情況下即使應用的版本號發送了改變緩存文件卻仍然是有效的,因此這個參數設為1比較好。 第三個參數表示單個節點所對應的數據的個數,一般設為1即可。第4個參數表示緩存的總大小,比如50MB,當緩存大小超出這個設定值后, Disk LruCache會清楚一些緩存從而保證大小不大于這個設定值。下面是一個典型的 Disk LruCache的創建過程:
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");if (!diskCacheDir.exists()){diskCacheDir.mkdirs();}try {mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);} catch (IOException e) {e.printStackTrace();}

2.DiskLruCache的緩存添加 Disk LruCache的緩存添加的操作是通過Editor完成的,Editor表示一個緩存對象的編輯對象。這里仍然以圖片緩存舉例,首先火藥獲取url所對應的key,然后更具key就可以通過edit()來獲取Editor對象,如果這個緩存正在被編輯,那么edit()會返回null,即 Disk LruCache不允許同時編輯一個緩存對象。之所以要把url轉換成key,是因為圖片url中很有可能存在特殊字符,這將影響url在Android中直接使用,一般采用url的md5作為key。如下所示。
private String hashKeyFromUrl(String url){String cacheKey;try {final MessageDigest digest = MessageDigest.getInstance("MD5");digest.update(url.getBytes());cacheKey = bytesToHexString(digest.digest());} catch (NoSuchAlgorithmException e) {cacheKey = String.valueOf(url.hashCode());}return cacheKey;}private String bytesToHexString(byte[] bytes){StringBuilder sb = new StringBuilder();for (int i=0; i< bytes.length; i++) {String hex = Integer.toHexString(0xFF & bytes[i]);if (hex.length() == 1){sb.append("0");}sb.append(hex);}return sb.toString();}

將圖片的url轉成key以后,就可以獲取Editor對象了。對于這個key來說,如果當前不存在其他Editor對象,那么edit()就會返回一個新的Editor對象,通過它就可以得到一個文件輸出流。需要注意的是前面在 Disk LruCache的open方法中設置了一個節點只能有一個數據,因此下面的DISK_CACHE_INDEX常量直接設置為0即可,如下所示。
String key = hashKeyFromUrl(url);try {DiskLruCache.Editor editor = mDiskLruCache.edit(key);if (editor != null) {OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);}} catch (IOException e) {e.printStackTrace();}

有了文件輸出流,接下來要怎么做呢?其實是這也的,當從網絡下載圖片時,圖片即可以通過這個文件輸出流寫入到文件系統上,這個過程的實現如下所示。
private boolean downloadUrlToStream(String urlString, OutputStream outputStream){HttpURLConnection urlConnection = null;BufferedInputStream in = null;BufferedOutputStream out = null;try {final URL url = new URL(urlString);urlConnection = (HttpURLConnection)url.openConnection();in = new BufferedInputStream(urlConnection.getInputStream());out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);int b;while ((b = in.read())!=-1){out.write(b);}return true;} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (urlConnection != null){urlConnection.disconnect();}MyUtils.close(out);MyUtils.close(in);}return false;}

經過上面的步驟,其實并沒有真正的將圖片寫入文件系統,還必須通過Editor的commit()來提交寫入操作,如果圖片下載過程發送了異常,那么還可以通過Editor的abort()來回退整個操作,這個過程如下所示。
OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);if (downloadUrlToStream(url, outputStream)){editor.commit();} else {editor.abort();}mDiskLruCache.flush();

經過上面的幾個步驟,圖片以及被正確地寫入到文件系統了,接下來圖片獲得的操作就需要請求網絡了。

3.DiskLruCache的緩存查找 和緩存添加過程類似,緩存查找也需要將url轉換為key,然后通過DiskLruCache的get方法得到一個Snapshot對象,接著再通過Snapshot對象即可得到緩存文件的輸出流,有了文件輸出流,自然就可以得到Bitmap對象了。為了避免加載過程中導致的OOM問題,一般不建議直接加載原始圖片。再1節中已經介紹了通過BitmapFactory.Options對象來加載一張縮放后的圖片,但是那種方法對FileInputStream的縮放存在問題,原因是FileInputStream是一種有序的文件流,而兩次decodeStream調用影響了文件流的位置屬性,導致了第二次decodeStream時得到的是null。為了解決這個問題,可以通過文件流來得到它所對應的文件描述符,然后再通過BitmapFactory.decodeFileDescriptor方法來加載一張縮放后的圖片,這個過程實現如下所示。 Bitmap bitmap = null;String key = hashKeyFromUrl(url);try {DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);if (snapshot != null) {FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX);FileDescriptor descriptor = fileInputStream.getFD();bitmap = BitmapUtil.decodeSampledBitmapFromFileDescriptor(descriptor,reqWidth,rewHeight);if (bitmap != null){addBitmapToMemoryCache(key,bitmap);}}} catch (IOException e) {e.printStackTrace();}return bitmap;

上面介紹了 Disk LruCache的創建、緩存的添加過和查找過程,讀者應該對 Disk LruCache的使用方式有了一個大致的了解,除此之外, Disk LruCache還提供了remove、delete等方法用于磁盤緩存的刪除操作。關于 Disk LruCache的內部實現這里就不再介紹了,感興趣的朋友可以查看它的源碼實現。

2.3 ImageLoader的實現

在本章的前面先后介紹了Bitmap的高效加載方式、LruCache以及 Disk LruCache,現在我們來著手實現一個優秀的ImageLoader。 一般來說,一個優秀的ImageLoader應該具備如下功能:

圖片的同步加載、圖片的異步加載、圖片壓縮、內存緩存、磁盤緩存、網絡拉取。 圖片的同步加載是指能夠以同步的方式向調用者提供鎖甲在的圖片,這個圖片可能是從內存中讀取的,也可能是從磁盤緩存中讀取的,還可能使從網絡拉取的。圖片的異步加載時一個很有用的功能,很多時候調用者不想再單獨的線程中已同步的方式來獲取圖片,這個時候ImageLoader內部需要自己在線程中加載圖片并將圖片設置給所需的ImageView。圖片壓縮的作用更毋庸置疑了,這是降低OOM概率的有效手段,ImageLoader必須合適地處理圖片的壓縮問題。

內存緩存和磁盤緩存時 ImageLoader的核心,也是 ImageLoader的意義所在,通過這兩集緩存極大地提高了程序的效率并且有效地降低了對用戶造成地流量消耗,只有當這兩級緩存都不可以時才需要從網絡中拉取圖片。

除此之外, ImageLoader還需要處理一些特殊情況,比如在ListView或者GridView中,View復用既是它們的有點也是它們的缺點,有點想必應該都清楚了,那缺點可能還不太清楚。考慮一種情況,在 ListView或者GridView中,假設一個item A 正在從網絡加載圖片,它對應的ImageView為A,這個時候用戶快速地向下滑動列表,很可能item B復用了ImageView A,然后等了一會之前的圖片下載完畢了。 如果直接給ImageView A設置圖片,由于這個時候ImageView A被item B所復用,但是item B顯然不是item A剛剛下載好的圖片,這個時候會出現B中顯示了A的圖片,這就是常見的列表的錯位問題,ImageLoader需要正確地處理這些特殊情況。 上面對ImageLoader的功能做了一個全面的分析,下面就可以一步步實現ImageLoader了,這里主要分為如下幾步。 1.圖片的壓縮功能的實現

圖片壓縮在1節中已經做了介紹,這里就不再多說了,為了有良好的設計風格,這里單獨抽象了一個類用于完成圖片的壓縮功功能,這個類叫ImageResizer,它的實現如下所示。 public class ImageResizer {public ImageResizer() {}public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor descriptor, int reqWidth, int rewHeight ){final BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeFileDescriptor(descriptor, null,options);options.inSampleSize = calculateInSampleSize(options, reqWidth, rewHeight);options.inJustDecodeBounds = false;return BitmapFactory.decodeFileDescriptor(descriptor, null,options);}public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int rewHeight){final BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(res,resId,options);options.inSampleSize = calculateInSampleSize(options, reqWidth, rewHeight);options.inJustDecodeBounds = false;return BitmapFactory.decodeResource(res,resId,options);}public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int rewHeight){if (reqWidth == 0 || rewHeight == 0) {return 1;}final int height = options.outHeight;final int width = options.outWidth;int inSampleSize = 1;if (height > rewHeight || width > reqWidth) {final int halfHeight = height/2;final int haleWidth = width/2;while ((halfHeight / inSampleSize) >= rewHeight&& (haleWidth/inSampleSize) >= reqWidth){inSampleSize *= 2;}}return inSampleSize;} }

2.內存緩存和磁盤緩存的實現

這里選擇LruCache和Disk LruCache來分貝完成內存緩存和磁盤緩存的工作。在ImageLoader初始化時,會創建 LruCache和Disk LruCache,如下所示。
private LruCache<String, Bitmap> mMemoryCache;private DiskLruCache mDiskLruCache;public ImageLoader(Context context) {mContext = context.getApplicationContext();int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024);int cacheSize = maxMemory /8;mMemoryCache = new LruCache<String, Bitmap>(cacheSize){@Overrideprotected int sizeOf(String key, Bitmap bitmap) {return bitmap.getRowBytes() * bitmap.getHeight() / 1024;}};File diskCacheDir = getDiskCacheDir(mContext, "bitmap");if (!diskCacheDir.exists()){diskCacheDir.mkdirs();}if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {try {mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);mIsDiskLruCacheCreated = true;} catch (IOException e) {e.printStackTrace();}}}

在創建磁盤緩存時,這里做了判斷,即有可能磁盤剩余空間小于磁盤緩存所需的大小,一般是指用戶的手機空間已經不足了,因此沒有辦法創建磁盤緩存,這個時候磁盤緩存就會失效。在上面的海馬實現中,ImageLoader的內存緩存容量為當前進程可用內存的1/8,磁盤緩存的容量是50MB。 內存緩存和磁盤緩存創建完畢后,還需要提高方法來完成緩存的添加和獲取功能。首先看內存緩存,它的添加和讀取過程比較簡單,如下所示。
private Bitmap getBitmapFromMemoryCache(String key){return mMemoryCache.get(key);}private void addBitmapToMemoryCache(String key, Bitmap bitmap){if (getBitmapFromMemoryCache(key) == null) {mMemoryCache.put(key, bitmap);}}

而磁盤緩存和讀取功能稍微復雜一些,具體內容已經在2.2節中進行了詳細的介紹,這里再簡單說明一下。磁盤緩存的添加需要通過Editor來完成, Editor提高了commit和abort方法來提交和撤銷對文件系統的寫操作,具體實現請參看下面的loadBitmapFromHttp方法。磁盤緩存的讀取需要通過Snapshot來完成,通過Snapshot可以得到磁盤緩存對象對應的FileInputStream,但是 FileInputStream無法邊界的進行壓縮,所以通過FileDescriptor來加載壓縮后的圖片,最后將加載后的Bitmap添加到內存中,具體實現請參考下面的loadBitmapFromDiskCache方法。
private Bitmap loadBitmapFromDiskCache(String url,int reqWidth, int rewHeight) throws IOException{if (Looper.myLooper() == Looper.getMainLooper()){throw new RuntimeException("can not visit network from UI thread.");}if (mDiskLruCache == null){return null;}Bitmap bitmap = null;String key = hashKeyFromUrl(url);DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);if (snapshot != null) {FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX);FileDescriptor descriptor = fileInputStream.getFD();bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(descriptor,reqWidth,rewHeight);if (bitmap != null){addBitmapToMemoryCache(key,bitmap);}}return bitmap;}private Bitmap loadBitmapFromHttp(String url,int reqWidth, int rewHeight)throws IOException{if (Looper.myLooper() == Looper.getMainLooper()){throw new RuntimeException("can not visit network from UI thread.");}if (mDiskLruCache == null){return null;}String key = hashKeyFromUrl(url);DiskLruCache.Editor editor = mDiskLruCache.edit(key);if (editor != null) {OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);if (downloadUrlToStream(url, outputStream)){editor.commit();} else {editor.abort();}mDiskLruCache.flush();}return loadBitmapFromDiskCache(url, reqWidth, rewHeight);}

3.同步加載和異步加載的接口設計 首先看同步加載,同步加載接口需要外部在線程中調用,這是因為同步很可能比較耗時,它的實現如下所示。
public Bitmap loadBitmap(String url,int reqWidth, int rewHeight){Bitmap bitmap = loadBitmapFromMemoryCache(url);if (bitmap != null) {return bitmap;}try {bitmap = loadBitmapFromDiskCache(url,reqWidth,rewHeight);if (bitmap != null) {return bitmap;}bitmap = loadBitmapFromHttp(url,reqWidth,rewHeight);} catch (IOException e) {e.printStackTrace();}if (bitmap == null && !mIsDiskLruCacheCreated) {bitmap = downloadFromUrl(url);}return bitmap;}

從loadBitmap的實現可以看出,其工作過程遵循如下幾步:首先嘗試從內存中讀取圖片,接著嘗試從磁盤緩存中讀取圖片,最后才從網絡中拉取圖片。另外,這個方法不能在主線程中調用,否則就會拋出異常。這個執行換下的檢查時在loadBitmapFromHttp中實現的,通過檢測當前線程的Looper是否為主線的Looper來判斷當前線程是否是主線程,如果不是主線程就直接拋出異常終止程序,如下所示。
if (Looper.myLooper() == Looper.getMainLooper()){throw new RuntimeException("can not visit network from UI thread.");}

接著看異步加載接口的設計,如下所示。
public void bindBitmap(final String uri, final ImageView imageView,final int reqWidth, final int reqHeight){imageView.setTag(TAG_KEY_URI, uri);final Bitmap bitmap = loadBitmapFromMemoryCache(uri);if (bitmap != null) {imageView.setImageBitmap(bitmap);return;}final Runnable loadBitmapTask = new Runnable() {@Overridepublic void run() {Bitmap bitmap1 = loadBitmap(uri,reqWidth,reqHeight);if (bitmap1 != null) {LoaderResult result = new LoaderResult(imageView, uri, bitmap);mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();}}};THREAD_POOL_EXECUTOR.execute(loadBitmapTask);}

從bindBitmap的實現來看,bindBitmap方法會嘗試從內存緩存中讀取圖片,如果讀取成功就直接返回,否則會在線程池中去調用loadBitmap方法,當圖片加載成功后再將圖片、圖片地址已經需要綁定的ImageView封裝成一個LoadResult對象,然后再通過mMainHandler向主線程發送一個消息,這也就可以在主線程中給ImageView設置圖片了,之所以通過Handler來中專是因為子線程無法訪問UI。

bindBitmap中用到了線程池和Handler,這里看一下它們的實現,首先看線程池THREAD_POOL_ECECUTOR的實現,如下所示。可以看出它的核心線程數為當前設備的CPU核心數+1,最大容量為CPU核心數的2倍加1,線程閑置超時時長為10秒,關于線程池的解釋可以看11章節的內容。 private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private static final int CORE_POOL_SIZE = CPU_COUNT + 1;private static final int MAX_POOL_SIZE = CPU_COUNT*2 + 1;private static final long KEEP_ALIVE = 10L; private static final ThreadFactory sThreadFactory = new ThreadFactory() {private final AtomicInteger mCount = new AtomicInteger(1);@Overridepublic Thread newThread(@NonNull Runnable r) {return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());}};public static final Executor THREAD_POOL_EXECUTOR =new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>(), sThreadFactory);

之所以采用線程池是有原因的,首先肯定不能采用普通的線程去做這個事,線程池的好處在11章已經做了詳細的說明。如果直接采用普通的線程去加載圖片,隨著列表的滑動這可能會產生大量的線程,這也并不利于整體效率的提示。另外一點,這里也沒有選擇采用AsyncTask, AsyncTask封裝了線程池和Handler,按道理它應該最適合ImageLoader的場景。從11章對 AsyncTask的分析可以知道, AsyncTask在3.0的低版本和高版本上具有不同的表現,在3.0以上的版本 AsyncTask無法實現并發效果,這顯然是不能接受的,因為ImageLoader需要并發現,雖然可以通過改造 AsyncTask或者使用 AsyncTask的executeExecutor方式的形式來執行異步任務,但是這最終不是太自然的實現方式。鑒于以上兩點原因,這里選擇線程池和Handler來提高ImageLoader的并發能力和訪問UI的能力。 分析完線程的選擇,下面看一下Handler的實現,如下所示。ImageLoader直接采用主線程的Looper來構造Handler對象,這就使得ImageLoader可以在非主線程中構造了。另外為了解決由于View復用所導致的列表錯位的這一問題,在給ImageView設置圖片之前都會檢查它的url有沒有發生改變,如果發送改變就不再給他設置圖片,這樣就解決了列表的錯位問題。
private Handler mMainHandler = new Handler(Looper.getMainLooper()){@Overridepublic void handleMessage(Message msg) {LoaderResult result = (LoaderResult) msg.obj;ImageView imageView = result.imageView;String uri = (String) imageView.getTag();if (uri.equals(result.uri)){imageView.setImageBitmap(result.bitmap);} else {Log.d(TAG, "set image bitmap, but uri has changed,ignored!");}}};

到此為止,ImageLoader的細節都已經做了全面的分析,下面是ImageLoader的完整代碼。
public class ImageLoader {private static final String TAG= "ImageLoader";private static final int MESSAGE_POST_RESULT = 1;private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private static final int CORE_POOL_SIZE = CPU_COUNT + 1;private static final int MAX_POOL_SIZE = CPU_COUNT*2 + 1;private static final long KEEP_ALIVE = 10L;private static final int IO_BUFFER_SIZE = 8 * 1024;private static final int TAG_KEY_URI = R.id.imageloader_url;private static final long DISK_CACHE_SIZE = 1024 * 1024 *50;private static final int DISK_CACHE_INDEX = 0;private boolean mIsDiskLruCacheCreated = false;private static final ThreadFactory sThreadFactory = new ThreadFactory() {private final AtomicInteger mCount = new AtomicInteger(1);@Overridepublic Thread newThread(@NonNull Runnable r) {return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());}};public static final Executor THREAD_POOL_EXECUTOR =new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE, TimeUnit.SECONDS,new LinkedBlockingDeque<Runnable>(), sThreadFactory);private Handler mMainHandler = new Handler(Looper.getMainLooper()){@Overridepublic void handleMessage(Message msg) {LoaderResult result = (LoaderResult) msg.obj;ImageView imageView = result.imageView;String uri = (String) imageView.getTag();if (uri.equals(result.uri)){imageView.setImageBitmap(result.bitmap);} else {Log.d(TAG, "set image bitmap, but uri has changed,ignored!");}}};private Context mContext;private LruCache<String, Bitmap> mMemoryCache;private DiskLruCache mDiskLruCache;private ImageResizer mImageResizer;private ImageLoader(Context context) {mImageResizer = new ImageResizer();mContext = context.getApplicationContext();int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024);int cacheSize = maxMemory /8;mMemoryCache = new LruCache<String, Bitmap>(cacheSize){@Overrideprotected int sizeOf(String key, Bitmap bitmap) {return bitmap.getRowBytes() * bitmap.getHeight() / 1024;}};File diskCacheDir = getDiskCacheDir(mContext, "bitmap");if (!diskCacheDir.exists()){diskCacheDir.mkdirs();}if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {try {mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);mIsDiskLruCacheCreated = true;} catch (IOException e) {e.printStackTrace();}}}public static ImageLoader build(Context context){return new ImageLoader(context);}private Bitmap getBitmapFromMemoryCache(String key){return mMemoryCache.get(key);}private void addBitmapToMemoryCache(String key, Bitmap bitmap){if (getBitmapFromMemoryCache(key) == null) {mMemoryCache.put(key, bitmap);}}public void bindBitmap(final String uri, final ImageView imageView){bindBitmap(uri,imageView,0,0);}public void bindBitmap(final String uri, final ImageView imageView,final int reqWidth, final int reqHeight){imageView.setTag(TAG_KEY_URI, uri);final Bitmap bitmap = loadBitmapFromMemoryCache(uri);if (bitmap != null) {imageView.setImageBitmap(bitmap);return;}final Runnable loadBitmapTask = new Runnable() {@Overridepublic void run() {Bitmap bitmap1 = loadBitmap(uri,reqWidth,reqHeight);if (bitmap1 != null) {LoaderResult result = new LoaderResult(imageView, uri, bitmap);mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result).sendToTarget();}}};THREAD_POOL_EXECUTOR.execute(loadBitmapTask);}public Bitmap loadBitmap(String url,int reqWidth, int rewHeight){Bitmap bitmap = loadBitmapFromMemoryCache(url);if (bitmap != null) {return bitmap;}try {bitmap = loadBitmapFromDiskCache(url,reqWidth,rewHeight);if (bitmap != null) {return bitmap;}bitmap = loadBitmapFromHttp(url,reqWidth,rewHeight);} catch (IOException e) {e.printStackTrace();}if (bitmap == null && !mIsDiskLruCacheCreated) {bitmap = downloadFromUrl(url);}return bitmap;}private Bitmap loadBitmapFromMemoryCache(String url){final String key = hashKeyFromUrl(url);return getBitmapFromMemoryCache(key);}private Bitmap loadBitmapFromHttp(String url,int reqWidth, int rewHeight)throws IOException{if (Looper.myLooper() == Looper.getMainLooper()){throw new RuntimeException("can not visit network from UI thread.");}if (mDiskLruCache == null){return null;}String key = hashKeyFromUrl(url);DiskLruCache.Editor editor = mDiskLruCache.edit(key);if (editor != null) {OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);if (downloadUrlToStream(url, outputStream)){editor.commit();} else {editor.abort();}mDiskLruCache.flush();}return loadBitmapFromDiskCache(url, reqWidth, rewHeight);}private Bitmap loadBitmapFromDiskCache(String url,int reqWidth, int rewHeight) throws IOException{if (Looper.myLooper() == Looper.getMainLooper()){throw new RuntimeException("can not visit network from UI thread.");}if (mDiskLruCache == null){return null;}Bitmap bitmap = null;String key = hashKeyFromUrl(url);DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);if (snapshot != null) {FileInputStream fileInputStream = (FileInputStream)snapshot.getInputStream(DISK_CACHE_INDEX);FileDescriptor descriptor = fileInputStream.getFD();bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(descriptor,reqWidth,rewHeight);if (bitmap != null){addBitmapToMemoryCache(key,bitmap);}}return bitmap;}private boolean downloadUrlToStream(String urlString, OutputStream outputStream){HttpURLConnection urlConnection = null;BufferedInputStream in = null;BufferedOutputStream out = null;try {final URL url = new URL(urlString);urlConnection = (HttpURLConnection)url.openConnection();in = new BufferedInputStream(urlConnection.getInputStream());out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);int b;while ((b = in.read())!=-1){out.write(b);}return true;} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (urlConnection != null){urlConnection.disconnect();}MyUtils.close(out);MyUtils.close(in);}return false;}private Bitmap downloadFromUrl(String urlString){Bitmap bitmap = null;HttpURLConnection urlConnection = null;BufferedInputStream in = null;try {final URL url = new URL(urlString);urlConnection = (HttpURLConnection)url.openConnection();in = new BufferedInputStream(urlConnection.getInputStream());bitmap = BitmapFactory.decodeStream(in);} catch (MalformedURLException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (urlConnection != null) {urlConnection.disconnect();}MyUtils.close(in);}return bitmap;}private String hashKeyFromUrl(String url){String cacheKey;try {final MessageDigest digest = MessageDigest.getInstance("MD5");digest.update(url.getBytes());cacheKey = bytesToHexString(digest.digest());} catch (NoSuchAlgorithmException e) {cacheKey = String.valueOf(url.hashCode());}return cacheKey;}private String bytesToHexString(byte[] bytes){StringBuilder sb = new StringBuilder();for (int i=0; i< bytes.length; i++) {String hex = Integer.toHexString(0xFF & bytes[i]);if (hex.length() == 1){sb.append("0");}sb.append(hex);}return sb.toString();}private File getDiskCacheDir(Context context, String name) {boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);final String cachePath;if (externalStorageAvailable){cachePath = context.getExternalCacheDir().getPath();} else {cachePath = context.getCacheDir().getPath();}return new File(cachePath + File.separator + name);}@TargetApi(Build.VERSION_CODES.GINGERBREAD)private long getUsableSpace(File path){if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){return path.getUsableSpace();}final StatFs stats = new StatFs(path.getPath());return (long)stats.getBlockSize() * (long)stats.getAvailableBlocks();}private static class LoaderResult {public ImageView imageView;public String uri;public Bitmap bitmap;public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {this.imageView = imageView;this.uri = uri;this.bitmap = bitmap;}} }

3.ImageLoader的使用

在2.3節中我們實現了一個完整功能的 ImageLoader,本節將演示如何通過 ImageLoader來實現一個照片強的效果,實際上我們會發現,通過 ImageLoader打造一個照片墻是輕而易舉的事情。最后針對如何提高列表流程都這個問題,本節會給出一些針對性的建議供讀者參考。

3.1照片強效果

實現照片強效果需要用到GridView,下面先準備好GridView所需的布局文件以及item的布局文件,如下所示。 #GridView的的布局文件 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context="study.chenj.chapter_9.TestActivity"><GridViewandroid:id="@+id/gridView1"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:horizontalSpacing="5dp"android:verticalSpacing="5dp"android:listSelector="@android:color/transparent"android:numColumns="3"android:stretchMode="columnWidth"/> </LinearLayout>#GridView的item的布局文件 <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"android:layout_height="wrap_content"android:gravity="center"android:orientation="vertical"><study.chenj.chapter_9.SquareImageViewandroid:id="@+id/image"android:scaleType="centerCrop"android:src="@mipmap/ic_launcher"android:layout_width="match_parent"android:layout_height="wrap_content" /> </LinearLayout>

GridView的item的布局文件中并沒有采用ImageView,而是采用了一個叫SquareImageView的自定義控件。顧名思義,它的作用就是打造一個正方形的ImageView,這樣整個照片墻看起來會比較整齊美觀。需要實現一個寬高相等的ImageView是非常簡單的是一件事,只需要在onMeasure方法中稍微處理一下,如下所示。
public class SquareImageView extends android.support.v7.widget.AppCompatImageView {public SquareImageView(Context context) {super(context);}public SquareImageView(Context context, @Nullable AttributeSet attrs) {super(context, attrs);}public SquareImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {super.onMeasure(widthMeasureSpec, widthMeasureSpec);} }

可以看出,我們在SquareImageView的onMeasure方法中很巧妙地將heightMeasureSpec替換為widthMeasureSpec,這一什么都不用做就可以一個寬高相等的ImageView了。關于View的測量過程在前面已經結束過了。 接著需要實現一個BaseAdapter給GridView使用,下面的代碼展示了ImageAdapter的實現細節,其中mUrList中存儲的是圖片的url:
public class ImageAdapter extends BaseAdapter {...@Overridepublic int getCount() {return mUrList.size();}@Overridepublic Object getItem(int position) {return mUrList.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {ViewHolder holder = null;if (convertView == null){convertView = LayoutInflater.from(mContext).inflate(R.layout.grid_item,parent,false);holder = new ViewHolder();holder.imageView = (ImageView)convertView.findViewById(R.id.image);convertView.setTag(holder);} else {holder = (ViewHolder)convertView.getTag();}ImageView imageView = holder.imageView;final String tag = (String)imageView.getTag();final String url = (String)getItem(position);if (!url.equals(tag)){imageView.setImageDrawable(mDefaultBitmapDrawable);}if (mIsGridViewIdle && mCanGetBitmapFromNetWork){imageView.setTag(url);mImageLoader.bindBitmap(url,imageView,mImageWidth, mImageWidth);}return convertView;}}

從上述代碼來看,ImageAdapter的實現過程非常簡捷,這幾乎是最簡潔的BaseAdapter的實現了。但是簡潔并不等于簡單,getView方法中核心代碼只有一句,那就是:mImageLoader.bindBitmap(url,imageView,mImageWidth, mImageWidth). 通過bindBitmap方法很輕松地將復雜的圖片加載過程交給了ImageLoader, ImageLoader加載圖片以后會把圖片自動設置給imageView,而整個過程,包括內存緩存、磁盤緩存一句圖片壓縮等工作過程對ImageAdapter來說都是透明的。在這張設計思想下, ImageAdapter也不需要知道,因此這是一個輕量級的 ImageAdapter。 接著將 ImageAdapter設置給GridView,如下所示。到此為止一個絢麗的圖片墻就大功告成了,是不是驚嘆于如此簡潔而又優美的實現過程呢?
GridView gridView = (GridView)findViewById(R.id.gridView1);gridView.setAdapter(new ImageAdapter(this,getUrls()));

最后看一下我們親手打造的圖片墻的效果圖,是不是看起來很優美呢?

3.2 優化列表的卡頓現象

這個問題困擾了很多開發者,其實答案很簡單,不要在主線程中做太多的耗時的操作即可提高滑動的流暢度,可以從三個方面來說。

首先,不要再getView中執行耗時操作。對于上面的例子來說,如果直接再getView中加載圖片,肯定會導致卡頓,加載圖片是一個耗時的操作,這張操作必須通過異步的方式來處理,就像ImageLoader實現的那樣。 其次,控制異步的任務執行頻率。這一點也很重要,對于列表來說,僅僅再getView中采用異步是不夠的。考慮一種情況,以照片墻來說,在getView方法中會通過ImageLoader的bindBitmap方法來異步加載圖片,但是如果用戶可以頻繁上下滑動,這就會在一瞬間產生上百個異步任務,這些異步任務會造成線程池的擁堵并隨即帶來大量的UI更新操作,這是沒有意義的。由于一瞬間存在大量的UI操作,這些UI操作是運行在主線程的,這就會造成一定的卡頓現象。如何解決這個問題呢?可以考慮在滑動的時候停止加載圖片,盡管這個過程是異步的,等列表停下來以后再加載圖片仍然可以獲得良好的用戶體驗。具體實現時,可以給ListView或者GridView設置setOnScrollListener,并在 setOnScrollListener的onScrollStateChanged方法中判斷列表是否處于滑動狀態,如果是的話,就停止加載圖片,如下所示。
public void onScrollStateChanged(AbsListView view, int scrollState) {if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE){mIsGridViewIdle = true;ImageAdapter.this.notifyDataSetChanged();} else {mIsGridViewIdle = false;}}

然后在getView方法中,僅當列表靜止時才能加載圖片,如下所示。
if (mIsGridViewIdle){imageView.setTag(url);mImageLoader.bindBitmap(url,imageView,mImageWidth, mImageWidth);}一般來說經過上面兩個步驟,列表都不會有卡頓現象,但是在某些特殊情況下,列表還是會有偶爾卡頓線程,這個時候還可以開啟硬件加速。絕大多說情況下,硬件加速可以解決莫名的卡頓問題,通過設置android:hardwareAccelerated="true"即可為Activity開啟硬件加速。

總結

以上是生活随笔為你收集整理的Bitmap的加载和Cache的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。