Android开发中比较重要的一块就是图片的加载,其中可以说道的地方太多了,无论是加载大图造成的OOM,多图同时加载造成滑动卡顿,以及网络图片重复加载慢而且耗流量这些问题都是一个新手遇到过的问题。现在已经有好多开源框架拥有图片加载缓存的功能,Universal-Image-Loader, afinal,Xutils都可以实现图片缓存的效果,但是我们不仅需要会用,而且要知道为什么能这么用。所以就产生了这个系列,自己去写一个图片缓存的框架。
缓存一般分为二级缓存,一级为内存缓存,二级为磁盘(硬盘)缓存,内存缓存速度快,但是内存大小有限制,磁盘一般为SD卡,或者是手机内存,大小限制上限比较大,但是速度相较于内存慢一点,不过肯定比从网络上加载图片要快的多。
我们的思路就是将图片从网上下载下来后,存入内存缓存和磁盘缓存中,内存缓存会有一个大小限制,一般设为可用最大内存的四分之一,当达到上限时根据LruCache算法将内存中访问最少的移除,在加载图片时优先从内存中加载,如果内存中没有就从磁盘中加载,如果磁盘中也没有就从网络上获取,然后再写入缓存中。
为了便于扩展,我们将缓存算法抽象成接口,除了LruCache算法,还可以使用其他的算法实现,但是实现的方法都是差不多的
MemoryCacheAware.java
public interface MemoryCacheAware<K ,V> {
//存储
boolean put(K key ,V value);
//获取
V get(K key);
//移除
void remove(K key);
//清空
void clear();
//返回所有键名
Collection<K> keys();
}
Android在v4的包下已经有了LruCache的实现,我们仿照其继承上面的接口实现自己的LruCache算法
LruMemoryCache.java
public class LruMemoryCache implements MemoryCacheAware<String, Bitmap> {
// Lru缓存
private final LinkedHashMap<String, Bitmap> cache;
// 最大缓存空间
private final int maxSize;
// 当前缓存空间
private int currentSize;
public LruMemoryCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize<=0");
}
this.maxSize = maxSize;
// 按照访问顺序排序,第三个参数设置为true时按照访问顺序排序,设为false时按照插入顺序排序
this.cache = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
}
//在链表中插入图片
@Override
public boolean put(String key, Bitmap value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
synchronized(this){
currentSize += sizeOf(key, value);
//previous是先前的key,value映射,如果没有就为null,这里因为前面加上了内存,所以判断如果先前有的话
//是不会插入的,这样再将内存减去,在操作完后,调用一次trimtosize移除缓存值达到上限时访问最少的对象
Bitmap previous = cache.put(key, value);
if (previous != null) {
currentSize -= sizeOf(key, previous);
}
trimToSize(maxSize);
}
return true;
}
//根据key得到图片
@Override
public Bitmap get(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}
synchronized(this){
return cache.get(key);
}
}
//从链表中移除指定的key
@Override
public void remove(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}
synchronized(this){
Bitmap previous = cache.remove(key);
if (previous != null) {
currentSize -= sizeOf(key, previous);
}
}
}
//清空缓存
@Override
public void clear() {
trimToSize(-1);
}
//获取链表中所有的key值
@Override
public Collection<String> keys() {
return new HashSet<String>(cache.keySet());
}
/**
* 把最近最少使用的对象在缓存值达到预设的值之前移除
* @param maxSize
*/
private void trimToSize(int maxSize) {
while (true) {
String key;
Bitmap value;
synchronized (this) {
if (currentSize < 0 || (cache.isEmpty() && currentSize != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results");
}
if (currentSize <= maxSize || cache.isEmpty()) {
break;
}
Entry<String, Bitmap> entry = cache.entrySet().iterator().next();
if (entry == null) {
break;
}
key = entry.getKey();
value = entry.getValue();
cache.remove(key);
currentSize -= sizeOf(key, value);
}
}
}
/**
* 返回指定图片的大小
*
* @param key
* @param value
* @return
*/
private int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
public synchronized final String toString() {
return String.format("Lrucache[maxSize=d%]", maxSize);
}
}
主要是使用的LinkedHashMap的方法,具体细节注释已经写的很详细了,然后我们实现一个总类,负责实例化线程池,设置内存缓存的大小,加入设置加载中的图片显示,加载失败的图片显示,以及放入内存,和从内存中取图片的方法。
public class AsyncImageLoader {
private static AsyncImageLoader imageLoader;
private Context context;
// 异步任务执行者
private Executor executor;
// 加载任务的集合
public Set<BitmapWorkerTask> taskCollection;
// 内存缓存
public LruMemoryCache memoryCache;
// 加载中显示的bitmap
public Bitmap loadingBitmap;
// 加载完成显示的Bitmap
public Bitmap loadfailBitmap;
public static AsyncImageLoader getInstance(Context context) {
if (imageLoader == null) {
imageLoader = new AsyncImageLoader(context);
}
return imageLoader;
}
public AsyncImageLoader(Context context) {
this.context = context;
// 初始化线程池
executor = new ThreadPoolExecutor(3, 200, 10, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>());
// 初始化任务集合
taskCollection = new HashSet<BitmapWorkerTask>();
// 获取应用程序最大可用内存
int maxMemory = (int) Runtime.getRuntime().maxMemory();
int cacheSize = maxMemory / 4;
// 设置内存缓存为最大可用内存的四分之一
memoryCache = new LruMemoryCache(cacheSize);
}
// 设置加载中的图片
public void setLoadingDrawable(int resourceId) {
loadingBitmap = BitmapFactory.decodeResource(context.getResources(),
resourceId);
}
// 设置加载失败的图片
public void setFailDrawable(int resourceId) {
loadfailBitmap = BitmapFactory.decodeResource(context.getResources(),
resourceId);
}
/**
* 加载图片,先是加载中图片,如果内存中没有再加载网络图片
*
* @param view
* @param imageView
* @param imgUrl
*/
public void loadBitmaps(View view, ImageView imageView, String imgUrl) {
if (imageView != null && loadingBitmap != null) {
imageView.setImageBitmap(loadingBitmap);
}
Bitmap bitmap = getBitmapFromMemoryCache(imgUrl);
if (bitmap == null) {
BitmapWorkerTask task = new BitmapWorkerTask(imageLoader, view);
taskCollection.add(task);
task.executeOnExecutor(executor, imgUrl);
} else {
if (imageView != null && bitmap != null) {
imageView.setImageBitmap(bitmap);
}
}
}
/**
* 设置图片到内存缓存中
*
* @param key
* @param value
*/
public void addBitmapToMemoryCache(String key, Bitmap value) {
if (getBitmapFromMemoryCache(key) == null) {
memoryCache.put(key, value);
}
}
/**
* 根据key从memorycache中取图片
*
* @param key
* @return
*/
public Bitmap getBitmapFromMemoryCache(String key) {
return memoryCache.get(key);
}
/**
* 取消所有正在下载或等待下载的任务
*/
public void cancelAllTask() {
if (taskCollection != null) {
for (BitmapWorkerTask task : taskCollection) {
task.cancel(false);
}
}
}
}
里面使用的相当于一个单例的模式,通过getInstance获取一个AsyncImageLoader对象,如果没有就调用构造函数,并在其中实例化线程池和memorycache的内存大小,除此之外每次加载图片时我们采用一个类继承于AsyncTask,这样就能在同时加载多张图片时进行异步加载,在加载时将BitmapWorkerTask对象加入Set集合中,在加载完成后再从集合中删除。
public class BitmapWorkerTask extends AsyncTask<String, Void, Bitmap> {
private AsyncImageLoader imageLoader;
// 显示图片控件所在的视图
private View view;
// 图片Url地址
protected String imageUrl;
public BitmapWorkerTask(AsyncImageLoader imageLoader, View view) {
this.imageLoader = imageLoader;
this.view = view;
}
@Override
protected Bitmap doInBackground(String... params) {
imageUrl = params[0];
// 通过url下载图片
Bitmap bitmap = downloadBitmap(params[0]);
if (bitmap != null) {
// 将图片放入内存缓存中
imageLoader.addBitmapToMemoryCache(params[0], bitmap);
}
return bitmap;
}
@Override
protected void onPostExecute(Bitmap result) {
super.onPostExecute(result);
// 通过tag返回一个imageview对象
ImageView imageView = (ImageView) view.findViewWithTag(imageUrl);
if (imageView != null) {
if (result != null) {
// 加载成功
imageView.setImageBitmap(result);
} else {
// 加载失败
if (imageLoader.loadfailBitmap != null) {
imageView.setImageBitmap(imageLoader.loadfailBitmap);
}
}
}
imageLoader.taskCollection.remove(this);
}
/**
* 通过http协议,根据url返回bitmap对象
*
* @param imgUrl
* @return
*/
private Bitmap downloadBitmap(String imgUrl) {
Bitmap bitmap = null;
HttpURLConnection conn = null;
try {
URL url = new URL(imgUrl);
conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(6 * 1000);
conn.setReadTimeout(10 * 1000);
bitmap = BitmapFactory.decodeStream(conn.getInputStream());
} catch (Exception e) {
e.printStackTrace();
} finally {
if (conn != null) {
conn.disconnect();
}
}
return bitmap;
}
}
中间的ImageView之所以是根据tag而不是id得到对象,是因为listview的加载机制,listview的item在滑动时如果移动了屏幕外会进入RecycleBin中,而我们在getview中开启的异步线程加载网络图片可能在图片还没加载出来时就已经被滑到屏幕外了,这时RecycleBin为新划入的item进行了复用,会用一个imageview实例,导致现在才加载出来的图片显示在这个上面,造成了位置顺序的错乱。所以为每一个imageview设置一个tag,这样就不会乱序了。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.list_item, null);
viewHolder.imageView = (ImageView) convertView
.findViewById(R.id.imageview);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
// 给imageview设置一个tag,是加载时不会乱序
viewHolder.imageView.setTag(mdatas.get(position));
// 开启异步线程加载图片
AsyncImageLoader.getInstance(mContext).loadBitmaps(mListView,
viewHolder.imageView, mdatas.get(position));
return convertView;
}
AsyncImageLoader.getInstance(this).setLoadingDrawable(
R.drawable.loading);
AsyncImageLoader.getInstance(this).setFailDrawable(
R.drawable.ic_launcher);
下一篇就是disk磁盘缓存的内容了,使用的是google推荐的DiskLruCache
源码已经开源github,并加入了演示demo:地址 https://github.com/sheepm/Cache