第十二章-實現一個ImageLoader

一、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設置圖片,由於這個時候ImageViewA被itemB所複用,但是item B要顯示的圖片顯然不是item A剛剛下載好的圖片,這個時候就會出現item B中顯示了item A的圖片,這就是常見的列表錯位問題,ImageLoader需要正確地處理這些特殊情況。

二、圖片壓縮功能的實現
圖片壓縮在上一章節已經分析過了,爲了有良好的設計風格,這裏單獨抽象了一個類用於完成圖片的壓縮功能,這個類叫ImageResizer,實現如下。

package com.example.bitmap.imagerLoader;

import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Log;

import java.io.FileDescriptor;

/**
 * 圖片壓縮工具類
 */
public class ImageResizer {
	private static final String TAG = "ImageResizer";

	public ImageResizer() {
	}

	/**
	 * 從資源文件中加載壓縮後的Bitmap圖片
	 * @param res eg:getResources()
	 * @param resId eg:R.mipmap.ic_launcher
	 * @param reqWidth 目標寬度
	 * @param reqHeight 目標高度
	 * @return
	 */
	public static Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
		BitmapFactory.Options options = new BitmapFactory.Options();

		//設置只請求大小不加載標記
		options.inJustDecodeBounds = true;

		//獲取到圖片的寬高
		BitmapFactory.decodeResource(res, resId, options);

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

		//重置標記,加載圖片
		options.inJustDecodeBounds = false;
		return BitmapFactory.decodeResource(res, resId, options);
	}

	/** 通過FileDescriptor加載Bitmap圖片 */
	public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
		BitmapFactory.Options options = new BitmapFactory.Options();

		//設置只請求大小不加載標記
		options.inJustDecodeBounds = true;

		//獲取到圖片的寬高
		BitmapFactory.decodeFileDescriptor(fd,null,options);

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

		//重置標記,加載圖片
		options.inJustDecodeBounds = false;
		return BitmapFactory.decodeFileDescriptor(fd,null,options);
	}

	/**計算採樣率,邏輯就是採樣後的寬和高都大於請求的寬高,採樣率*2。直到滿足條件返回 */
	public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
		int width = options.outWidth;
		int height = options.outHeight;
		int inSampleSize = 1;

		if (height > reqHeight || width > reqWidth) {
			int halfHeight = height / 2;
			int halfWidth = width / 2;
			while (halfHeight / inSampleSize >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
				inSampleSize *= 2;
			}
		}
		Log.d(TAG,"採樣率 = " + inSampleSize);
		return inSampleSize;
	}

}

三、內存緩存和磁盤緩存的實現
這裏選擇LruCache和DiskLruCache來分別完成內存緩存和磁盤緩存的工作。在ImageLoader初始化時,會創建LruCache和DiskLruCache,如下所示。

private LruCache<String, Bitmap> mMemoryLruCache;
private DiskLruCache mDiskLruCache;
private Context mContext;
private long DISK_CACHE_SIZE = 1024 * 1024 *50;
private boolean mIsDiskLruCacheCreate;

public ImageLoader(Context context) {
	Log.d(TAG,"ImageLoader");
	mContext = context.getApplicationContext();
	int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//KB
	int cacheSize = maxMemory / 8;
	Log.d(TAG,"memory cacheSize = " + cacheSize);

	mMemoryLruCache = new LruCache<String, Bitmap>(cacheSize) {
		@Override
		protected int sizeOf(String key, Bitmap value) {
			return value.getRowBytes() * value.getHeight() / 1024;//KB
		}
	};

	File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
	if(!diskCacheDir.exists()){
		diskCacheDir.mkdirs();
	}

	try {
		mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
		mIsDiskLruCacheCreate = true;
	} catch (IOException e) {
		e.printStackTrace();
	}

}

private File getDiskCacheDir(Context context,String folderName){
	String cacheDirPath = context.getCacheDir().getAbsolutePath();
	File diskCacheDir = new File(cacheDirPath + File.separator + "bitmap");
	//File diskCacheDir = new File("/sdcard" + File.separator + "bitmap");//sdcard路徑
	return diskCacheDir;
}

