定制Volley,实现加载本地和资源图片

volley加载网络图片

众所周知volley提供了一个ImageLoader类用于网络图片的加载,本质上也是用消息队列的那一套去进行图片请求,只是请求以后做了一些图片本地缓存、缩放、错位处理等内容。
下面我们来看一个简单的加载例子:

public RequestQueue requestQueue = Volley.newRequestQueue(mContext);
public ImageLoader mImageLoader = new ImageLoader(requestQueue, null);
mImageLoader.get(url, new ImageListener() {                 
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        Log.i("cky", "error");
                        ImageView img = (ImageView)(findViewById(R.id.img));                        
                        //设置默认失败图片
                                                                     img.setImageDrawable(mContext.getResources().getDrawable(R.drawable.ic_launcher));
                    }

                    @Override
                    public void onResponse(ImageContainer response, boolean isImmediate) {
                        ImageView img = (ImageView)(findViewById(R.id.img));
                        img.setImageBitmap(response.getBitmap());
                    }
                },400,400);

使用方式非常简单,使用一个RequestQueue生成一个ImageLoader以后,调用get()方法进行图片请求,最终在监听器中为imageview设置请求得到的Bitmap即可。


为什么Volley不能加载本地图片?

根据源码,从Volley设计结构来看,Volley并没有让使用者添加本地图片加载机制的考虑,Volley作为一款网络请求框架,更适合于大量小数据的请求。
但是在我们使用一套图片加载框架的时候,不能加载本地图片和资源图片,显然带来极大不得方便。
在这里虽然我提供了本地图片加载的方法,但是还是要说一句,对于图片请求,我更建议使用U**niversal-ImageLoader**这个框架。
那么为什么volley不能加载本地图片呢?我们来看代码片段,在BasicNetwork类中:

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
    long requestStart = SystemClock.elapsedRealtime();
    while (true) {
        ...
        } catch (SocketTimeoutException e) {
            attemptRetryOnException("socket", request, new TimeoutError());
        } catch (ConnectTimeoutException e) {
            attemptRetryOnException("connection", request, new TimeoutError());
        } catch (MalformedURLException e) {
            throw new RuntimeException("Bad URL " + request.getUrl(), e);
        } catch (IOException e) {
            ...
        }
    }
}

可以看到,这里catch了一个MalformedURLException,也就是说Volley会检查请求地址是否是http,https等开头网络请求地址,如果不是,就会有异常了。所以本地图片的地址,一般以file://开头,资源图片地址,以drawable://开头,都会导致异常。


怎么修改Volley使其能加载本地图片

我们从ImageLoader入手,注意到ImageLoader并没有实现一级缓存(也就是内存缓存),Google的原意是让我自己去扩展这个缓存,我考虑从这里入手。
首先来看,如果我们要为ImageLoader加简单的一级缓存,应该怎么做。

1、继承Cache类,封装LruCache类

public class BitmapCache implements ImageCache {  

    private LruCache<String, Bitmap> mCache;  

    public BitmapCache() {  
        int maxSize = 10 * 1024 * 1024;  
        mCache = new LruCache<String, Bitmap>(maxSize) {  
            @Override  
            protected int sizeOf(String key, Bitmap bitmap) {  
                return bitmap.getRowBytes() * bitmap.getHeight();  
            }  
        };  
    }  

    @Override  
    public Bitmap getBitmap(String url) {  
        return mCache.get(url);  
    }  

    @Override  
    public void putBitmap(String url, Bitmap bitmap) {  
        mCache.put(url, bitmap);  
    }  

}  

LruCache是常见的一个使用先进先出原理的缓存类,但是我们要对这个类进行一下封装,原因可以看下面IamgeLoader的源码:

public class ImageLoader {
    ...
    /** The cache implementation to be used as an L1 cache before calling into volley. */
    private final ImageCache mCache;
    ...
    /** * 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);
    }
    ...
    /** * Constructs a new ImageLoader. * @param queue The RequestQueue to use for making image requests. * @param imageCache The cache to use as an L1 cache. */
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
        mRequestQueue = queue;
        mCache = imageCache;
    }

也就是ImageLoader提供了一个接口IamgeCache让我们设置,所以必须进行一层包装

