ImageLoader的實現

一般來說,一個優秀的ImageLoader應該具備如下功能:

  • 圖片的同步加載
  • 圖片的異步加載
  • 圖片壓縮
  • 內存緩存
  • 磁盤緩存
  • 網絡拉取
ImageLoader的完整代碼如下:
package com.example.imageloader;

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;

import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.StatFs;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;

public class ImageLoader {
	private static final String TAG = ImageLoader.class.getSimpleName();
	public 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 MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
	private static final long KEEP_ALIVE = 10L;

	private static final int TAG_KEY_URI = R.id.imageloder_uri;
	private static final int DISK_CACHE_SIZE = 1024 * 1024 * 100;
	private static final int IO_BUFFER_SIZE = 1024 * 8;
	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(Runnable r) {

			return new Thread(r, "imageloder#" + mCount.getAndIncrement());
		}
	};

	/**
	 * 自定義線程池
	 */
	public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
			KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(), sThreadFactory);

	private Handler mMainHandler = new Handler() {
		public void handleMessage(android.os.Message msg) {
			LoaderResult result = (LoaderResult) msg.obj;
			ImageView imageView = result.imageView;
			String uri = (String) imageView.getTag(TAG_KEY_URI);
			if (uri.equals(result.uri)) {
				imageView.setImageBitmap(result.bitmap);
			} else {
				Log.w(TAG, "see image bitmap ,but rul has changed,ignored!");
			}
		};
	};

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

	private 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 value) {
				// 計算緩存大小
				return value.getRowBytes() * value.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);
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * 創建ImageLoader單例
	 * 
	 * @param context
	 * @return
	 */
	public static ImageLoader build(Context context) {
		return new ImageLoader(context);
	}

	/**
	 * 添加到內存
	 * 
	 * @param key
	 * @param bitmap
	 */
	private void addBitmapToMemoryCache(String key, Bitmap bitmap) {
		if (getBitmapFromMemoryCache(key) == null) {
			mMemoryCache.put(key, bitmap);
		}
	}

	/**
	 * 從內存中取出
	 * 
	 * @param key
	 * @return
	 */
	private Bitmap getBitmapFromMemoryCache(String key) {
		return mMemoryCache.get(key);
	}

	/**
	 * 異步加載和顯示圖片
	 * 
	 * @param uri
	 * @param imageView
	 */
	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);
		Bitmap bitmap = loadBitmapFromMemCache(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);
	}

	protected Bitmap loadBitmap(String uri, int reqWidth, int reqHeight) {
		Bitmap bitmap = loadBitmapFromMemCache(uri);
		if (bitmap != null) {
			Log.d(TAG, "loadBitmapFromMemCache,url:" + uri);
			return bitmap;
		}
		try {
			bitmap = loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
			if (bitmap != null) {
				Log.d(TAG, "loadBitmapFromDiskCache,url:" + uri);
				return bitmap;
			}

			bitmap = loadBitmapFromHttp(uri, reqWidth, reqHeight);
			Log.d(TAG, "loadBitmapFromHttp,url:" + uri);
		} catch (IOException e) {
			e.printStackTrace();
		}
		if (bitmap == null && !mIsDiskLruCacheCreated) {
			Log.w(TAG, "encounter error,DiskChache is not created.");
			bitmap = downloadBitmapFromUrl(uri);
		}
		return bitmap;
	}

	/**
	 * 從網絡下載圖片不緩存
	 * 
	 * @param uri
	 * @return
	 */
	private Bitmap downloadBitmapFromUrl(String uri) {
		Bitmap bitmap = null;
		HttpURLConnection urlConnection = null;
		BufferedInputStream in = null;

		try {
			URL url = new URL(uri);
			urlConnection = (HttpURLConnection) url.openConnection();
			in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
			bitmap = BitmapFactory.decodeStream(in);

		} catch (IOException e) {
			Log.e(TAG, "Error in downloadBitmap:" + e);
		} finally {
			if (urlConnection != null) {
				urlConnection.disconnect();
			}

			MyUtils.close(in);
		}

		return bitmap;
	}

	/**
	 * 從網絡下載圖片並緩存
	 * 
	 * @param uri
	 * @param reqWidth
	 * @param reqHeight
	 * @return
	 * @throws IOException
	 */
	private Bitmap loadBitmapFromHttp(String uri, int reqWidth, int reqHeight) 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(uri);
		DiskLruCache.Editor editor = mDiskLruCache.edit(key);
		if (editor != null) {
			OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
			if (downloadUrlToStream(uri, outputStream)) {
				editor.commit();
			} else {
				editor.abort();
			}
			mDiskLruCache.flush();
		}

		return loadBitmapFromDiskCache(uri, reqWidth, reqHeight);
	}

	private boolean downloadUrlToStream(String uri, OutputStream outputStream) {
		HttpURLConnection urlConnection = null;
		BufferedOutputStream out = null;
		BufferedInputStream in = null;

		try {
			final URL url = new URL(uri);
			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.e(TAG, "downloadBitmap failed." + e);
		} finally {
			if (urlConnection != null) {
				urlConnection.disconnect();
			}
			MyUtils.close(out);
			MyUtils.close(in);
		}

		return false;
	}

	/**
	 * 從磁盤獲取緩存並壓縮存入內存當中
	 * 
	 * @param uri
	 * @param reqWidth
	 * @param reqHeight
	 * @return
	 * @throws IOException
	 */
	private Bitmap loadBitmapFromDiskCache(String uri, int reqWidth, int reqHeight) throws IOException {
		if (Looper.myLooper() == Looper.getMainLooper()) {
			Log.w(TAG, "loadBitmap from UI Thread,it's not recommended!");
		}
		if (mDiskLruCache == null) {
			return null;
		}

		Bitmap bitmap = null;
		String key = hashKeyFromUrl(uri);
		DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
		if (snapshot != null) {
			FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
			FileDescriptor fileDescriptor = fileInputStream.getFD();
			bitmap = mImageResizer.decodeSampledBitmapFromDescriptor(fileDescriptor, reqWidth, reqHeight);
			if (bitmap != null) {
				addBitmapToMemoryCache(key, bitmap);
			}
		}

		return bitmap;
	}

	/**
	 * 向內存中添加
	 * 
	 * @param url
	 * @return
	 */
	private Bitmap loadBitmapFromMemCache(String url) {
		final String key = hashKeyFromUrl(url);
		Bitmap bitmap = getBitmapFromMemoryCache(key);
		return bitmap;
	}

	/**
	 * 對url進行md5加密
	 * 
	 * @param url
	 * @return
	 */
	private String hashKeyFromUrl(String url) {
		String cacheKey;
		try {
			final MessageDigest mDigest = MessageDigest.getInstance("MD5");
			mDigest.update(url.getBytes());
			cacheKey = bytesToHexString(mDigest.digest());

		} catch (NoSuchAlgorithmException e) {
			cacheKey = String.valueOf(url.hashCode());
		}
		return cacheKey;
	}

	/**
	 * 字節數組轉化爲16進制的字符串
	 * 
	 * @param bytes
	 * @return
	 */
	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();
	}

	/**
	 * 獲取設備的緩存路徑
	 * 
	 * @param context
	 * @param uniqueName
	 * @return
	 */
	public File getDiskCacheDir(Context context, String uniqueName) {
		boolean externalStorageAvaliable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
		final String cachePath;
		if (externalStorageAvaliable) {
			cachePath = context.getExternalCacheDir().getPath();
		} else {
			cachePath = context.getCacheDir().getPath();
		}

		return new File(cachePath + File.separator + uniqueName);
	}

	/**
	 * sd卡剩餘空間
	 * 
	 * @param path
	 * @return
	 */
	@TargetApi(VERSION_CODES.GINGERBREAD)
	private long getUsableSpace(File path) {
		if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD) {
			return path.getUsableSpace();
		}
		final StatFs stats = new StatFs(path.getPath());
		return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
	}

	/**
	 * bitmap包裝類
	 * 
	 * @author yl
	 *
	 */
	private static class LoaderResult {
		private ImageView imageView;
		private String uri;
		private Bitmap bitmap;

		public LoaderResult(ImageView imageView, String uri, Bitmap bitmap) {
			super();
			this.imageView = imageView;
			this.uri = uri;
			this.bitmap = bitmap;
		}

	}
}
ImageResizer的代碼如下:
import java.io.FileDescriptor;

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

