之前实现了一个选择本地图片进行加载显示的选择器,利用Glide作为图片加载器,Glide是一个十分方便的图片加载库,在项目中使用Glide也十分方便。由于项目需要自己搭建一个图片加载框架,实现功能比较简单,查阅资料后开始着手实现。
一、 实现目标
要实现的图片加载器主要是加载网络图片进行显示,加入LruCache进行内存缓存,在进行列表显示的时候加载重复图片可以直接从缓存中取图片进行显示,节约了网络资源,不用再重复下载。但是这个内存缓存只能在程序运行时分配到内存才能进行缓存,在程序结束时缓存也就释放了,下一次打开程序还是要重新下载。利用硬盘缓存DiskLruCache作为一个二级缓存目录,将下载的图片保存到本地缓存,由于本地缓存目录是默认创建的可以随程序卸载而删除,也可以显示调用删除缓存的方法进行清除,因此不用担心缓存占用很大空间的问题。
二、 框架实现
1. 线程池
在进行图片下载的时候要开辟一个新线程进行耗时操作,往往在加载列表显示的图片时会一次性开启很多线程,这里使用线程池来进行管理线程,利用一个任务Map进行管理,如果该图片地址存在线程正在进行下载,就不会创建新的线程,而是等待线程的下载完成。之前已经进行过线程池的使用,在之前的基础上进行开发。
2. LruCache
上一篇日记是关于LruCache的使用的,在使用内存缓存进行下载任务的缓存已经搞懂了,具体的实现代码见上一篇。
3. DiskLruCache
硬盘缓存是在LruCache的基础上进行改进的缓存机制,相比于内存缓存,硬盘缓存可以把缓存保存到本地,利用生成的key进行保存和获取,这个key可以根据传入的网络地址进行生成,得到一个哈希值,在取值的时候传入要查找的key就可以找到缓存文件,进行解码显示。
首先新建一个类,在这个类的基础上进行
public class ImageDiskCache {
/**
* 得到硬盘缓存目录
* @param context
* @param uniqueName
* @return
*/
public static File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
/**
* 得到版本号
* @param context
* @return
*/
public static int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
return info.versionCode;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
/**
* 获得缓存文件hash值
* @param key
* @return
*/
public static String hashKeyForDisk(String key) {
String cacheKey;
try {
final MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(key.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(key.hashCode());
}
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();
}
}
创建一个DiskLruCache对象不能直接new,要调用open方法进行
try {
File cacheDir = ImageDiskCache.getDiskCacheDir (context, "bitmap");
if(!cacheDir.exists ()) {
cacheDir.mkdirs ();
}
//创建硬盘缓存实例
mDiskLruCache = DiskLruCache.open (cacheDir, ImageDiskCache.getAppVersion (context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace ();
}
下载完成时把缓存文件保存到本地
String key = ImageDiskCache.hashKeyForDisk (imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit (key);
if(editor != null) {
OutputStream outputStream = editor.newOutputStream (0);
if(downloadStream (imageUrl, outputStream)) {
editor.commit ();
} else {
editor.abort ();
}
}
mDiskLruCache.flush ();
加载图片时首先访问内存缓存,如果内存缓存有就调用,没有继续访问硬盘缓存,如果硬盘缓存也没用再开启一个新的下载线程进行下载显示
public void loadImage(String imageUrl, ImageView iv) {
iv.setTag (imageUrl);
Bitmap bitmap = getBitmapFromCache (imageUrl);
if(bitmap != null) {
iv.setImageBitmap (bitmap);
Log.i (TAG, "cache image");
} else {
try {
String key = ImageDiskCache.hashKeyForDisk (imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get (key);
if(snapShot != null) {
InputStream is = snapShot.getInputStream (0);
bitmap = BitmapFactory.decodeStream (is);
iv.setImageBitmap (bitmap);
Log.i (TAG, "disk cache image");
}
} catch (IOException e) {
e.printStackTrace ();
}
}
if(bitmap == null) {
if(!mLoadMap.containsKey (imageUrl)) {
ImageDownloaderTask task = new ImageDownloaderTask (imageUrl, iv);
mLoadMap.put (imageUrl, task);
try {
mThreadPoolExecutor.execute (task);
} catch (Exception e) {
mLoadMap.remove (imageUrl);
}
}else {
ImageDownloaderTask task = mLoadMap.get (imageUrl);
task.run ();
}
Log.i (TAG, "download image");
}
}
4. 自定义ImageView
从网络上下载的图片尺寸不一,在使用一般的ImageView进行显示的时候不能实现按图片比例进行填充屏幕的显示,在显示图片的时候宽度固定为手机屏幕尺寸,高度随图片比例进行显示。
public class ImageLoadView extends android.support.v7.widget.AppCompatImageView {
public ImageLoadView(Context context) {
super (context);
}
public ImageLoadView(Context context, @Nullable AttributeSet attrs) {
super (context, attrs);
}
public ImageLoadView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super (context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Drawable drawable = getDrawable ();
if(drawable != null){
int width = drawable.getMinimumWidth();
int height = drawable.getMinimumHeight();
//确定显示比例,宽高比不变
float scale = (float)height/width;
int widthMeasure = MeasureSpec.getSize(widthMeasureSpec);
int heightMeasure = (int)(widthMeasure*scale);
heightMeasureSpec = MeasureSpec.makeMeasureSpec(heightMeasure, MeasureSpec.EXACTLY);
}
super.onMeasure (widthMeasureSpec, heightMeasureSpec);
}
}
三、 完整代码
package com.ruadong.utils.imageloader;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import android.util.LruCache;
import android.widget.ImageView;
import android.widget.Toast;
import com.jakewharton.disklrucache.DiskLruCache;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author ruandong
* @create 2018/8/18
* @package com.ruadong.utils
* @Describe 图片加载器
*/
public class ImageLoader {
private static final String TAG = "ImageLoader";
/**
* 返回Java虚拟机可用的处理器数量
*/
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 + 1;
/**
* 空闲线程存活时间为2秒
*/
private static final int KEEP_ALIVE_TIME = 2;
/**
* 最大任务队列数
*/
private static final int MAX_TASK_COUNT = 1000;
/**
* 内存缓存最大值
*/
private static final int MAX_CACHE_SIZE = (int) (Runtime.getRuntime ().maxMemory () / 8);
/**
* 图片加载器实例
*/
private static volatile ImageLoader mImageLoader;
/**
* 图片加载线程池
*/
private ThreadPoolExecutor mThreadPoolExecutor;
/**
* 任务队列
*/
private BlockingQueue<Runnable> mBlockingDeque;
/**
* 下载集合
*/
private Map<String, ImageDownloaderTask> mLoadMap;
/**
* 主线程handler
*/
private Handler mMainHandler;
/**
* 上下文
*/
private Context mContext;
/**
* 内存缓存LruCache
*/
private static LruCache<String, Bitmap> mLruCache;
/**
* 硬盘缓存DiskLruCache作为图片加载器的二级缓存,可以缓存从内存缓存中remove的bitmap,利用open方法创建实例
*/
private DiskLruCache mDiskLruCache;
/**
* 图片加载器构造器
*/
private ImageLoader() {
}
/**
* 单例模式,获取图片加载器实例
*
* @return
*/
public static ImageLoader getInstance() {
if(mImageLoader == null) {
synchronized (ImageLoader.class) {
if(mImageLoader == null) {
mImageLoader = new ImageLoader ();
}
}
}
return mImageLoader;
}
/**
* 初始化ImageLoader
*
* @param context
*/
public void initImageLoader(Context context) {
this.mContext = context.getApplicationContext ();
mBlockingDeque = new LinkedBlockingDeque<> ();
mThreadPoolExecutor = new ThreadPoolExecutor (CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, mBlockingDeque);
mMainHandler = new Handler ();
mLruCache = new LruCache<String, Bitmap> (MAX_CACHE_SIZE) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount ();
}
};
try {
File cacheDir = ImageDiskCache.getDiskCacheDir (context, "bitmap");
if(!cacheDir.exists ()) {
cacheDir.mkdirs ();
}
//创建硬盘缓存实例
mDiskLruCache = DiskLruCache.open (cacheDir, ImageDiskCache.getAppVersion (context), 1, 10 * 1024 * 1024);
} catch (IOException e) {
e.printStackTrace ();
}
}
/**
* 获取内存缓存
*
* @return
*/
public LruCache<String, Bitmap> getLruCache() {
return mLruCache;
}
/**
* 清除内存缓存
*/
public void clearLruCache() {
mLruCache.evictAll ();
}
/**
* 保存bitmap到内存缓存
*
* @param key
* @param bitmap
*/
public void saveBitmapToCache(String key, Bitmap bitmap) {
if(getBitmapFromCache (key) == null) {
mLruCache.put (key, bitmap);
Log.i (TAG, "cache size is " + mLruCache.size ());
}
}
/**
* 查询内存缓存是否已经存在当前bitmap
*
* @param key
* @return
*/
public Bitmap getBitmapFromCache(String key) {
return mLruCache.get (key);
}
/**
* 从内存缓存中移除该bitmap
*/
public void clearDiskLruCache() {
try {
mDiskLruCache.delete ();
Log.i (TAG, "clear disk cache");
} catch (IOException e) {
e.printStackTrace ();
}
}
/**
* 从外部开启任务线程加载图片
*
* @param imageUrl
* @param iv
*/
public void loadImage(String imageUrl, ImageView iv) {
iv.setTag (imageUrl);
Bitmap bitmap = getBitmapFromCache (imageUrl);
if(bitmap != null) {
iv.setImageBitmap (bitmap);
Log.i (TAG, "cache image");
} else {
try {
String key = ImageDiskCache.hashKeyForDisk (imageUrl);
DiskLruCache.Snapshot snapShot = mDiskLruCache.get (key);
if(snapShot != null) {
InputStream is = snapShot.getInputStream (0);
bitmap = BitmapFactory.decodeStream (is);
iv.setImageBitmap (bitmap);
Log.i (TAG, "disk cache image");
}
} catch (IOException e) {
e.printStackTrace ();
}
}
if(bitmap == null) {
if(!mLoadMap.containsKey (imageUrl)) {
ImageDownloaderTask task = new ImageDownloaderTask (imageUrl, iv);
mLoadMap.put (imageUrl, task);
try {
mThreadPoolExecutor.execute (task);
} catch (Exception e) {
mLoadMap.remove (imageUrl);
}
}else {
ImageDownloaderTask task = mLoadMap.get (imageUrl);
task.run ();
}
Log.i (TAG, "download image");
}
}
/**
* 图片加载任务类
*/
private class ImageDownloaderTask implements Runnable {
private static final int CONNECT_TIMEOUT = 5 * 1000;
private static final int READ_TIMEOUT = 20 * 1000;
protected static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
private Bitmap bitmap;
private String imageUrl;
private ImageView ivLoader;
public ImageDownloaderTask(String imageUrl, ImageView ivLoader) {
this.imageUrl = imageUrl;
this.ivLoader = ivLoader;
}
@Override
public void run() {
boolean flag = true;
try {
while (flag) {
bitmap = downloadBitmap (imageUrl);
mImageLoader.saveBitmapToCache (imageUrl, bitmap);
String key = ImageDiskCache.hashKeyForDisk (imageUrl);
DiskLruCache.Editor editor = mDiskLruCache.edit (key);
if(editor != null) {
OutputStream outputStream = editor.newOutputStream (0);
if(downloadStream (imageUrl, outputStream)) {
editor.commit ();
} else {
editor.abort ();
}
}
mDiskLruCache.flush ();
Log.i (TAG, "disk cache success");
//图片下载完成,handler通知主线程更新界面
if(mMainHandler != null) {
mMainHandler.post (new Runnable () {
@Override
public void run() {
if(ivLoader != null && imageUrl.equals (ivLoader.getTag ())) {
ivLoader.setImageBitmap (bitmap);
Toast.makeText (mContext, "download success", Toast.LENGTH_SHORT).show ();
}
}
});
}
flag = false;
}
} catch (Exception e) {
e.printStackTrace ();
}
}
/**
* 解析图片流为bitmap
*
* @param imageUrl
* @return
*/
protected Bitmap downloadBitmap(String imageUrl) {
Bitmap bitmap;
InputStream is = getInputStreamFromURL (imageUrl);
bitmap = BitmapFactory.decodeStream (is);
return bitmap;
}
/**
* 根据传入的图片地址获取网络连接
*
* @param imageUrl
* @return
*/
protected HttpURLConnection getConnection(String imageUrl) {
HttpURLConnection connection = null;
String encodedUrl = Uri.encode (imageUrl, ALLOWED_URI_CHARS);
try {
if(connection != null) {
connection.disconnect ();
} else {
URL url = new URL (encodedUrl);
connection = (HttpURLConnection) url.openConnection ();
connection.setConnectTimeout (CONNECT_TIMEOUT);
connection.setReadTimeout (READ_TIMEOUT);
}
} catch (MalformedURLException e) {
e.printStackTrace ();
} catch (IOException e) {
e.printStackTrace ();
}
return connection;
}
/**
* 从网络连接获取图片流
*
* @param imageUrl
* @return
*/
protected InputStream getInputStreamFromURL(String imageUrl) {
InputStream inputStream = null;
HttpURLConnection connection = getConnection (imageUrl);
try {
inputStream = connection.getInputStream ();
} catch (IOException e) {
e.printStackTrace ();
if(inputStream != null) {
try {
inputStream.close ();
} catch (IOException e1) {
e1.printStackTrace ();
}
}
}
return inputStream;
}
/**
* @param imageUrl
* @param outputStream
* @return
*/
protected Boolean downloadStream(String imageUrl, OutputStream outputStream) {
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
in = new BufferedInputStream (getInputStreamFromURL (imageUrl), 8 * 1024);
out = new BufferedOutputStream (outputStream, 8 * 1024);
int b;
while ((b = in.read ()) != -1) {
out.write (b);
}
return true;
} catch (final IOException e) {
e.printStackTrace ();
} finally {
if(getConnection (imageUrl) != null) {
getConnection (imageUrl).disconnect ();
}
try {
if(out != null) {
out.close ();
}
if(in != null) {
in.close ();
}
} catch (final IOException e) {
e.printStackTrace ();
}
}
return false;
}
}
}
使用也很方便
ImageLoader imageLoader = ImageLoader.getInstance ();
String imageUrl = "...图片地址";
ImageLoadView imageView = findViewById(R.id.iv);
imageLoader.initImageLoader (context);
imageLoader.loadImage(imageUrl,imageView);