前面http://www.cnblogs.com/androidsuperman/p/8a157b18ede85caa61ca5bc04bba43d0.html
有讲到使用LRU来处理缓存的,但是只是处理内存里面的缓存,没进行文件缓存和处理,那么如何实现Volley在本地的缓存呢
一般硬盘缓存使用com.jakewharton.disklrucache.DiskLruCache这个Lru缓存,具体代码在
重写ImageCache实现图片二级缓存L2LRUImageCache.java
public class L2LRUImageCache implements ImageLoader.ImageCache{ LruCache<String, Bitmap> lruCache; DiskLruCache diskLruCache; final int RAM_CACHE_SIZE = 10 * 1024 * 1024; String DISK_CACHE_DIR = "cache"; //硬盘缓存50M final long DISK_MAX_SIZE = 50 * 1024 * 1024; String cacheFullPath; public L2LRUImageCache(Context context) { //此处是标准的Lru缓存写法 this.lruCache = new LruCache<String, Bitmap>(RAM_CACHE_SIZE) { @Override protected int sizeOf(String key, Bitmap value) { return value.getByteCount(); } }; //如果sd卡存在,创建缓存目录 if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { File cacheDir = context.getExternalFilesDir(DISK_CACHE_DIR); cacheFullPath=cacheDir.getAbsolutePath(); if(!cacheDir.exists()) { cacheDir.mkdir(); } try { diskLruCache = DiskLruCache.open(cacheDir, 1, 1, DISK_MAX_SIZE); } catch (IOException e) { e.printStackTrace(); } } } @Override public Bitmap getBitmap(String url) { String key=generateKey(url); //从内存缓存读取 Bitmap bmp = lruCache.get(key); //内存缓存中没有,读取本地文件缓存 if (bmp == null) { PLog.d(this,"内存读图失败,从磁盘读"+url); bmp = getBitmapFromDiskLruCache(key); //从磁盘读出后,放入内存 if(bmp!=null) { lruCache.put(key,bmp); } } //如果文件里面也没有这个文件,就有必要采取网络下载方式进行下载 if(bmp==null) { PLog.d(this,"从缓存读图失败,去下载"+url); } return bmp; } //文件缓存到内存缓存和本地缓存 @Override public void putBitmap(String url, Bitmap bitmap) { //文件缓存中的key为md5后的url链接 String key=generateKey(url); lruCache.put(key, bitmap); putBitmapToDiskLruCache(key,bitmap); } //清理内存缓存,以及缓存目录里面的文件 @Override public void clear() { lruCache.evictAll(); FileUtils.deleteFile(cacheFullPath); } //图片放入文件缓存中区 private void putBitmapToDiskLruCache(String key, Bitmap bitmap) { if(diskLruCache!=null) { try { DiskLruCache.Editor editor = diskLruCache.edit(key); if (editor != null) { OutputStream outputStream = editor.newOutputStream(0); bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream); editor.commit(); } } catch (IOException e) { e.printStackTrace(); } } } //获取图片本地缓存 private Bitmap getBitmapFromDiskLruCache(String key) { if(diskLruCache!=null) { try { DiskLruCache.Snapshot snapshot = diskLruCache.get(key); if (snapshot != null) { InputStream inputStream = snapshot.getInputStream(0); if (inputStream != null) { Bitmap bmp = BitmapFactory.decodeStream(inputStream); inputStream.close(); return bmp; } } } catch (IOException e) { e.printStackTrace(); } } return null; } /** * 因为DiskLruCache对key有限制,只能是[a-z0-9_-]{1,64},所以用md5生成key * @param url * @return */ private String generateKey(String url) { return MD5Utils.getMD532(url); } }
接下来考虑Volley正常加载图片时怎么加载的,如下:
ImageListener listener = ImageLoader.getImageListener(ivImage,
R.drawable.ic_launcher, R.drawable.ic_launcher);
imageLoader.get(string, listener);
那么调用L2LRUImageCache 进行二级缓存和缓存文件读取并加载到组件上面的逻辑也就在重写imageLoader的逻辑里面:
原生的volley里面 imageLoader.get(string, listener);进入如下代码
public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { // only fulfill requests that were initiated from the main thread. throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); // Try to look up the request in the cache of remote images. Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { // Return the cached bitmap. ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null); imageListener.onResponse(container, true); return container; } // The bitmap did not exist in the cache, fetch it! ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); // Update the caller to let them know that they should use the default bitmap. imageListener.onResponse(imageContainer, true); // Check to see if a request is already in-flight. BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // If it is, add this request to the list of listeners. request.addContainer(imageContainer); return imageContainer; } // The request is not already in flight. Send the new request to the network and // track it. Request<?> newRequest = new ImageRequest(requestUrl, new Listener<Bitmap>() { @Override public void onResponse(Request request,Bitmap response,boolean isFromCache) { onGetImageSuccess(cacheKey, response); } }, maxWidth, maxHeight, Config.RGB_565, new ErrorListener() { @Override public void onErrorResponse(Request request,VolleyError error) { onGetImageError(cacheKey, error); } }); mRequestQueue.add(newRequest); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); return imageContainer; }
其中throwIfNotOnMainThread为检查是否在主线程,代码如下:
private void throwIfNotOnMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ImageLoader must be invoked from the main thread."); } }
可见Imageloader加载图片必须运行在主线程。
然后getCacheKey获取key信息:如下解释是为1级缓存创建缓存key,创建方法如代码所述:
/** * Creates a cache key for use with the L1 cache. * @param url The URL of the request. * @param maxWidth The max-width of the output. * @param maxHeight The max-height of the output. */ private static String getCacheKey(String url, int maxWidth, int maxHeight) { return new StringBuilder(url.length() + 12).append("#W").append(maxWidth) .append("#H").append(maxHeight).append(url).toString(); } }
Bitmap cachedBitmap = mCache.getBitmap(cacheKey)然后就是去1级缓存里面去都去缓存内容:
如下ImageCache是个接口,推荐使用LRUCache来实现1级缓存:
/** * Simple cache adapter interface. If provided to the ImageLoader, it * will be used as an L1 cache before dispatch to Volley. Implementations * must not block. Implementation with an LruCache is recommended. */ public interface ImageCache { public Bitmap getBitmap(String url); public void putBitmap(String url, Bitmap bitmap); public void clear(); }
如果1级缓存不为null,就cachedBitmap回调给图片加载的response;
如果1级缓存为null的话,就去加载默认图片:
ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener);
其中imageListener携带加载失败和默认图片的设置信息.
后面代码就是如果这个图片url没有请求过就会去请求,通过网络的形式从服务器端拉去图片信息,并对成功失败进行处理。
为了实现加载网络图片二级缓存和从从二级缓存中读取,必须重现原来的ImageLoader类,对缓存读取,加载进行重写:
AsyncImageLoader.java
public class AsyncImageLoader extends ImageLoader{ /** * 在取的请求,可能没取到 */ ConcurrentHashMap<String, ReadImageRequest> readImageRequestConcurrentHashMap = new ConcurrentHashMap<>(); // 读数据线程池,限制两个线程 private ExecutorService readExecutorService = new ThreadPoolExecutor(0, 2, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); //UI线程的Handler Handler mainHandler; private static AsyncImageLoader instance; //独立请求列队 private static RequestQueue requestQueue; private AsyncImageLoader(RequestQueue queue, ImageCache imageCache) { super(queue, imageCache); mainHandler = new Handler(Looper.getMainLooper()); } /** * 返回默认的ImageLoader,使用两级缓存,单独的请求队列 * @return */ public static AsyncImageLoader getDefaultImageLoader() { if(instance==null) { requestQueue=Volley.newRequestQueue(PApplication.getInstance()); requestQueue.start(); instance = new AsyncImageLoader(requestQueue, new L2LRUImageCache(PApplication.getInstance())); } return instance; } /** * 销毁,停止所有未处理请求 */ public void destory() { requestQueue.stop(); instance=null; } @Override public ImageContainer get(String requestUrl, ImageListener imageListener, int maxWidth, int maxHeight) { // TODO Auto-generated method stub throwIfNotOnMainThread(); final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight); ImageContainer imageContainer = new ImageContainer(null, requestUrl, cacheKey, imageListener); ReadImageRequest readImageRequest =readImageRequestConcurrentHashMap.get(cacheKey); if(readImageRequest ==null){ readImageRequest =new ReadImageRequest(imageContainer, cacheKey); readImageRequestConcurrentHashMap.put(cacheKey, readImageRequest); //去读缓存,读不到会自动转到请求网络 readExecutorService.execute(new ReadCache(imageContainer, cacheKey,maxWidth,maxHeight)); }else{ //如果该请求已经存在,添加ImageContainer,不再发请求 readImageRequest.addContainer(imageContainer); } return imageContainer; } private void throwIfNotOnMainThread() { if (Looper.myLooper() != Looper.getMainLooper()) { throw new IllegalStateException("ImageLoader must be invoked from the main thread."); } } /** * 创建缓存的key * * @param url * @param maxWidth * @param maxHeight * @return */ private static String getCacheKey(String url, int maxWidth, int maxHeight) { return new StringBuilder(url.length() + 12).append("#W").append(maxWidth).append("#H").append(maxHeight) .append(url).toString(); } /** * 读取缓存,读不到会转发给网络 */ class ReadCache implements Runnable { ImageContainer container; String cacheKey; int maxWidth, maxHeight; public ReadCache(ImageContainer container, String cacheKey,int maxWidth,int maxHeight) { this.container = container; this.cacheKey = cacheKey; this.maxWidth=maxWidth; this.maxHeight=maxHeight; } @Override public void run() { // TODO Auto-generated method stub Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { ReadImageRequest cacheRequest = readImageRequestConcurrentHashMap.get(cacheKey); if (cacheRequest != null) { cacheRequest.setCacheBitmap(cachedBitmap); readSuccess(cacheKey); } } else { // 读不到缓存,去下载 mainHandler.post(new GetImageUseNetWork(container,cacheKey,maxWidth,maxHeight)); } } } /** * 读取缓存或下载图片成功,分发结果 * @param cacheKey */ private void readSuccess(String cacheKey) { ReadImageRequest successedCacheRequest = readImageRequestConcurrentHashMap.remove(cacheKey); if(successedCacheRequest!=null) { successedCacheRequest.deliver(); } } private void readFailure(String cacheKey,VolleyError error) { ReadImageRequest successedCacheRequest = readImageRequestConcurrentHashMap.remove(cacheKey); if(successedCacheRequest!=null) { successedCacheRequest.deliverError(error); } } class GetImageUseNetWork implements Runnable { ImageContainer imageContainer; String cacheKey; int maxWidth,maxHeight; public GetImageUseNetWork(ImageContainer imageContainer, String cacheKey,int maxWidth,int maxHeight) { this.imageContainer = imageContainer; this.cacheKey = cacheKey; this.maxWidth=maxWidth; this.maxHeight=maxHeight; } @Override public void run() { BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // If it is, add this request to the list of listeners. request.addContainer(imageContainer); } // The request is not already in flight. Send the new request to the network and // track it. Request<?> newRequest = new ImageRequest(imageContainer.getRequestUrl(), new Response.Listener<Bitmap>() { @Override public void onResponse(Request request,Bitmap response,boolean isFromCache) { PLog.d(this,"onResponse"); Bitmap bmpCompressed=ImageUtil.scaleBitmap(response, maxWidth, maxHeight); ReadImageRequest cacheRequest = readImageRequestConcurrentHashMap.get(cacheKey); if (cacheRequest != null) { cacheRequest.setCacheBitmap(bmpCompressed); //放到缓存里 mCache.putBitmap(cacheKey, bmpCompressed); readSuccess(cacheKey); } } }, 0, 0, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(Request request,VolleyError error) { PLog.d(this,"onErrorResponse"); onGetImageError(cacheKey, error); readFailure(cacheKey,error); } }); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); mRequestQueue.add(newRequest); } } /** * 清除缓存 */ public void clearCache() { mCache.clear(); } }
1,可以设置线程池的大小,这里设置为2条线程;
2,getDefaultImageLoader里面调用上面的图片缓存的代码L2LRUImageCache;
3,重点的读取和缓存还是在get方法里面。
readExecutorService.execute(new ReadCache(imageContainer, cacheKey,maxWidth,maxHeight));
然后看线程池里面读取缓存的逻辑:
// TODO Auto-generated method stub Bitmap cachedBitmap = mCache.getBitmap(cacheKey); if (cachedBitmap != null) { ReadImageRequest cacheRequest = readImageRequestConcurrentHashMap.get(cacheKey); if (cacheRequest != null) { cacheRequest.setCacheBitmap(cachedBitmap); readSuccess(cacheKey); } } else { // 读不到缓存,去下载 mainHandler.post(new GetImageUseNetWork(container,cacheKey,maxWidth,maxHeight)); }
mCache.getBitmap(cacheKey)是从一级缓存中区读取bitmap,如果一级缓存里面没有,就去下载,调用下面逻辑,拉取下来后,对图片进行裁剪,并将图片放入缓存里面去,而mCache.putBitmap(cacheKey,bmpCompressed);就是调用L2LRUImageCache 的putbitmap放来将缓存内容放入文件和内存中去。
BatchedImageRequest request = mInFlightRequests.get(cacheKey); if (request != null) { // If it is, add this request to the list of listeners. request.addContainer(imageContainer); } // The request is not already in flight. Send the new request to the network and // track it. Request<?> newRequest = new ImageRequest(imageContainer.getRequestUrl(), new Response.Listener<Bitmap>() { @Override public void onResponse(Request request,Bitmap response,boolean isFromCache) { PLog.d(this,"onResponse"); Bitmap bmpCompressed=ImageUtil.scaleBitmap(response, maxWidth, maxHeight); ReadImageRequest cacheRequest = readImageRequestConcurrentHashMap.get(cacheKey); if (cacheRequest != null) { cacheRequest.setCacheBitmap(bmpCompressed); //放到缓存里 mCache.putBitmap(cacheKey, bmpCompressed); readSuccess(cacheKey); } } }, 0, 0, Bitmap.Config.RGB_565, new Response.ErrorListener() { @Override public void onErrorResponse(Request request,VolleyError error) { PLog.d(this,"onErrorResponse"); onGetImageError(cacheKey, error); readFailure(cacheKey,error); } }); mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer)); mRequestQueue.add(newRequest);
代码摘自:https://github.com/pocketdigi/PLib/tree/androidstudio/src/main/java/com/pocketdigi/plib/volley,可以参考优化volley对图片的二级缓存