public class ImageResizer {

	/**
	 * 縮放資源id爲resId的圖片
	 * 
	 * @param res
	 * @param resId
	 *            資源id
	 * @param reqWidth
	 *            縮放後的寬度
	 * @param reqHeight
	 *            縮放後的高度
	 * @return
	 */
	public static Bitmap decodeSampledBitmapFromResource(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);
	}

	/**
	 * 對圖片文件進行縮放
	 * @param fd
	 * @param reqWidth
	 * @param reqHeight
	 * @return
	 */
	public static Bitmap decodeSampledBitmapFromDescriptor(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;

		try {
			return BitmapFactory.decodeFileDescriptor(fd, null, options);
		} catch (Exception e) {
			Log.e("ImageResizer", "decodeFileDescriptor error:"+e);
		}
		return null;
	}
	/**
	 * 對圖片文件進行縮放
	 * @param path
	 * @param reqWidth
	 * @param reqHeight
	 * @return
	 */
	public static Bitmap decodeSampledBitmapFromFile(String path, int reqWidth, int reqHeight) {
		BitmapFactory.Options options = new BitmapFactory.Options();
		options.inJustDecodeBounds = true;
		BitmapFactory.decodeFile(path, options);
		
		options.inSampleSize = calculateInSampleSizeFromFile(options, reqWidth, reqHeight);
		options.inJustDecodeBounds = false;
		
		return BitmapFactory.decodeFile(path, options);
	}

	/**
	 * 計算inSampleSize
	 * 
	 * @param options
	 * @param reqWidth
	 *            縮放後的寬度
	 * @param reqHeight
	 *            縮放後的高度
	 * @return
	 */
	private static int calculateInSampleSize(Options options, int reqWidth, int reqHeight) {
		final int height = options.outHeight;
		final int width = options.outWidth;
		int inSampleSize = 1;
		if (height > reqHeight || width > reqWidth) {
			final int halfHeight = height / 2;
			final int halfWidth = width / 2;
			// 計算inSampleSize直到縮放後的寬高都小於指定的寬高
			while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
				inSampleSize *= 2;
			}

		}

		System.out.println(inSampleSize);
		return inSampleSize;
	}

	/**
	 * 
	 * @param options
	 * @param reqWidth
	 * @param reqHeight
	 * @return
	 */
	private static int calculateInSampleSizeFromFile(Options options, int reqWidth, int reqHeight) {
		//獲取比例大小
		int wRatio = (int)Math.ceil(options.outWidth/reqWidth);
		int hRatio = (int)Math.ceil(options.outHeight/reqHeight);
		//如果超出指定大小,則縮小相應的比例
		if(wRatio > 1 && hRatio > 1){
			if(wRatio > hRatio){
				options.inSampleSize = wRatio;
			}else{
				options.inSampleSize = hRatio;
			}
		}
		System.out.println(options.inSampleSize);
		return options.inSampleSize;
	}
}
MyUtils的代碼如下:
import java.io.Closeable;
import java.io.IOException;

