【第五篇】Volley代码修改之图片二级缓存以及相关源码阅读(重写ImageLoader.ImageCache)

前面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对图片的二级缓存

上一篇:辣眼睛:程序员这样过儿童节


下一篇:开心农场助手——开发总结