在創建磁盤緩存時,這裏做了一個判斷,即有可能磁盤剩餘空間小於磁盤緩存所需要大小,一般是指用戶的手機空間已經不足了,因爲沒有辦法創建磁盤緩存,這個時候磁盤緩存就會失效,在上面的代碼中,ImageLoader的內存緩存的容量爲當前進程可用內存的1/8,磁盤緩存的容量爲50MB

內存緩存和磁盤緩存創建完畢後,還需要提供方法來完成緩存的添加和獲取功能。先看內存緩存,它的添加過程比較簡單,如下所示。

    /** 內存緩存的添加 */
    private void addBitmapToMemoryCache(String key,Bitmap bitmap){
        if(getBitmapFromMemoryCache(key) == null){
            mMemoryLruCache.put(key,bitmap);
        }
    }

    /** 內存緩存的獲取 */
    private Bitmap getBitmapFromMemoryCache(String key){
        return mMemoryLruCache.get(key);
    }

而磁盤緩存的添加和讀取功能稍微複雜一些,上一章節介紹過了,這裏再簡單說明一下,磁盤緩存的添加需要通過Editor來完成,Editor提供了commit和abort方法來提交和撤銷對文件系統的寫操作,具體實現請參考下面的loadBitmapFromHttp方法。磁盤緩存的讀取需要通過Snapshot來完成,通過SnapShot可以得到磁盤緩存對象對應的FileInputStream,但是FileInputStream無法便捷地進行壓縮,所以通過FileDescriptor來加載壓縮後的圖片,最後將加載後的Bitmap添加到內存中,具體實現參看下面的loadBitmapFromDiskCache方法。

    /** 從網絡加載Bitmap */
    private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(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,reqHeight);//獲取從網絡下載到disk的緩存圖片
    }
	
    /** 從磁盤上獲取緩存文件 */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if(snapshot != null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if(bitmap != null){
                addBitmapToMemoryCache(key,bitmap);
            }
        }
        return bitmap;
    }

四、同步加載和異步加載接口的設計
首先看同步加載,同步加載接口需要外部在線程中調用,這是因爲同步加載很可能比較耗時,實現如下。

    /** 同步加載 */
    private Bitmap loadBitmap(String url,int reqWidth,int reqHeight){
        /** 嘗試從內存中加載圖片 */
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if(bitmap != null){
            Log.d(TAG,"loadBitmapFromMemCache,url = " + url);
            return bitmap;
        }

        /** 嘗試從磁盤中加載圖片 */
        try {
            bitmap = loadBitmapFromDiskCache(url,reqWidth,reqHeight);
            if(bitmap != null){
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);//loadBitmap這個方法不能在UI線程中調用,這個裏面做了判斷
        } catch (IOException e) {
            e.printStackTrace();
        }

        /** 磁盤中寫不進去,讀取不到,從網絡下載圖片 */
        if(bitmap == null && !mIsDiskLruCacheCreate){
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }

從loadBitmap的實現可以看出,其工作過程遵循如下幾個步:首先嚐試從內存緩存中讀取圖片,接着嘗試從磁盤緩存中讀取圖片,最後纔會從網絡拉取圖片。另外,這個方法不能在主線程中調用,否則就拋出異常。這個檢查是在loadBitmapFromHttp中實現的:

	if(Looper.myLooper() == Looper.getMainLooper()){
		throw new RuntimeException("cat 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);
        Bitmap bitmap = getBitmapFromMemoryCache(uri);
        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap =  loadBitmap(uri,reqWidth,reqHeight);
                if(bitmap != null){
                    LoaderResult result = new LoaderResult(imageView,uri,bitmap);
                    mMainHandler.obtainMessage(MESSAGE_POST_RESULT,result).sendToTarget();
                }
            }
        };
        THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
    }

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

    /** 線程池初始化的一些參數 */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CODE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_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( Runnable runnable) {
            return new Thread(runnable,"ImageLoader#"+ mCount.getAndIncrement());
        }
    };
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CODE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(),
            sThreadFactory);

