众所周知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作为一款网络请求框架,更适合于大量小数据的请求。
但是在我们使用一套图片加载框架的时候,不能加载本地图片和资源图片,显然带来极大不得方便。
在这里虽然我提供了本地图片加载的方法,但是还是要说一句,对于图片请求,我更建议使用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://开头,都会导致异常。
我们从ImageLoader入手,注意到ImageLoader并没有实现一级缓存(也就是内存缓存),Google的原意是让我自己去扩展这个缓存,我考虑从这里入手。
首先来看,如果我们要为ImageLoader加简单的一级缓存,应该怎么做。
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让我们设置,所以必须进行一层包装
接下来使用就很简单了,只有在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();
}
}
}
接下来我们实现本地文件,资源的加载类(看成一种缓存!)。
首先,我们给几种资源定义成一个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。
最后我们来看一下这个类的使用,其实我们最终要使用的,也就是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,实现加载本地和资源图片