Android Volley图片加载功能详解
gituhb项目
volley源码中文注释项目我已经上传到github,欢迎大家fork和start.
为什么写这篇博客
本来文章是维护在github上的,但是我在分析imageloader源码过程中与到了一个问题,希望大家能帮助解答.
volley获取网络图片
本来想分析universal image loader的源码,但是发现volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:
1. 获取网络图片的url.
2. 判断该url对应的图片是否有本地缓存.
3. 有本地缓存,直接使用本地缓存图片,通过异步回调给imageview进行设置.
4. 无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给imageview进行设置.
我们通过volley源码,看一下volley是否是按照这个步骤实现网络图片加载的.
imagerequest.java
按照volley的架构,我们首先需要构造一个网络图片请求,volley帮我们封装了imagerequest类,我们来看一下它的具体实现:
/** 网络图片请求类. */ @suppresswarnings("unused") public class imagerequest extends request<bitmap> { /** 默认图片获取的超时时间(单位:毫秒) */ public static final int default_image_request_ms = 1000; /** 默认图片获取的重试次数. */ public static final int default_image_max_retries = 2; private final response.listener<bitmap> mlistener; private final bitmap.config mdecodeconfig; private final int mmaxwidth; private final int mmaxheight; private imageview.scaletype mscaletype; /** bitmap解析同步锁,保证同一时间只有一个bitmap被load到内存进行解析,防止oom. */ private static final object sdecodelock = new object(); /** * 构造一个网络图片请求. * @param url 图片的url地址. * @param listener 请求成功用户设置的回调接口. * @param maxwidth 图片的最大宽度. * @param maxheight 图片的最大高度. * @param scaletype 图片缩放类型. * @param decodeconfig 解析bitmap的配置. * @param errorlistener 请求失败用户设置的回调接口. */ public imagerequest(string url, response.listener<bitmap> listener, int maxwidth, int maxheight, imageview.scaletype scaletype, bitmap.config decodeconfig, response.errorlistener errorlistener) { super(method.get, url, errorlistener); mlistener = listener; mdecodeconfig = decodeconfig; mmaxwidth = maxwidth; mmaxheight = maxheight; mscaletype = scaletype; } /** 设置网络图片请求的优先级. */ @override public priority getpriority() { return priority.low; } @override protected response<bitmap> parsenetworkresponse(networkresponse response) { synchronized (sdecodelock) { try { return doparse(response); } catch (outofmemoryerror e) { return response.error(new volleyerror(e)); } } } private response<bitmap> doparse(networkresponse response) { byte[] data = response.data; bitmapfactory.options decodeoptions = new bitmapfactory.options(); bitmap bitmap; if (mmaxwidth == 0 && mmaxheight == 0) { decodeoptions.inpreferredconfig = mdecodeconfig; bitmap = bitmapfactory.decodebytearray(data, 0, data.length, decodeoptions); } else { // 获取网络图片的真实尺寸. decodeoptions.injustdecodebounds = true; bitmapfactory.decodebytearray(data, 0, data.length, decodeoptions); int actualwidth = decodeoptions.outwidth; int actualheight = decodeoptions.outheight; int desiredwidth = getresizeddimension(mmaxwidth, mmaxheight, actualwidth, actualheight, mscaletype); int desireheight = getresizeddimension(mmaxwidth, mmaxheight, actualwidth, actualheight, mscaletype); decodeoptions.injustdecodebounds = false; decodeoptions.insamplesize = findbestsamplesize(actualwidth, actualheight, desiredwidth, desireheight); bitmap tempbitmap = bitmapfactory.decodebytearray(data, 0, data.length, decodeoptions); if (tempbitmap != null && (tempbitmap.getwidth() > desiredwidth || tempbitmap.getheight() > desireheight)) { bitmap = bitmap.createscaledbitmap(tempbitmap, desiredwidth, desireheight, true); tempbitmap.recycle(); } else { bitmap = tempbitmap; } } if (bitmap == null) { return response.error(new volleyerror(response)); } else { return response.success(bitmap, httpheaderparser.parsecacheheaders(response)); } } static int findbestsamplesize( int actualwidth, int actualheight, int desiredwidth, int desireheight) { double wr = (double) actualwidth / desiredwidth; double hr = (double) actualheight / desireheight; double ratio = math.min(wr, hr); float n = 1.0f; while ((n * 2) <= ratio) { n *= 2; } return (int) n; } /** 根据imageview的scaletype设置图片的大小. */ private static int getresizeddimension(int maxprimary, int maxsecondary, int actualprimary, int actualsecondary, imageview.scaletype scaletype) { // 如果没有设置imageview的最大值,则直接返回网络图片的真实大小. if ((maxprimary == 0) && (maxsecondary == 0)) { return actualprimary; } // 如果imageview的scaletype为fix_xy,则将其设置为图片最值. if (scaletype == imageview.scaletype.fit_xy) { if (maxprimary == 0) { return actualprimary; } return maxprimary; } if (maxprimary == 0) { double ratio = (double)maxsecondary / (double)actualsecondary; return (int)(actualprimary * ratio); } if (maxsecondary == 0) { return maxprimary; } double ratio = (double) actualsecondary / (double) actualprimary; int resized = maxprimary; if (scaletype == imageview.scaletype.center_crop) { if ((resized * ratio) < maxsecondary) { resized = (int)(maxsecondary / ratio); } return resized; } if ((resized * ratio) > maxsecondary) { resized = (int)(maxsecondary / ratio); } return resized; } @override protected void deliverresponse(bitmap response) { mlistener.onresponse(response); } }
因为volley本身框架已经实现了对网络请求的本地缓存,所以imagerequest做的主要事情就是解析字节流为bitmap,再解析过程中,通过静态变量保证每次只解析一个bitmap防止oom,使用scaletype和用户设置的maxwidth和maxheight来设置图片大小.
总体来说,imagerequest的实现非常简单,这里不做过多的讲解.imagerequest的缺陷在于:
1.需要用户进行过多的设置,包括图片的大小的最大值.
2.没有图片的内存缓存,因为volley的缓存是基于disk的缓存,有对象反序列化的过程.
imageloader.java
鉴于以上两个缺点,volley又提供了一个更牛逼的imageloader类.其中,最关键的就是增加了内存缓存.
再讲解imageloader的源码之前,需要先介绍一下imageloader的使用方法.和之前的request请求不同,imageloader并不是new出来直接扔给requestqueue进行调度,它的使用方法大体分为4步:
•创建一个requestqueue对象.
requestqueue queue = volley.newrequestqueue(context);
•创建一个imageloader对象.
imageloader构造函数接收两个参数,第一个是requestqueue对象,第二个是imagecache对象(也就是内存缓存类,我们先不给出具体实现,讲解完imageloader源码之后,我会提供一个利用lru算法的imagecache实现类)
imageloader imageloader = new imageloader(queue, new imagecache() { @override public void putbitmap(string url, bitmap bitmap) {} @override public bitmap getbitmap(string url) { return null; } });
•获取一个imagelistener对象.
imagelistener listener = imageloader.getimagelistener(imageview, r.drawable.default_imgage, r.drawable.failed_image);
•调用imageloader的get方法加载网络图片.
imageloader.get(mimageurl, listener, maxwidth, maxheight, scaletype);
有了imageloader的使用方法,我们结合使用方法来看一下imageloader的源码:
@suppresswarnings({"unused", "stringbufferreplaceablebystring"}) public class imageloader { /** * 关联用来调用imageloader的requestqueue. */ private final requestqueue mrequestqueue; /** 图片内存缓存接口实现类. */ private final imagecache mcache; /** 存储同一时间执行的相同cachekey的batchedimagerequest集合. */ private final hashmap<string, batchedimagerequest> minflightrequests = new hashmap<string, batchedimagerequest>(); private final hashmap<string, batchedimagerequest> mbatchedresponses = new hashmap<string, batchedimagerequest>(); /** 获取主线程的handler. */ private final handler mhandler = new handler(looper.getmainlooper()); private runnable mrunnable; /** 定义图片k1缓存接口,即将图片的内存缓存工作交给用户来实现. */ public interface imagecache { bitmap getbitmap(string url); void putbitmap(string url, bitmap bitmap); } /** 构造一个imageloader. */ public imageloader(requestqueue queue, imagecache imagecache) { mrequestqueue = queue; mcache = imagecache; } /** 构造网络图片请求成功和失败的回调接口. */ public static imagelistener getimagelistener(final imageview view, final int defaultimageresid, final int errorimageresid) { return new imagelistener() { @override public void onresponse(imagecontainer response, boolean isimmediate) { if (response.getbitmap() != null) { view.setimagebitmap(response.getbitmap()); } else if (defaultimageresid != 0) { view.setimageresource(defaultimageresid); } } @override public void onerrorresponse(volleyerror error) { if (errorimageresid != 0) { view.setimageresource(errorimageresid); } } }; } public imagecontainer get(string requesturl, imagelistener imagelistener, int maxwidth, int maxheight, scaletype scaletype) { // 判断当前方法是否在ui线程中执行.如果不是,则抛出异常. throwifnotonmainthread(); final string cachekey = getcachekey(requesturl, maxwidth, maxheight, scaletype); // 从l1级缓存中根据key获取对应的bitmap. bitmap cachebitmap = mcache.getbitmap(cachekey); if (cachebitmap != null) { // l1缓存命中,通过缓存命中的bitmap构造imagecontainer,并调用imagelistener的响应成功接口. imagecontainer container = new imagecontainer(cachebitmap, requesturl, null, null); // 注意:因为目前是在ui线程中,因此这里是调用onresponse方法,并非回调. imagelistener.onresponse(container, true); return container; } imagecontainer imagecontainer = new imagecontainer(null, requesturl, cachekey, imagelistener); // l1缓存命中失败,则先需要对imageview设置默认图片.然后通过子线程拉取网络图片,进行显示. imagelistener.onresponse(imagecontainer, true); // 检查cachekey对应的imagerequest请求是否正在运行. batchedimagerequest request = minflightrequests.get(cachekey); if (request != null) { // 相同的imagerequest正在运行,不需要同时运行相同的imagerequest. // 只需要将其对应的imagecontainer加入到batchedimagerequest的mcontainers集合中. // 当正在执行的imagerequest结束后,会查看当前有多少正在阻塞的imagerequest, // 然后对其mcontainers集合进行回调. request.addcontainer(imagecontainer); return imagecontainer; } // l1缓存没命中,还是需要构造imagerequest,通过requestqueue的调度来获取网络图片 // 获取方法可能是:l2缓存(ps:disk缓存)或者http网络请求. request<bitmap> newrequest = makeimagerequest(requesturl, maxwidth, maxheight, scaletype, cachekey); mrequestqueue.add(newrequest); minflightrequests.put(cachekey, new batchedimagerequest(newrequest, imagecontainer)); return imagecontainer; } /** 构造l1缓存的key值. */ private string getcachekey(string url, int maxwidth, int maxheight, scaletype scaletype) { return new stringbuilder(url.length() + 12).append("#w").append(maxwidth) .append("#h").append(maxheight).append("#s").append(scaletype.ordinal()).append(url) .tostring(); } public boolean iscached(string requesturl, int maxwidth, int maxheight) { return iscached(requesturl, maxwidth, maxheight, scaletype.center_inside); } private boolean iscached(string requesturl, int maxwidth, int maxheight, scaletype scaletype) { throwifnotonmainthread(); string cachekey = getcachekey(requesturl, maxwidth, maxheight, scaletype); return mcache.getbitmap(cachekey) != null; } /** 当l1缓存没有命中时,构造imagerequest,通过imagerequest和requestqueue获取图片. */ protected request<bitmap> makeimagerequest(final string requesturl, int maxwidth, int maxheight, scaletype scaletype, final string cachekey) { return new imagerequest(requesturl, new response.listener<bitmap>() { @override public void onresponse(bitmap response) { ongetimagesuccess(cachekey, response); } }, maxwidth, maxheight, scaletype, bitmap.config.rgb_565, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { ongetimageerror(cachekey, error); } }); } /** 图片请求失败回调.运行在ui线程中. */ private void ongetimageerror(string cachekey, volleyerror error) { batchedimagerequest request = minflightrequests.remove(cachekey); if (request != null) { request.seterror(error); batchresponse(cachekey, request); } } /** 图片请求成功回调.运行在ui线程中. */ protected void ongetimagesuccess(string cachekey, bitmap response) { // 增加l1缓存的键值对. mcache.putbitmap(cachekey, response); // 同一时间内最初的imagerequest执行成功后,回调这段时间阻塞的相同imagerequest对应的成功回调接口. batchedimagerequest request = minflightrequests.remove(cachekey); if (request != null) { request.mresponsebitmap = response; // 将阻塞的imagerequest进行结果分发. batchresponse(cachekey, request); } } private void batchresponse(string cachekey, batchedimagerequest request) { mbatchedresponses.put(cachekey, request); if (mrunnable == null) { mrunnable = new runnable() { @override public void run() { for (batchedimagerequest bir : mbatchedresponses.values()) { for (imagecontainer container : bir.mcontainers) { if (container.mlistener == null) { continue; } if (bir.geterror() == null) { container.mbitmap = bir.mresponsebitmap; container.mlistener.onresponse(container, false); } else { container.mlistener.onerrorresponse(bir.geterror()); } } } mbatchedresponses.clear(); mrunnable = null; } }; // post the runnable mhandler.postdelayed(mrunnable, 100); } } private void throwifnotonmainthread() { if (looper.mylooper() != looper.getmainlooper()) { throw new illegalstateexception("imageloader must be invoked from the main thread."); } } /** 抽象出请求成功和失败的回调接口.默认可以使用volley提供的imagelistener. */ public interface imagelistener extends response.errorlistener { void onresponse(imagecontainer response, boolean isimmediate); } /** 网络图片请求的承载对象. */ public class imagecontainer { /** imageview需要加载的bitmap. */ private bitmap mbitmap; /** l1缓存的key */ private final string mcachekey; /** imagerequest请求的url. */ private final string mrequesturl; /** 图片请求成功或失败的回调接口类. */ private final imagelistener mlistener; public imagecontainer(bitmap bitmap, string requesturl, string cachekey, imagelistener listener) { mbitmap = bitmap; mrequesturl = requesturl; mcachekey = cachekey; mlistener = listener; } public void cancelrequest() { if (mlistener == null) { return; } batchedimagerequest request = minflightrequests.get(mcachekey); if (request != null) { boolean canceled = request.removecontainerandcancelifnecessary(this); if (canceled) { minflightrequests.remove(mcachekey); } } else { request = mbatchedresponses.get(mcachekey); if (request != null) { request.removecontainerandcancelifnecessary(this); if (request.mcontainers.size() == 0) { mbatchedresponses.remove(mcachekey); } } } } public bitmap getbitmap() { return mbitmap; } public string getrequesturl() { return mrequesturl; } } /** * cachekey相同的imagerequest请求抽象类. * 判定两个imagerequest相同包括: * 1. url相同. * 2. maxwidth和maxheight相同. * 3. 显示的scaletype相同. * 同一时间可能有多个相同cachekey的imagerequest请求,由于需要返回的bitmap都一样,所以用batchedimagerequest * 来实现该功能.同一时间相同cachekey的imagerequest只能有一个. * 为什么不使用requestqueue的mwaitingrequestqueue来实现该功能? * 答:是因为仅靠url是没法判断两个imagerequest相等的. */ private class batchedimagerequest { /** 对应的imagerequest请求. */ private final request<?> mrequest; /** 请求结果的bitmap对象. */ private bitmap mresponsebitmap; /** imagerequest的错误. */ private volleyerror merror; /** 所有相同imagerequest请求结果的封装集合. */ private final linkedlist<imagecontainer> mcontainers = new linkedlist<imagecontainer>(); public batchedimagerequest(request<?> request, imagecontainer container) { mrequest = request; mcontainers.add(container); } public volleyerror geterror() { return merror; } public void seterror(volleyerror error) { merror = error; } public void addcontainer(imagecontainer container) { mcontainers.add(container); } public boolean removecontainerandcancelifnecessary(imagecontainer container) { mcontainers.remove(container); if (mcontainers.size() == 0) { mrequest.cancel(); return true; } return false; } } }
重大疑问
个人对imageloader的源码有两个重大疑问?
•batchresponse方法的实现.
我很奇怪,为什么imageloader类里面要有一个hashmap来保存batchedimagerequest集合呢?
private final hashmap<string, batchedimagerequest> mbatchedresponses = new hashmap<string, batchedimagerequest>();
毕竟batchresponse是在特定的imagerequest执行成功的回调中被调用的,调用代码如下:
protected void ongetimagesuccess(string cachekey, bitmap response) { // 增加l1缓存的键值对. mcache.putbitmap(cachekey, response); // 同一时间内最初的imagerequest执行成功后,回调这段时间阻塞的相同imagerequest对应的成功回调接口. batchedimagerequest request = minflightrequests.remove(cachekey); if (request != null) { request.mresponsebitmap = response; // 将阻塞的imagerequest进行结果分发. batchresponse(cachekey, request); } }
从上述代码可以看出,imagerequest请求成功后,已经从minflightrequests中获取了对应的batchedimagerequest对象.而同一时间被阻塞的相同的imagerequest对应的imagecontainer都在batchedimagerequest的mcontainers集合中.
那我认为,batchresponse方法只需要遍历对应batchedimagerequest的mcontainers集合即可.
但是,imageloader源码中,我认为多余的构造了一个hashmap对象mbatchedresponses来保存batchedimagerequest集合,然后在batchresponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.
诡异代码如下:
private void batchresponse(string cachekey, batchedimagerequest request) { mbatchedresponses.put(cachekey, request); if (mrunnable == null) { mrunnable = new runnable() { @override public void run() { for (batchedimagerequest bir : mbatchedresponses.values()) { for (imagecontainer container : bir.mcontainers) { if (container.mlistener == null) { continue; } if (bir.geterror() == null) { container.mbitmap = bir.mresponsebitmap; container.mlistener.onresponse(container, false); } else { container.mlistener.onerrorresponse(bir.geterror()); } } } mbatchedresponses.clear(); mrunnable = null; } }; // post the runnable mhandler.postdelayed(mrunnable, 100); } }
我认为的代码实现应该是:
private void batchresponse(string cachekey, batchedimagerequest request) { if (mrunnable == null) { mrunnable = new runnable() { @override public void run() { for (imagecontainer container : request.mcontainers) { if (container.mlistener == null) { continue; } if (request.geterror() == null) { container.mbitmap = request.mresponsebitmap; container.mlistener.onresponse(container, false); } else { container.mlistener.onerrorresponse(request.geterror()); } } mrunnable = null; } }; // post the runnable mhandler.postdelayed(mrunnable, 100); } }
•使用imageloader默认提供的imagelistener,我认为存在一个缺陷,即图片闪现问题.当为listview的item设置图片时,需要增加tag判断.因为对应的imageview可能已经被回收利用了.
自定义l1缓存类
首先说明一下,所谓的l1和l2缓存分别指的是内存缓存和硬盘缓存.
实现l1缓存,我们可以使用android提供的lru缓存类,示例代码如下:
import android.graphics.bitmap; import android.support.v4.util.lrucache; /** lru算法的l1缓存实现类. */ @suppresswarnings("unused") public class imagelrucache implements imageloader.imagecache { private lrucache<string, bitmap> mlrucache; public imagelrucache() { this((int) runtime.getruntime().maxmemory() / 8); } public imagelrucache(final int cachesize) { createlrucache(cachesize); } private void createlrucache(final int cachesize) { mlrucache = new lrucache<string, bitmap>(cachesize) { @override protected int sizeof(string key, bitmap value) { return value.getrowbytes() * value.getheight(); } }; } @override public bitmap getbitmap(string url) { return mlrucache.get(url); } @override public void putbitmap(string url, bitmap bitmap) { mlrucache.put(url, bitmap); } }
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: JAVA防止重复提交Web表单的方法