Bitmap的加載和Cache

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

因此如何高效的加載Bitmap是一個很重要也很容易被開發者或忽視的問題。
接着介紹Android中常用的緩存策略,緩存策略是一種通用的思想,可以用在很多場景中,但是實際開發中經常需要用Bitmap緩存。通過緩存策略,我們不需要每次都從網絡上請求圖片或者中設備中加載圖片,這樣就極大地提高了圖片加載效率以及產品的用戶體驗。目前比較常用的緩存策略是LruCache和DiskLruCache,其中LruCache常用用作內存緩存,而DiskLruCache常用用作存儲緩存。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){
            @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的創建
DiskLruCache並不能通過構造方法來創建,它提供了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即可。當版本號發送改變時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的緩存添加
DiskLruCache的緩存添加的操作是通過Editor完成的,Editor表示一個緩存對象的編輯對象。這裏仍然以圖片緩存舉例,首先火藥獲取url所對應的key,然後更具key就可以通過edit()來獲取Editor對象,如果這個緩存正在被編輯,那麼edit()會返回null,即DiskLruCache不允許同時編輯一個緩存對象。之所以要把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對象,通過它就可以得到一個文件輸出流。需要注意的是前面在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的緩存查找
和緩存添加過程類似,緩存查找也需要將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;

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

2.3 ImageLoader的實現

在本章的前面先後介紹了Bitmap的高效加載方式、LruCache以及DiskLruCache,現在我們來着手實現一個優秀的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和DiskLruCache來分貝完成內存緩存和磁盤緩存的工作。在ImageLoader初始化時,會創建LruCache和DiskLruCache,如下所示。
    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
    xmlns: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.SquareImageView
        android: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);
    }

    @Override
    protected 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 {
		...

    @Override
    public int getCount() {
        return mUrList.size();
    }

    @Override
    public Object getItem(int position) {
        return mUrList.get(position);
    }

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public 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開啓硬件加速。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章