2、为ImageLoader设置一级缓存

接下来使用就很简单了,只有在ImageLoader的构造函数中传入这个BitmapCache对象就可以了

ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());



本地图片请求设计思路

直接思路是这样子的,我们在一级缓存里面做文章,新建一个实现了ImageCache的缓存类,这个类可以进行内存缓存,也可以进行本地图片的加载。
由于Volley在请求网络图片请求前,会先检查缓存中是否存在该图片,如果存在,就进行网络请求了。
内存缓存的实现上面我们已经看到了,如果内存缓存中没有,我们可以尝试去根据url加载本地资源文件,如果存在这个资源,就返回这张图片就好了。
也就是把本地图片当成一种缓存
并且这种缓存在内存缓存之后,在硬盘缓存之前,也在网络请求之前。
大家可以看一下流程图:
定制Volley,实现加载本地和资源图片_第1张图片


类关系设计

看明白上面的流程图以后,我们就动手写代码了,由于ImageLoader只允许设计一个一级缓存,而我们这里起码要实现两层缓存(把本地资源图片看做是一种缓存),所以我们还要将这两层缓存组合成一个缓存。
这里我使用组合模式,便于以后其他缓存的扩展。
定制Volley,实现加载本地和资源图片_第2张图片

首先来看BaseImageCache这个抽象类,其实就是提供了md5加密算法,目的是根据url保证缓存中key的唯一性,同事避免url格式造成的解析错误

public abstract class BaseImageCache implements ImageCache{
    public int priority = 1;
    protected boolean isCache = true;
    private final static boolean debug = true;
    public final static String TAG = "cacheDebug";
    public BitmapFilter mBitmapFilter = new BitmapFilter(){

        @Override
        public boolean isFilter() {
            return false;
        }       
    };

    /** * 使用MD5算法对传入的key进行加密并返回。 */  
    public static String hashKeyForDisk(String key) {
        long s = System.currentTimeMillis();        
        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());  
        }  
        long e = System.currentTimeMillis();        
        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();  
    }  

    public abstract void clear();  
    protected interface BitmapFilter{
        public boolean isFilter();
    }

    public void setBitmapFilter(BitmapFilter mBitmapFilter){
        this.mBitmapFilter = mBitmapFilter;
    }
}

同时可以看到,有一个BitmapFilter的内部接口,这个接口的作用是用于自定义过滤机制的,接下来就会讲到。

有一个基类以后,首先实现内存缓存,实现方式和前面的BitmapCache是一模一样的

public class MemoryCache extends BaseImageCache{
    public static final String TAG = MemoryCache.class.getSimpleName();
    LruCache<String, Bitmap> mLruCache;

    public MemoryCache() {      
        this(0.125f);
        priority = Integer.MAX_VALUE;
    }

    public MemoryCache(float scale) {
        // 获取应用程序最大可用内存 
        int maxMemory = (int) Runtime.getRuntime().maxMemory();   
        int cacheSize = (int) (scale*maxMemory);  
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {  
            @Override  
            protected int sizeOf(String key, Bitmap bitmap) {  
                return bitmap.getByteCount();  
            }  
        };  
    }

    @Override
    public Bitmap getBitmap(String url) {
        Bitmap res = mLruCache.get(hashKeyForDisk(url));                
        return res;
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        if(isCache){
            mLruCache.put(hashKeyForDisk(url), bitmap);
        }
    }

    @Override
    public void clear() {
        if(isCache){
            mLruCache.evictAll();
        }
    }
}



本地资源文件缓存类LocalFileCache

接下来我们实现本地文件,资源的加载类(看成一种缓存!)。
首先,我们给几种资源定义成一个Enum

