在上一篇我们已经先后介绍了 Bitmap 的高效加载方式、LruCache 以及 DiskLruCache,那么我们就动手来写一个简化版的 ImageLoader 吧!!!
一般来说,一个优秀的 ImageLoader 应该具备如下功能:
声明一下,这里实现的 ImageLoader 并不是为了写一个框架,而是纯粹的加深下三级缓存以及图片的高效加载而写的一个小Demo。
图片压缩在上一篇博客中已经做了介绍,这里就不在废话了,为了有良好的设计风格,这里单独抽象了一个类似于完成图片的压缩功能,这个类叫 ImageResizer,它的实现如下:
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
/**
* 根据指定的资源文件以及指定的宽/高进行等比例缩放
* @param res
* @param resId
* @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 fileDescriptor
* @param reqWidth
* @param reqHeight
* @return
*
*/
public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fileDescriptor, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
Log.e("00000000", reqWidth + "=====" + reqHeight + "======" + options.inSampleSize);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
}
/**
* 根据指定的宽/高进行 2 的指数缩放
*
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (reqHeight > 0 || reqHeight > 0) {
if (width > reqHeight && height > reqHeight) {
final int halfWidth = width / 2;
final int halfHeight = height / 2;
while (halfWidth / inSampleSize >= reqWidth
&& halfHeight / inSampleSize >= reqHeight) {
inSampleSize *= 2;
}
}
}
return inSampleSize;
}
}
public class MemoryCache {
private static MemoryCache instance;
private LruCache mMemoryCache;
private MemoryCache() {
//获取当前进程的可用内存
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
//完成bitmap对象大小的计算
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
}
public static MemoryCache getInstance() {
if (instance == null) {
synchronized (MemoryCache.class) {
if (instance == null) {
instance = new MemoryCache();
}
}
}
return instance;
}
public void addBitmapToMemoryCache(String uri, Bitmap bitmap) {
String key = MD5.hashKeyFormUrl(uri);
mMemoryCache.put(key, bitmap);
}
public Bitmap getBitmapFromMemoryCache(String key) {
return mMemoryCache.get(key);
}
}
没什么好说的,略过…
public class DiskCache {
private static DiskCache instance;
private final long DISK_CACHE_SIZE = 1024 * 1024 * 50; //可缓存的大小
private DiskLruCache mDiskLruCache;
public static final int DISK_CACHE_INDEX = 0;
private final int IO_BUFFER_SIZE = 8 * 1024; //缓冲流的大小
private DiskCache(Context context) {
File diskCacheDir = FilePath.getDiskCacheDir(context, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
//判断当前sdk缓存目录可用大小是否满足我们设置的缓存大小
if (FilePath.getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static DiskCache getInstance(Context context) {
if (instance == null) {
synchronized (DiskCache.class) {
if (instance == null) {
instance = new DiskCache(context);
}
}
}
return instance;
}
public Bitmap get(String uri) throws IOException {
FileDescriptor fileDescriptor = getFileDescriptor(uri);
if (fileDescriptor != null) {
return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, 0, 0);
} else {
return null;
}
}
public Bitmap get(String uri, int reqWidth, int reqHeight) throws IOException {
FileDescriptor fileDescriptor = getFileDescriptor(uri);
if (fileDescriptor != null) {
return ImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
} else {
return null;
}
}
public void edit(String uri) throws IOException {
String key = MD5.hashKeyFormUrl(uri);
DiskLruCache.Editor editor = null;
//mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null
if (mDiskLruCache != null) {
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();
}
}
public boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.connect();
InputStream inputStream = urlConnection.getInputStream();
in = new BufferedInputStream(inputStream, IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
int b = 0;
while ((b = in.read()) != -1) {
out.write(b);
}
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
private FileDescriptor getFileDescriptor(String uri) throws IOException {
//mDiskLruCache在sdk缓存空间小于 DISK_CACHE_INDEX 可能为null
if (mDiskLruCache != null) {
String key = MD5.hashKeyFormUrl(uri);
Bitmap bitmap = null;
FileDescriptor fileDescriptor = null;
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DiskCache.DISK_CACHE_INDEX);
fileDescriptor = fileInputStream.getFD();
}
return fileDescriptor;
} else {
return null;
}
}
}
在创建磁盘缓存时,这里做了一个判断,即有可能磁盘剩余空间小于磁盘缓存所需要的大小,一般是指用户的手机空间不足了,因此没有办法创建磁盘缓存,这个时候磁盘缓存就会失效。
磁盘缓存的添加以及读取功能稍微复杂一些,这里在简单说明一下。磁盘缓存的添加需要通过 Editor 来完成,Editor 提供了 commit 和 abort 方法来提交和撤销对文件系统的写操作,具体实现请看 DiskCache 的 edit 方法。磁盘缓存的读取需要通过 Snapshot 来完成,通过 Snapshot 可以得到磁盘缓存对象对应的 FileInputStream,但是 FileInputStream 无法便捷地进行压缩,所以通过 FileDescriptor 来加载压缩后的图片,最后将加载后的 bitmap 添加到内存缓存中,具体实现请参看 DiskCache 的 get 方法
public Bitmap loadBitmap(String uri, ImageView imageView) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromDiskCache(uri, imageView);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, imageView);
if (bitmap != null) {
return bitmap;
}
return downloadBitmapFromUrl(uri);
}
从 loadBitmap 的实现可以看出,其工作过程遵循如下几步:首先尝试从内存缓存中读取图片,接着尝试冲磁盘缓存中读取图片,最后才从网络中拉取图片。另外这个方法不能在主线程中调用,否则就抛出异常。这个执行环境的检测是在各种缓存获取方法中实现的,通过检测当前线程的 Looper 是否为主线程的 Looper 来判断当前线程是否是主线程,如果是主线程就直接抛出异常终止程序。如下所示:
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network form UI Thread.");
}
public void bindBitmap(final String uri, final ImageView imageView) {
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, imageView);
if (bitmap != null) {
LoaderResult loaderResult = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, loaderResult).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
从 bindBitmap 的实现来看,bindBitmap 方法会尝试从内存缓存中获取图片,如果获取成功就直接返回结果,否则会在线程池中去调用 loadBitmap 方法,当图片加载成功后再将图片、图片的地址以及需要绑定的 ImageView 封装成一个 LoaderResult 对象,然后再通过 mMainHandler 向主线程发送一个消息,这样就可以在主线程中给 ImageView 设置图片了,之所以通过 Handler 来中转是因为子线程无法访问 UI。
bindBitm 中用到了线程池和 Handler,这里看一下它们的实现,首先看线程池 THREAD_POOL_EXECUTOR 的实现,如下所示。可以看出它的核心线程数为当前设备的 CPU 核心数 +1,最大容量为 CPU 核心数的 2 倍 +1,线程闲置超时时长为 10 秒,关于线程池的详细介绍我们在 Android——线程和线程池 已经详细介绍过。
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 int KEEP_ALIVE = 10; //存活时间
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue sPoolWorkQueue =
new LinkedBlockingQueue(); //等待队列
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
之所以采用线程池是有原因的,首先肯定不能采用普通的线程去做这个事,线程池的好处上篇博客已经说明。如果直接采用普通的线程去加载图片,随着列表的滑动这可能会产生大量的线程,这样并不利于整体效率的提升。另外一点,这里也没有选择采用 AsyncTask,AsyncTask 封装了线程池和 Handler,按道理它应该适合 ImageLoader 的场景。从 Android——线程和线程池 我们对 AsyncTask 的分析可以知道,AsyncTask 在 3.0 的低版本和高版本上具有不同的表现,在 3.0 以上的版本 AsyncTask 无法实现并发的效果,这显然是不能接受的,因为 ImageLoader 就是需要并发特性,虽然可以通过改造 AsyncTask 或者使用 AsyncTask 的 executeOnExecutor 方法的形式来执行异步任务,但是这总归是不太自然的实现f方式。鉴于以上两点原因,这里选择线程池和 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(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed, ignored!");
}
}
};
到此为止,ImageLoader 的细节都已经做了全面的分析,下面是 ImageLoader的完整代码:
public class ImageLoader {
private static ImageLoader instance;
private Context mContext;
private final int TAG_KEY_URI = R.id.imageloader_uri;
private final String TAG = "ImageLoader";
private 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 int KEEP_ALIVE = 10; //存活时间
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
private static final BlockingQueue sPoolWorkQueue =
new LinkedBlockingQueue(); //等待队列
public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE,
KEEP_ALIVE,
TimeUnit.SECONDS,
sPoolWorkQueue,
sThreadFactory);
//更新 ImageView
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(TAG_KEY_URI);
if (uri.equals(result.uri)) {
imageView.setImageBitmap(result.bitmap);
} else {
Log.w(TAG, "set image bitmap,but url has changed, ignored!");
}
}
};
private ImageLoader() {
}
public static ImageLoader getInstance() {
if (instance == null) {
synchronized (ImageLoader.class) {
if (instance == null) {
instance = new ImageLoader();
}
}
}
return instance;
}
public void init(Context context) {
this.mContext = context;
}
/**
* 异步加载
* @param uri
* @param imageView
*/
public void bindBitmap(final String uri, final ImageView imageView) {
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, imageView);
if (bitmap != null) {
LoaderResult loaderResult = new LoaderResult(imageView, uri, bitmap);
mMainHandler.obtainMessage(MESSAGE_POST_RESULT, loaderResult).sendToTarget();
}
}
};
THREAD_POOL_EXECUTOR.execute(loadBitmapTask);
}
/**
* 同步加载
* @param uri
* @param imageView
* @return
*/
public Bitmap loadBitmap(String uri, ImageView imageView) {
Bitmap bitmap = loadBitmapFromMemCache(uri);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromDiskCache(uri, imageView);
if (bitmap != null) {
return bitmap;
}
bitmap = loadBitmapFromHttp(uri, imageView);
if (bitmap != null) {
return bitmap;
}
return downloadBitmapFromUrl(uri);
}
/**
* 从网络获取bitmap
* @param urlString
* @return
*/
private Bitmap downloadBitmapFromUrl(String urlString) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network form UI Thread.");
}
return DownLoad.downLoadBitmapFromUrl(urlString);
}
/**
* 从网络获取bitmap,并添加到磁盘缓存
* @param uri
* @param imageView
* @return
*/
private Bitmap loadBitmapFromHttp(String uri, ImageView imageView) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network form UI Thread.");
}
try {
DiskCache.getInstance(mContext).edit(uri);
return loadBitmapFromDiskCache(uri, imageView);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 从磁盘缓存中获取bitmap
* @param uri
* @param imageView
* @return
*/
private Bitmap loadBitmapFromDiskCache(String uri, ImageView imageView) {
if (Looper.myLooper() == Looper.getMainLooper()) {
throw new RuntimeException("can not visit network form UI Thread.");
}
Bitmap bitmap = null;
try {
bitmap = DiskCache.getInstance(mContext).get(uri, imageView.getWidth(), imageView.getHeight());
if (bitmap != null) {
MemoryCache.getInstance().addBitmapToMemoryCache(uri, bitmap);
}
} catch (IOException e) {
e.printStackTrace();
}
return bitmap;
}
/**
* 从内存缓存中获取bitmap
* @param uri
* @return
*/
private Bitmap loadBitmapFromMemCache(String uri) {
final String key = MD5.hashKeyFormUrl(uri);
return MemoryCache.getInstance().getBitmapFromMemoryCache(key);
}
/**
* 返回结果的封装
*/
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;
}
}
}
其他相关类:
//MD5
public class MD5 {
public static String hashKeyFormUrl(String url){
String cacheKey = url;
try {
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return cacheKey;
}
private static 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();
}
}
//路径获取相关类 FilePath
public class FilePath {
public static File getDiskCacheDir(Context context, String uniqueName) {
boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
final String cachePath;
if (externalStorageAvailable) {
cachePath = context.getExternalCacheDir().getParent();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
public static long getUsableSpace(File path){
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){
return path.getUsableSpace();
}
final StatFs statFs = new StatFs(path.getPath());
return statFs.getBlockSizeLong() * statFs.getAvailableBlocksLong();
}
}
http://download.csdn.net/detail/akaic/9659585