public class MyUtils {
	/**
	 * 關閉當前流
	 * 
	 * @param closeable
	 */
	public static void close(Closeable closeable) {// 只需要把你想要關閉的流傳入就可以關閉此流了
		if (null != closeable) {
			try {
				closeable.close();// 此接口只有一個關閉流的方法
			} catch (IOException e) {
				System.out.println("關閉流出錯了,錯誤信息---->" + e);
			}
		}
	}
}
DiskLruCache的源碼可在這裏下載
http://download.csdn.net/detail/yule12345/9819313

注意事項:
  • 這裏採用了線程池和Handler,沒有直接用AsycTask,是因爲AsyncTask在3.0以上版本默認是串行的執行任務,這顯然不能體現ImageLoader的併發特性。不過依然可以通過改造AsyncTask來達到併發效果。
  • 之所以對url進行MD5加密轉換,是因爲圖片的url中很可能有特殊字符,影響url在android中直接使用
  • 在給ImageView設置圖片之前可以檢查它的url有沒有發生改變,如果發生改變就不在給它設置圖片,這樣就解決了列表所謂的問題
  • 在列表處於滑動狀態時,修改布爾參數,在adpter的getview方法中根據改參數值來判斷是否停止加載圖片,這樣可以解決滑動帶來的卡頓問題


發佈了36 篇原創文章 · 獲贊 3 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章