/** Represents supported schemes(protocols) of URI. Provides convenient methods for work with schemes and URIs. */
    public enum Scheme {
        FILE("file"),CONTENT("content"),ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN("");

        private String scheme;
        private String uriPrefix;

        Scheme(String scheme) {
            this.scheme = scheme;
            uriPrefix = scheme + "://";
        }

        /** * Defines scheme of incoming URI * * @param uri URI for scheme detection * @return Scheme of incoming URI */
        public static Scheme ofUri(String uri) {
            if (uri != null) {
                for (Scheme s : values()) {
                    if (s.belongsTo(uri)) {
                        return s;
                    }
                }
            }
            return UNKNOWN;
        }

        private boolean belongsTo(String uri) {
            return uri.toLowerCase(Locale.US).startsWith(uriPrefix);
        }

        /** Appends scheme to incoming path */
        public String wrap(String path) {
            return uriPrefix + path;
        }

        /** Removed scheme part ("scheme://") from incoming URI */
        public String crop(String uri) {
            if (!belongsTo(uri)) {
                throw new IllegalArgumentException(String.format("URI [%1$s] doesn't have expected scheme [%2$s]", uri, scheme));
            }
            return uri.substring(uriPrefix.length());
        }
    }

这样的目的是根据请求地址,解析出请求的是哪种资源
例如如果请求地址是drawable://icon.png我们就看使用一个Enum类的ofUri()方法将其转换成对应的Enum对象DRAWABLE。
不同的资源有不同的加载方式,但是本质都是获取它们的数据流,也就是获得一个InputStream

public class LocalFileCache extends BaseImageCache{ 
    ...
    @Override
    public Bitmap getBitmap(String url) {
        Bitmap res = null;          
        //因为volley会给Url加上一个头,这里做一个替换以获得正确地址
        String key = url.replaceFirst("#W[0-9]*#H[0-9]*", "");
        log(key);
        try {
            InputStream in = getStream(key);//根据地址获取数据流
            if(in!=null){
                res = InputStream2Bitmap(in);       
            }
        } catch (IOException e) {           
            e.printStackTrace();
        }       
        return res;
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        if(isCache)
            log("do nothing!Sorry,you can't write file by this method!");
    }       
    ...
}

接下来就是getStream()方法

public InputStream getStream(String imageUri) throws IOException {
        switch (Scheme.ofUri(imageUri)) {           
            case FILE:
                return getStreamFromFile(imageUri);
            case CONTENT:
                return getStreamFromContent(imageUri);
            case ASSETS:
                return getStreamFromAssets(imageUri);
            case DRAWABLE:
                return getStreamFromDrawable(imageUri);
            case UNKNOWN:
            default:
                return getStreamFromOtherSource(imageUri);
        }
    }

OK,看到这里我们大致明白这个类是做什么的了,就是根据请求地址不同,有各种方式去请求资源或者文件的数据流,然后包装成一个bitmap返回。
上面switch下的各种方法,只是针对不同资源而写的,例如我贴一个从assets中获取资源的:

protected InputStream getStreamFromAssets(String imageUri) throws IOException {
        String filePath = Scheme.ASSETS.crop(imageUri);
        return mContext.getAssets().open(filePath);
    }

相信看到这里,大家就明白了这个类设计的原理,其实就是做了一个文件数据流的加载工作。
另外还有一些工作,例如图片压缩,比例缩放等,我就不贴出来了,下面大家可以看我上传的源码。



实现内存缓存和本地资源文件缓存的结合

在实现了上面两个类以后,我们要做的工作,就是把它们放在一个类里面,我称为CombineCache

public class CombineCache extends BaseImageCache{

    HashMap<String,BaseImageCache> cacheMap = new HashMap<String,BaseImageCache>();
    List<BaseImageCache> cacheArray = new ArrayList<BaseImageCache>();

    public CombineCache(Context context) {
            addCache(new MemoryCache());
            //addCache(new DiskCache());取消硬盘缓存方案,因为volley内部已经实现了这一级缓存
            addCache(new LocalFileCache(context,Config.RGB_565,400,400));//本地图片缩放方案
    }

    @Override
    public Bitmap getBitmap(String url) {       
        Bitmap res = null;
        for(BaseImageCache cache:cacheArray){
            res = cache.getBitmap(url);
            if(res!=null){//如果优先级较高的缓存中有
                putPriorityBitmap(url,res);//向优先级高缓存
                return res; //返回结果
            }
        }
        return res;     
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        for(BaseImageCache cache:cacheArray){//加入缓存
            if(!cache.mBitmapFilter.isFilter())
                cache.putBitmap(url,bitmap);            
        }
    }

    public void clear(){
        for(BaseImageCache cache:cacheArray){
            cache.clear();
        }
    }

