一般來說,一個優秀的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;
}
}
}
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;
}
}
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方法中根據改參數值來判斷是否停止加載圖片,這樣可以解決滑動帶來的卡頓問題