Android 基礎 十二 Bitmap的載入和Cache
因此如何高效的載入Bitmap是一個很重要也很容易被開發者或忽視的問題。
接著介紹Android中常用的快取策略,快取策略是一種通用的思想,可以用在很多場景中,但是實際開發中經常需要用Bitmap快取。通過快取策略,我們不需要每次都從網路上請求圖片或者中裝置中載入圖片,這樣就極大地提高了圖片載入效率以及產品的使用者體驗。目前比較常用的快取策略是LruCache和DiskLruCache,其中LruCache常用用作記憶體快取,而DiskLruCache常用用作儲存快取。Lru是Least Recently Used的所需,即使用最少使用演算法,這種演算法的核心思想為:當快取快滿時,會淘汰最近最少使用的快取目標,很顯然Lru演算法的思想是很容易被接受的。 最後本章會介紹如何優化列表的卡頓現象,ListView和GridView由於要載入大量的子檢視,當用戶快速滑動時就很容易出現卡頓的現象,因此本章最後針對這個問題將會一一給出一些優化建議。為了更好地介紹上述三個主題,本章提供了一個示例程式,該程式會嘗試從網路載入大量圖片,並在GridView中現實,可以發現這個程式具有很強的使用性,並且技術細節完全覆蓋了本章的三個主題:圖片載入、快取策略、列表的滑動流程性,通過這個示例程式讀者可以很好地理解本章地全部內容並能夠在實際中靈活應用。
一 Bitmap的高效載入
在介紹Bitmap的高效載入之前,先說一下如何載入一個Bitmap,Bitmap在Android中指的是一張圖片,可以是png格式也可以是jpg等其他常見格式。那麼如何載入一個圖片呢?BitmapFactory類提供了四類方法:decodeFile、decodeResource、decodeStream和decodeByteArray,分別用於支援檔案系統、資源、輸入流以及位元組陣列中加載出一個Bitmap物件,其中decodeFile和decodeResource又間接呼叫了decodeStream方法,這四類方法最終在Android底層實現的,對應著BitmapFactory類的幾個native方法。 如何高效地載入Bitmap呢?其核心思想也很簡單,那就是採用BitmapFactory.Options來載入所需尺寸的圖片,很多時候ImageView並沒有圖片的原始尺寸那麼大,這個時候把整個圖片載入進來後設給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,然後重寫載入圖片。
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); } (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方法稍微有點特殊,這個會在後續內容中詳細介紹。通過本節的介紹,讀者應該能很好地掌握這種高效地載入圖片的方法了。
二 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回收。
int maxMemory = (int)(Runtime.getRuntime().maxMemory()/1024); int cacheSize = maxMemory /8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize){ @Override protected 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
DiskLruCache用於實現儲存裝置快取,即磁碟快取,它通過將快取物件寫入檔案系統從而實現快取的效果。DiskLruCache得到了Android官方文件的推薦,但它不屬於Android SDK的一部分,它的原始碼請讀者自行獲取。 下面分別從DiskLruCache的建立、快取查詢和快取新增這三個方面來解釋DiskLruCache的使用方式。- 1.DiskLruCache的建立
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOExceptionopen方法有4個引數,其中第一個引數表示磁碟快取在檔案系統中的儲存路徑。快取路徑可以選擇SD卡上的快取目錄,具體是指/sdcard/Android/data/package_name/cache目錄,其中package_name表示當前應用的包名,當應用被解除安裝後,此目錄一併被刪除。當然也可以選擇SD卡上的其他指定目錄,還可以選擇data下的當前應用的目錄,具體可以根據需要靈活設定。這裡給出一個建議:如果應該解除安裝後就希望刪除快取檔案,那麼就選擇SD卡上的快取目錄,如果希望保留快取資料,那就應該選擇SD卡上的其他特定目錄。 第二個引數表示應用的版本號,一般設為1即可。當版本號發生改變時DiskLruCache會清空之前所有的快取檔案,而這個特性在實際開發中作用不大,很多情況下即使應用的版本號傳送了改變快取檔案卻仍然是有效的,因此這個引數設為1比較好。 第三個引數表示單個節點所對應的資料的個數,一般設為1即可。 第4個引數表示快取的總大小,比如50MB,當快取大小超出這個設定值後,DiskLruCache會清除一些快取從而保證大小不大於這個設定值。下面是一個典型的DiskLruCache的建立過程:
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的快取新增
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物件,通過它就可以得到一個檔案輸出流。需要注意的是前面在DiskLruCache的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的快取查詢
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;上面介紹了DiskLruCache的建立、快取的新增過和查詢過程,讀者應該對DiskLruCache的使用方式有了一個大致的瞭解,除此之外,DiskLruCache還提供了remove、delete等方法用於磁碟快取的刪除操作。關於DiskLruCache的內部實現這裡就不再介紹了,感興趣的朋友可以檢視它的原始碼實現。
2.3 ImageLoader的實現
在本章的前面先後介紹了Bitmap的高效載入方式、LruCache以及DiskLruCache,現在我們來著手實現一個優秀的ImageLoader。 一般來說,一個優秀的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.圖片的壓縮功能的實現
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.記憶體快取和磁碟快取的實現
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){ @Override protected 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() { @Override public 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); @Override public 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()){ @Override public 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); @Override public 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()){ @Override public 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){ @Override protected 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() { @Override public 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"> <GridView android: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的佈局檔案 <LinearLayout