定制Volley,实现加载本地和资源图片
2015-11-03 15:12
1161 查看
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加载本地资源文件,如果存在这个资源,就返回这张图片就好了。
也就是把本地图片当成一种缓存!
并且这种缓存在内存缓存之后,在硬盘缓存之前,也在网络请求之前。
大家可以看一下流程图:
类关系设计
看明白上面的流程图以后,我们就动手写代码了,由于ImageLoader只允许设计一个一级缓存,而我们这里起码要实现两层缓存(把本地资源图片看做是一种缓存),所以我们还要将这两层缓存组合成一个缓存。这里我使用组合模式,便于以后其他缓存的扩展。
首先来看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); }
相信看到这里,大家就明白了这个类设计的原理,其实就是做了一个文件数据流的加载工作。
另外还有一些工作,例如图片压缩,比例缩放等,我就不贴出来了,下面大家可以看我上传的源码。
实现内存缓存和本地资源文件缓存的结合
在实现了上面两个类以后,我们要做的工作,就是把它们放在一个类里面,我称为CombineCachepublic 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,载本地、资源图片
最后我们来看一下这个类的使用,其实我们最终要使用的,也就是ComposeCacheRequestQueue 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,实现加载本地和资源图片
相关文章推荐
- 使用C++实现JNI接口需要注意的事项
- Android IPC进程间通讯机制
- Android Manifest 用法
- [转载]Activity中ConfigChanges属性的用法
- Android之获取手机上的图片和视频缩略图thumbnails
- Android之使用Http协议实现文件上传功能
- Android学习笔记(二九):嵌入浏览器
- android string.xml文件中的整型和string型代替
- i-jetty环境搭配与编译
- android之定时器AlarmManager
- android wifi 无线调试
- Android Native 绘图方法
- Android java 与 javascript互访(相互调用)的方法例子
- android 代码实现控件之间的间距
- android FragmentPagerAdapter的“标准”配置
- Android"解决"onTouch和onClick的冲突问题
- android:installLocation简析
- android searchView的关闭事件
- SourceProvider.getJniDirectories