    private void putPriorityBitmap(String url, Bitmap bitmap){
        for(BaseImageCache cache:cacheArray){           
            if(cache==mCache){
                return;
            }
            if(!cache.mBitmapFilter.isFilter())
                cache.putBitmap(url,bitmap);            
        }
    }

    //添加缓存类
    public void addCache(BaseImageCache cache){
        cacheMap.put(cache.getClass().getSimpleName(), cache);
        cacheArray.add(cache);
        Collections.sort(cacheArray,new Comparator<BaseImageCache>(){

            @Override
            public int compare(BaseImageCache lhs, BaseImageCache rhs) {
                return lhs.priority>rhs.priority?-1:1;
            }                           
        });
    }
    //移除缓存类
    public void removeCache(BaseImageCache cache){
        cacheMap.remove(cache.getClass().getSimpleName());
        for(BaseImageCache mcache:cacheArray){
            if(mcache==cache){              
                cacheArray.remove(cache);
                return;
            }
        }
    }
    //清空缓存
    public void clearCache(){
        cacheMap.clear();
        cacheArray.clear();
    }
}

注意到,我维护了一个队列cacheArray,里面装的都是BaseImageCache对象,并且按照优先级排序
在putBitmap()方法中,将获得的Bitmap对象加入缓存。
在getBitmap()方法中,根据优先级从高到底,逐个查找是否存在缓存,如果存在缓存,还有将这个bitmap缓存到比自己优先级高缓存当中
通过这个类,我就可以根据需求控制各种缓存的顺序,并且ComposeCache本身也是BaseImageCache的一种,所以我们也可以将其添加到cacheArray中(当然对于缓存来说,不会有太多这样需求,这里只是我的过度设计哈哈)。
这是我们常见的组合模式,有所不同的是,组合模式一般是基类提供一个add()方法,一个显然的例子就是ViewGroup


使用定制Volley,载本地、资源图片

最后我们来看一下这个类的使用,其实我们最终要使用的,也就是ComposeCache

RequestQueue requestQueue = Volley.newRequestQueue(mContext);
CombineCache mCombineCache = new CombineCache(mContext);
ImageLoader mImageLoader = new ImageLoader(requestQueue, mCombineCache);

只要将缓存类替换一下,就可以和普通的ImageLoader一样使用啦!
加载本地图片:

mImageLoader.get("file://"+SDCardUtils.getSDCardPath()+"hhh.jpg", new ImageListener() {                 
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        ...                     
                    }

                    @Override
                    public void onResponse(ImageContainer response, boolean isImmediate) {
                        ImageView img = (ImageView)(findViewById(R.id.img));
                        img.setImageBitmap(response.getBitmap());
                    }
                },400,400);

加载drawable中的图片:

mImageLoader.get("drawable://"+R.drawable.icon, new ImageListener() {                   
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        ...                     
                    }

                    @Override
                    public void onResponse(ImageContainer response, boolean isImmediate) {
                        ImageView img = (ImageView)(findViewById(R.id.img));
                        img.setImageBitmap(response.getBitmap());
                    }
                },400,400);

加载assets中的图片:

mImageLoader.get("assets://hhh.jpg", new ImageListener() {                  
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        ...                     
                    }

                    @Override
                    public void onResponse(ImageContainer response, boolean isImmediate) {
                        ImageView img = (ImageView)(findViewById(R.id.img));
                        img.setImageBitmap(response.getBitmap());
                    }
                },400,400);



写在最后

看完上面的代码,可能有的朋友会问,为什么没有实现硬盘缓存的方案,而我的类图设计中,确实是有一个DiskCache的。
通过分析Volley源码,我们会发现Volley本身就实现了一个硬盘缓存,所以我们也没有必要去自己写一个。
当然这也造成了这个缓存不在我们的控制之中,我们只有把握好整个Volley的请求流程,才能很好的结合这个硬盘缓存方案。
当然我们也可以重写Volley的硬盘缓存类DiskBasedCache,Google也是鼓励我们这样去做的。
但是在这个例子中,这也做不好改。再次重申,Volley更适合做网络数据请求

源码下载地址:源码
转载请注明出处定制Volley,实现加载本地和资源图片

你可能感兴趣的:(android,图片,缓存,Volley,本地图片)