之所以採用線程池是有原因的,首先肯定不能採用普通的線程去做這個事。如果直接採用普通的線程去加載圖片,隨着列表的滑動這可能會產生大量的線程,這樣並不利於整體效率的提升。而且這個是高併發場景,所以使用線程池就合適。

下面看Handler的實現。ImageLoader直接採用主線程的Looper來構造Handler對象,這就使得ImageLoader可以在非主線程中構造了。另外爲了解決由於View的複用所導致的列表錯位這一問題,在給ImageView設置圖片之前都會檢查它的url有沒有發生改變,如果發生改變就不再給它設置圖片,這樣就解決了列表錯位問題。

    private Handler mMainHandler = new Handler(Looper.myLooper()){

        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            imageView.setImageBitmap(result.bitmap);
            /** 拿到imageView的tag對應的uri,如果和這個result中的uri相等,表示這個圖片就是這個imageView需要的,
             可能出現網絡下載慢,用戶滑動後,其它的imageView複用了這個。使用tag很好的解決了列表錯位的問題。
             */
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if(uri.equals(result.uri)){
                imageView.setImageBitmap(result.bitmap);
            }
        }
    };

到此爲止,ImageLoader的細節已經做了全部的分析。

五、ImageLoader的完整代碼
以下代碼在真機上實測通過

package com.example.bitmap.imagerLoader;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

import com.example.bitmap.disklrucache.DiskLruCache;
import com.example.test.R;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ImageLoader {
    private static final String TAG = "ImageLoader";

    public static final int MESSAGE_POST_RESULT = 1;

    /** 線程池初始化的一些參數 */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CODE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final long KEEP_ALIVE = 10L;

    private static final int TAG_KEY_URI = R.id.imageloader_uri;//這個id必須是應用唯一的id,可以在values文件夾中創建一個ids.xml文件定義
    private static final long DISK_CACHE_SIZE = 1024 * 1024 *50;
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    private static final int DISK_CACHE_INDEX = 0;
    private boolean mIsDiskLruCacheCreate = false;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);
        @Override
        public Thread newThread( Runnable runnable) {
            return new Thread(runnable,"ImageLoader#"+ mCount.getAndIncrement());
        }
    };
    public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
            CODE_POOL_SIZE,
            MAXIMUM_POOL_SIZE,
            KEEP_ALIVE,
            TimeUnit.SECONDS,
            new LinkedBlockingDeque<Runnable>(),
            sThreadFactory);

    private Handler mMainHandler = new Handler(Looper.myLooper()){

        @Override
        public void handleMessage(Message msg) {
            LoaderResult result = (LoaderResult) msg.obj;
            ImageView imageView = result.imageView;
            imageView.setImageBitmap(result.bitmap);
            /** 拿到imageView的tag對應的uri,如果和這個result中的uri相等,表示這個圖片就是這個imageView需要的,
             可能出現網絡下載慢,用戶滑動後,其它的imageView複用了這個。使用tag很好的解決了列表錯位的問題。
             */
            String uri = (String) imageView.getTag(TAG_KEY_URI);
            if(uri.equals(result.uri)){
                imageView.setImageBitmap(result.bitmap);
            }
        }
    };

    private Context mContext;
    private ImageResizer mImageResizer = new ImageResizer();
    private LruCache<String, Bitmap> mMemoryLruCache;
    private DiskLruCache mDiskLruCache;


    private ImageLoader(Context context) {
        Log.d(TAG,"ImageLoader");
        mContext = context.getApplicationContext();
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);//KB
        int cacheSize = maxMemory / 8;
        Log.d(TAG,"memory cacheSize = " + cacheSize);

        mMemoryLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight() / 1024;//KB
            }
        };

        File diskCacheDir = getDiskCacheDir(mContext,"bitmap");
        if(!diskCacheDir.exists()){
            diskCacheDir.mkdirs();
        }

        try {
            mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
            mIsDiskLruCacheCreate = true;
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    public static ImageLoader build(Context context){
        return new ImageLoader(context);
    }


    /** 內存緩存的添加 */
    private void addBitmapToMemoryCache(String key,Bitmap bitmap){
        if(getBitmapFromMemoryCache(key) == null){
            mMemoryLruCache.put(key,bitmap);
        }
    }
    /** 內存緩存的獲取 */
    public Bitmap getBitmapFromMemoryCache(String key){
        return mMemoryLruCache.get(key);
    }

    public void bindBitmap(final String uri, final ImageView imageView){
        bindBitmap(uri,imageView,0,0);
    }

    /** 異步加載 */
    private void bindBitmap(final String uri, final ImageView imageView, final int reqWidth, final int reqHeight){
        imageView.setTag(TAG_KEY_URI,uri);
        Bitmap bitmap = getBitmapFromMemoryCache(uri);
        if(bitmap != null){
            imageView.setImageBitmap(bitmap);
            return;
        }

        Runnable loadBitmapTask = new Runnable() {
            @Override
            public void run() {
                Bitmap bitmap =  loadBitmap(uri,reqWidth,reqHeight);
                if(bitmap != 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 reqHeight){
        /** 嘗試從內存中加載圖片 */
        Bitmap bitmap = loadBitmapFromMemCache(url);
        if(bitmap != null){
            Log.d(TAG,"loadBitmapFromMemCache,url = " + url);
            return bitmap;
        }

        /** 嘗試從磁盤中加載圖片 */
        try {
            bitmap = loadBitmapFromDiskCache(url,reqWidth,reqHeight);
            if(bitmap != null){
                return bitmap;
            }
            bitmap = loadBitmapFromHttp(url,reqWidth,reqHeight);//loadBitmap這個方法不能在UI線程中調用,這個裏面做了判斷
        } catch (IOException e) {
            e.printStackTrace();
        }

        /** 磁盤中寫不進去,讀取不到,從網絡下載圖片 */
        if(bitmap == null && !mIsDiskLruCacheCreate){
            bitmap = downloadBitmapFromUrl(url);
        }
        return bitmap;
    }


    private Bitmap loadBitmapFromMemCache(String url) {
        final String key = hashKeyFormUrl(url);
        Bitmap bitmap = getBitmapFromMemoryCache(key);
        return bitmap;
    }

    /** 從網絡加載Bitmap */
    private Bitmap loadBitmapFromHttp(String url,int reqWidth,int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        String key = hashKeyFormUrl(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,reqHeight);//獲取從網絡下載到disk的緩存圖片
    }


    /** 從磁盤上獲取緩存文件 */
    private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException {
        if(Looper.myLooper() == Looper.getMainLooper()){
            throw new RuntimeException("cat not visit network from UI thread");
        }
        if(mDiskLruCache == null){
            return null;
        }
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
        if(snapshot != null){
            FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor,reqWidth,reqHeight);
            if(bitmap != null){
                addBitmapToMemoryCache(key,bitmap);
            }
        }
        return bitmap;
    }


    /** 網絡下載圖片並且寫入到磁盤 */
    private boolean downloadUrlToStream(String urlString,OutputStream outputStream){
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
            int b ;
            while ((b = in.read()) != -1){
                out.write(b);
            }
            return true;
        } catch (IOException e) {
            Log.d(TAG,"下載網絡圖片失敗");
        }finally {
            if(urlConnection != null){
                urlConnection.disconnect();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

    /** 網絡下載圖片,返回bitmap對象 */
    private Bitmap downloadBitmapFromUrl(String urlString){
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        try {
            URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
        } catch (IOException e) {
            Log.d(TAG,"下載網絡圖片失敗");
        }finally {
            if(urlConnection != null){
                urlConnection.disconnect();
            }
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return bitmap;
    }


    private String hashKeyFormUrl(String url){
        String cacheKey;
        try {
            MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(url.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] digest) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < digest.length; i++) {
            String hex = Integer.toHexString(0xFF&digest[i]);
            if(hex.length() == 1){
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

    private File getDiskCacheDir(Context context,String folderName){
        String cacheDirPath = context.getCacheDir().getAbsolutePath();
        File diskCacheDir = new File(cacheDirPath + File.separator + "bitmap");
//        File diskCacheDir = new File("/sdcard" + File.separator + "bitmap");//sdcard路徑
        return diskCacheDir;
    }


    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;
        }
    }

}

特別注意:private static final int TAG_KEY_URI = R.id.imageloader_uri;
這個id是在res/values/ids.xml中定義的

	<?xml version="1.0" encoding="utf-8"?>
	<resources>
		<item name="imageloader_uri" type="id"/>
	</resources>

六、ImageLoader的使用
測試的activity (TestBitmapActivity.java)

package com.example.bitmap;

import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.ImageView;

import com.example.bitmap.imagerLoader.ImageLoader;
import com.example.bitmap.imagerLoader.MyUtils;
import com.example.test.R;

import java.util.ArrayList;

public class TestBitmapActivity extends AppCompatActivity implements AbsListView.OnScrollListener {
    private static final String TAG = "G_TestBitmapActivity";

    private ArrayList<String> mUrList = new ArrayList<String>();
    private ImageLoader mImageLoader;
    private GridView mImageGridView;
    private BaseAdapter mImageAdapter;

    private boolean mIsGridViewIdle = true;
    private int mImageWidth = 0;
    private boolean mIsWifi = false;
    private boolean mCanGetBitmapFromNetWork = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test_bitmap);

        initData();
        initView();
        mImageLoader = ImageLoader.build(this);//初始化一個ImageLoader
    }

    String[] imageUrls = {
            "http://img3.imgtn.bdimg.com/it/u=3719870775,3435259810&fm=26&gp=0.jpg",
            "http://img5.imgtn.bdimg.com/it/u=1177374310,3414687935&fm=26&gp=0.jpg",
            "http://img0.imgtn.bdimg.com/it/u=1737410255,2903983043&fm=26&gp=0.jpg",
            "http://img3.imgtn.bdimg.com/it/u=101044436,1645701061&fm=26&gp=0.jpg",
            "http://img2.imgtn.bdimg.com/it/u=3195176921,3515190403&fm=26&gp=0.jpg",
            "http://img5.imgtn.bdimg.com/it/u=1665477401,3216469060&fm=26&gp=0.jpg"
    };
    private void initData() {
        for (String url : imageUrls) {
            mUrList.add(url);
        }
        int screenWidth = MyUtils.getScreenMetrics(this).widthPixels;
        int space = (int)MyUtils.dp2px(this, 20f);
        mImageWidth = (screenWidth - space) / 3;//計算image的寬度
        mIsWifi = MyUtils.isWifi(this);
        if (mIsWifi) {
            mCanGetBitmapFromNetWork = true;
        }
    }

    private void initView() {
        mImageGridView = findViewById(R.id.gv);
        mImageAdapter = new ImageAdapter(this);
        mImageGridView.setAdapter(mImageAdapter);
        mImageGridView.setOnScrollListener(this);

        if (!mIsWifi) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setMessage("初次使用會從網絡下載大概5MB的圖片,確認要下載嗎?");
            builder.setTitle("注意");
            builder.setPositiveButton("是", new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialog, int which) {
                    mCanGetBitmapFromNetWork = true;
                    mImageAdapter.notifyDataSetChanged();
                }
            });
            builder.setNegativeButton("否", null);
            builder.show();
        }
    }

    public void eventLoadMore(View view) {
        for (String url : imageUrls) {
            mUrList.add(url);
        }
        mImageAdapter.notifyDataSetChanged();
    }


    private class ImageAdapter extends BaseAdapter {
        private LayoutInflater mInflater;
        private Drawable mDefaultBitmapDrawable;

        private ImageAdapter(Context context) {
            mInflater = LayoutInflater.from(context);
            mDefaultBitmapDrawable = context.getResources().getDrawable(R.mipmap.ic_launcher);
        }

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

        @Override
        public String 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;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.image_list_item,parent, false);
                holder = new ViewHolder();
                holder.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 uri = getItem(position);
            if (!uri.equals(tag)) {
                imageView.setImageDrawable(mDefaultBitmapDrawable);
            }

            /** mIsGridViewIdle 沒有在滑動,並且允許下載就開始異步加載bitmap圖片
               看到京東客戶端在滑動過程中也會加載,淘寶客戶端滑動中就不會。是否需要mIsGridViewIdle這個標記看業務和性能之間的取捨 */
            if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
                imageView.setTag(uri);
                mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);//這個大小主要控制壓縮圖片的質量
            }
            return convertView;
        }

    }

    private static class ViewHolder {
        public ImageView imageView;
    }

    /** 判斷是否在滑動,如果在滑動就設置標記位,停止加載*/
    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {
        if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
            mIsGridViewIdle = true;
            mImageAdapter.notifyDataSetChanged();
        } else {
            mIsGridViewIdle = false;
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem,int visibleItemCount, int totalItemCount) {
    }

}

activity佈局文件(activity_test_bitmap.xml)

<?xml version="1.0" encoding="utf-8"?>
<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="com.example.bitmap.TestBitmapActivity"
    android:orientation="vertical"
    >

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="加載更多"
        android:onClick="eventLoadMore"
        />

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="5dp" >

        <GridView
            android:id="@+id/gv"
            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" >
        </GridView>
    </LinearLayout>

</LinearLayout>

工具類MyUtils.java

package com.example.bitmap.imagerLoader;

import android.app.ActivityManager;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.view.WindowManager;

import java.io.Closeable;
import java.io.IOException;
import java.util.List;

public class MyUtils {
    public static String getProcessName(Context cxt, int pid) {
        ActivityManager am = (ActivityManager) cxt
                .getSystemService(Context.ACTIVITY_SERVICE);
        List<ActivityManager.RunningAppProcessInfo> runningApps = am.getRunningAppProcesses();
        if (runningApps == null) {
            return null;
        }
        for (ActivityManager.RunningAppProcessInfo procInfo : runningApps) {
            if (procInfo.pid == pid) {
                return procInfo.processName;
            }
        }
        return null;
    }

    public static void close(Closeable closeable) {
        try {
            if (closeable != null) {
                closeable.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static DisplayMetrics getScreenMetrics(Context context) {
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        return dm;
    }

    public static float dp2px(Context context, float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp,
                context.getResources().getDisplayMetrics());
    }

    public static boolean isWifi(Context context) {
        ConnectivityManager connectivityManager = (ConnectivityManager) context
                .getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo activeNetInfo = connectivityManager.getActiveNetworkInfo();
        if (activeNetInfo != null
                && activeNetInfo.getType() == ConnectivityManager.TYPE_WIFI) {
            return true;
        }
        return false;
    }

    public static void executeInThread(Runnable runnable) {
        new Thread(runnable).start();
    }

}

自定義ImageView,用於設置相等寬高,如下所示。

package com.example.bitmap.imagerLoader;

import android.content.Context;
import android.support.v7.widget.AppCompatImageView;
import android.util.AttributeSet;

public class SquareImageView extends AppCompatImageView {

    public SquareImageView(Context context) {
        super(context);
    }

    public SquareImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public SquareImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, widthMeasureSpec);//相等寬高
    }
}

ImageAdapter中的item佈局(image_list_item),如下所示。

<?xml version="1.0" encoding="utf-8"?>
<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" >

    <com.example.bitmap.imagerLoader.SquareImageView
        android:id="@+id/image"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:scaleType="centerCrop"
        android:src="@mipmap/ic_launcher" />

</LinearLayout>

AndroidManifest.xml配置的activity,開啓硬件加速

	<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.INTERNET"/>
    
	<activity android:name="com.example.bitmap.TestBitmapActivity"
		android:hardwareAccelerated="true">
	</activity>

上面的代碼解決了imageView複用導致的列表錯位問題。滑動過程中不加載,避免出現大量的線程佔用資源(看業務取捨,如果用戶非常快的滑動,就沒必要從網絡中加載)。

很使用的工具類封裝,可以做成和淘寶、京東一樣的刷新效果。

運行的效果圖如下所示。
在這裏插入圖片描述

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