Android Volley框架全面解析
volley简介
我们平时在开发android应用的时候不可避免地都需要用到网络技术,而多数情况下应用程序都会使用http协议来发送和接收网络数据。android系统中主要提供了两种方式来进行http通信,httpurlconnection和httpclient,几乎在任何项目的代码中我们都能看到这两个类的身影,使用率非常高。
不过httpurlconnection和httpclient的用法还是稍微有些复杂的,如果不进行适当封装的话,很容易就会写出不少重复代码。于是乎,一些android网络通信框架也就应运而生,比如说asynchttpclient,它把http所有的通信细节全部封装在了内部,我们只需要简单调用几行代码就可以完成通信操作了。再比如universal-image-loader,它使得在界面上显示网络图片的操作变得极度简单,开发者不用关心如何从网络上获取图片,也不用关心开启线程、回收图片资源等细节,universal-image-loader已经把一切都做好了。
android开发团队也是意识到了有必要将http的通信操作再进行简单化,于是在2013年google i/o大会上推出了一个新的网络通信框架——volley。volley可是说是把asynchttpclient和universal-image-loader的优点集于了一身,既可以像asynchttpclient一样非常简单地进行http通信,也可以像universal-image-loader一样轻松加载网络上的图片。除了简单易用之外,volley在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,volley的表现就会非常糟糕。
准备工作
导入jar包(),申请网络权限
<uses-permission android:name="android.permission.internet" />
http请求与响应
1. 使用stringrequest接收string类型的响应
一个最基本的http请求与响应主要就是进行以下三步操作:
创建一个requestqueue对象。
创建一个stringrequest对象(以stringrequest为例,后面还会介绍其他request)。
将stringrequest对象添加到requestqueue里面。
(1)初始化请求队列对象——requestqueue
requestqueue mqueue = volley.newrequestqueue(context);
requestqueue是一个请求队列对象,它可以缓存所有的http请求,然后按照一定的算法并发地发出这些请求。requestqueue内部的设计就是非常合适高并发的,因此我们不必为每一次http请求都创建一个requestqueue对象,这是非常浪费资源的。所以这里建议用单例模式定义这个对象。当然,你可以选择在一个activity中定义一个requestqueue对象,但这样可能会比较麻烦,而且还可能出现请求队列包含activity强引用的问题。
(2)使用stringrequest接收string类型的响应
前面定义了请求对象,那么自然就有接收响应的对象了,这个框架中有多个响应对象,像stringrequest接受到的响应就是string类型的;jsonrequest接收的响应就是json类型对象。其实它们都是继承自request<\t>,然后根据不同的响应数据来进行特殊的处理。
来看stringrequest的两个构造函数
/** method:请求方法 url:请求的地址 listener:响应成功的监听器 errorlistener:出错时的监听器 **/ public stringrequest(int method, string url, listener<string> listener, errorlistener errorlistener) /**不传入method,默认会调用get方式进行请求**/ public stringrequest(string url, listener<string> listener, errorlistener errorlistener) { this(method.get, url, listener, errorlistener); }
get方式请求网络,代码如下:
stringrequest stringrequest = new stringrequest("http://www.baidu.com", new response.listener<string>() { @override public void onresponse(string response) { toast.maketext(mainactivity.this, response, toast.length_short).show(); } }, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { showlog(error.getmessage()); } });
post方式请求网络,一般我们的post都是要带一些参数的,volley没有提供附加参数的方法,所以我们必须要在stringrequest的匿名类中重写getparams()方法,代码如下所示:
stringrequest stringrequest = new stringrequest(method.post, url, listener, errorlistener) { @override protected map<string, string> getparams() throws authfailureerror { map<string, string> map = new hashmap<string, string>(); map.put("params1", "value1"); map.put("params2", "value2"); return map; } };
这样就传入了value1和value2两个参数了。现在可能有人会问为啥这个框架不提供这个传参的方法,还非得让我们重写。个人觉得这个框架本身的目的就是执行频繁的网络请求,比如下载图片,解析json数据什么的,用get就能很好的实现了,所以就没有提供传参的post方法。
(3)发送请求
发送请求很简单,将stringrequest对象添加到requestqueue里面即可。
mqueue.add(stringrequest);
运行一下程序,发出一条http请求,把服务器返回的string用toast展示出来:
没错,百度返回给我们的就是这样一长串的html代码,虽然我们看起来会有些吃力,但是浏览器却可以轻松地对这段html代码进行解析,然后将百度的首页展现出来。
2. 使用jsonobjectrequest接收json类型的响应
类似于stringrequest,jsonrequest也是继承自request类的,不过由于jsonrequest是一个抽象类,因此我们无法直接创建它的实例,那么只能从它的子类入手了。jsonrequest有两个直接的子类,jsonobjectrequest和jsonarrayrequest,从名字上你应该能就看出它们的区别了吧?一个是用于请求一段json数据的,一个是用于请求一段json数组的。
这里看一下jsonobjectrequest的构造函数:
//jsonrequest:post请求携带的参数,可以为空,表示不携带参数 public jsonobjectrequest(int method, string url, jsonobject jsonrequest, listener<jsonobject> listener, errorlistener errorlistener) { super(method, url, (jsonrequest == null) ? null : jsonrequest.tostring(), listener, errorlistener); } //如果jsonrequest为空,默认使用get请求,否则使用post public jsonobjectrequest(string url, jsonobject jsonrequest, listener<jsonobject> listener, errorlistener errorlistener) { this(jsonrequest == null ? method.get : method.post, url, jsonrequest, listener, errorlistener); }
和stringrequest一样,遵循三步走原则:
requestqueue mqueue = volley.newrequestqueue(context); jsonobjectrequest jsonobjectrequest = new jsonobjectrequest("http://weather.51wnl.com/weatherinfo/getmoreweather?citycode=101020100&weathertype=0", null, new response.listener<jsonobject>() { @override public void onresponse(jsonobject response) { toast.maketext(mainactivity.this, response.tostring(), toast.length_short).show(); try { response = response.getjsonobject("weatherinfo"); showlog("city = " + response.getstring("city")); showlog("weather1 = " + response.getstring("weather1")); } catch (jsonexception e) { e.printstacktrace(); } } }, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { showlog(error.getmessage()); } }); mqueue.add(jsonobjectrequest);
注意jsonobjectrequest的post方式携带参数和stringrequest有些不同,上面stringrequest的方式在这里不起作用。需要下面方式实现:
map<string, string> params = new hashmap<string, string>(); params.put("name1", "value1"); params.put("name2", "value2"); jsonobject jsonrequest= new jsonobject(params); jsonobjectrequest jsonobjectrequest = new jsonobjectrequest(method.post, url, jsonrequest, listener, errorlistener)
上面我们请求的地址是*天气预报的上海天气,看一下运行效果:
可以看出,服务器返回给我们的数据确实是json格式的,并且onresponse()方法中携带的参数也正是一个jsonobject对象,之后只需要从jsonobject对象取出我们想要得到的那部分数据就可以了。
3. 使用imagerequest来请求图片
首先来看一下imagerequest的构造函数
public imagerequest(string url, response.listener<bitmap> listener, int maxwidth, int maxheight, config decodeconfig, response.errorlistener errorlistener) { super(method.get, url, errorlistener); setretrypolicy(new defaultretrypolicy(image_timeout_ms, image_max_retries, image_backoff_mult)); mlistener = listener; mdecodeconfig = decodeconfig; mmaxwidth = maxwidth; mmaxheight = maxheight; }
默认的请求方式是get,初始化方法需要传入:图片的url,一个响应结果监听器,图片的最大宽度,图片的最大高度,图片的颜色属性,出错响应的监听器。
第三第四个参数分别用于指定允许图片最大的宽度和高度,如果指定的网络图片的宽度或高度大于这里的最大值,则会对图片“等比例”进行压缩,指定成0的话就表示不管图片有多大,都不会进行压缩。第五个参数用于指定图片的颜色属性,bitmap.config下的几个常量都可以在这里使用,其中argb_8888可以展示最好的颜色属性,每个图片像素占据4个字节的大小,而rgb_565则表示每个图片像素占据2个字节大小。
三步走开始:
requestqueue mqueue = volley.newrequestqueue(context); imagerequest imagerequest = new imagerequest( "http://img.my.csdn.net/uploads/201308/31/1377949454_6367.jpg", new response.listener<bitmap>() { @override public void onresponse(bitmap response) { image.setimagebitmap(response); } }, 0, 0, config.rgb_565, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { image.setimageresource(r.drawable.default_image); } }); mqueue.add(imagerequest);
看运行效果图:
加载图片— imageloader & networkimageview
volley有没有其他的,更好的方式来获取图片呢?当然有的,比如imageloader、networkimageview这样的对象,它们可以更加方便的获取图片。值得一提的是这两个对象的内部都是使用了imagerequest进行操作的,也就是说imagerequest是本质。
1. imageloader加载图片
imageloader也可以用于加载网络上的图片,不过imageloader明显要比imagerequest更加高效,因为它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求。
由于imageloader已经不是继承自request的了,所以它的用法也和我们之前学到的内容有所不同,总结起来大致可以分为以下四步:
创建一个requestqueue对象。
创建一个imageloader对象。
获取一个imagelistener对象。
调用imageloader的get()方法加载网络上的图片。
(1)创建一个requestqueue对象
我们前面已经写过很多遍了,不再重复介绍了
(2)创建一个imageloader对象
示例代码如下所示:
imageloader imageloader = new imageloader(mqueue, new imagecache() { @override public void putbitmap(string url, bitmap bitmap) { } @override public bitmap getbitmap(string url) { return null; } });
可以看到,imageloader的构造函数接收两个参数,第一个参数就是requestqueue对象,第二个参数是一个imagecache对象(不能传null!),这里的imagecache就是为我们做内存缓存用的,我们可以定制自己的实现方式,现在主流的实现是lrucache,关于lrucache可以参考我之前写的一篇文章android的缓存技术:lrucache和disklrucache。
imageloader imageloader = new imageloader(mqueue, new bitmapcache()); //bitmapcache的实现类 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 value) { return value.getrowbytes() * value.getheight(); } }; @override public bitmap getbitmap(string url) { return mcache.get(url); } @override public void putbitmap(string url, bitmap bitmap) { mcache.put(url, bitmap); } }
(3)获取一个imagelistener对象
imagelistener listener = imageloader.getimagelistener(imageview, r.drawable.default_image, r.drawable.fail_image);
我们通过调用imageloader的getimagelistener()方法能够获取到一个imagelistener对象,getimagelistener()方法接收三个参数,第一个参数指定用于显示图片的imageview控件,第二个参数指定加载图片的过程中显示的图片,第三个参数指定加载图片失败的情况下显示的图片。
(4)调用imageloader的get()方法加载网络上的图片
imageloader.get("http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg", listener);
get()方法接收两个参数,第一个参数就是图片的url地址,第二个参数则是刚刚获取到的imagelistener对象。当然,如果你想对图片的大小进行限制,也可以使用get()方法的重载,指定图片允许的最大宽度和高度,如下所示:
imageloader.get("http://img.my.csdn.net/uploads/201309/01/1378037128_5291.jpg", listener, 600, 600);
运行一下程序点击加载图片,你将看到imageview会先显示一张默认的加载过程中图片,等到网络上的图片加载完成后,imageview则会自动显示该图。如果我们用imageloader再次加载该图片,会很快显示出来而看不到默认的加载过程中图片,这是因为这次的图片是从缓存中取的,速度很快。效果如下图所示。
注:上面我们只是定制了内存缓存,查看源码,可以发现imageloader对图片也进行了硬盘缓存,我们在执行get()方法前可以通过imageloader.setshouldcache(false);来取消硬盘缓存,如果你不进行设置的话默认是执行硬盘缓存的。看看控制硬盘缓存的几个方法:
public final boolean shouldcache() //查看是否已经做了磁盘缓存。 void setshouldcache(boolean shouldcache)//设置是否运行磁盘缓存,此方法需要在get方法前使用 public boolean iscached(string requesturl, int maxwidth, int maxheight)//判断对象是否已经被缓存,传入url,还有图片的最大宽高
2. networkimageview加载图片
networkimageview继承自imageview,你可以认为它是一个可以实现加载网络图片的imageview,十分简单好用。这个控件在被从父控件分离的时候,会自动取消网络请求的,即完全不用我们担心相关网络请求的生命周期问题。
networkimageview控件的用法大致可以分为以下五步:
创建一个requestqueue对象。 创建一个imageloader对象。 在布局文件中添加一个networkimageview控件。 在代码中获取该控件的实例。 设置要加载的图片地址。 <com.android.volley.toolbox.networkimageview android:id="@+id/network_image_view" android:layout_width="200dp" android:layout_height="200dp" android:layout_gravity="center_horizontal" /> /**创建requestqueue以及imageloader对象**/ requestqueue mqueue = volley.newrequestqueue(context); imageloader imageloader = new imageloader(mqueue, new bitmapcache()); /**获取networkimageview控件**/ networkimageview networkimageview = (networkimageview) findviewbyid(r.id.network_image_view); /**设置加载中显示的图片**/ networkimageview.setdefaultimageresid(r.drawable.default_image); /**加载失败时显示的图片**/ networkimageview.seterrorimageresid(r.drawable.fail_image); /**设置目标图片的url地址**/ networkimageview.setimageurl("http://img.my.csdn.net/uploads/201309/01/1378037151_7904.jpg", imageloader);
好了,就是这么简单,现在重新运行一下程序,你将看到和使用imageloader来加载图片一模一样的效果,这里我就不再截图了。
networkimageview没有提供任何设置图片宽高的方法,这是由于它是一个控件,在加载图片的时候它会自动获取自身的宽高,然后对比网络图片的宽度,再决定是否需要对图片进行压缩。也就是说,压缩过程是在内部完全自动化的,并不需要我们关心。
networkimageview最终会始终呈现给我们一张大小比控件尺寸略大的网络图片,因为它会根据控件宽高来等比缩放原始图片,不会多占用任何一点内存,这也是networkimageview最简单好用的一点吧。
如果你不想对图片进行压缩的话,只需要在布局文件中把networkimageview的layout_width和layout_height都设置成wrap_content就可以了,这样它就会将该图片的原始大小展示出来,不会进行任何压缩。
自定义request
volley中提供了几个常用request(stringrequest、jsonobjectrequest、jsonarrayrequest、imagerequest),如果我们有自己特殊的需求,其实完全可以自定义自己的request。
自定义request之前,我们先来看看stringrequest的源码实现:
package com.android.volley.toolbox; public class stringrequest extends request<string> { // 建立监听器来获得响应成功时返回的结果 private final listener<string> mlistener; // 传入请求方法,url,成功时的监听器,失败时的监听器 public stringrequest(int method, string url, listener<string> listener, errorlistener errorlistener) { super(method, url, errorlistener); // 初始化成功时的监听器 mlistener = listener; } /** * creates a new get request. * 建立一个默认的get请求,调用了上面的构造函数 */ public stringrequest(string url, listener<string> listener, errorlistener errorlistener) { this(method.get, url, listener, errorlistener); } @override protected void deliverresponse(string response) { // 用监听器的方法来传递下响应的结果 mlistener.onresponse(response); } @override protected response<string> parsenetworkresponse(networkresponse response) { string parsed; try { // 调用了new string(byte[] data, string charsetname) 这个构造函数来构建string对象,将byte数组按照特定的编码方式转换为string对象,主要部分是data parsed = new string(response.data, httpheaderparser.parsecharset(response.headers)); } catch (unsupportedencodingexception e) { parsed = new string(response.data); } return response.success(parsed, httpheaderparser.parsecacheheaders(response)); } }
首先stringrequest是继承自request类的,request可以指定一个泛型类,这里指定的当然就是string了,接下来stringrequest中提供了两个有参的构造函数,参数包括请求类型,请求地址,以及响应回调等。但需要注意的是,在构造函数中一定要调用super()方法将这几个参数传给父类,因为http的请求和响应都是在父类中自动处理的。
另外,由于request类中的deliverresponse()和parsenetworkresponse()是两个抽象方法,因此stringrequest中需要对这两个方法进行实现。deliverresponse()方法中的实现很简单,仅仅是调用了mlistener中的onresponse()方法,并将response内容传入即可,这样就可以将服务器响应的数据进行回调了。parsenetworkresponse()方法中则是对服务器响应的数据进行解析,其中数据是以字节的形式存放在networkresponse的data变量中的,这里将数据取出然后组装成一个string,并传入response的success()方法中即可。
1. 自定义xmlrequest
了解了stringrequest的实现原理,下面我们就可以动手来尝试实现一下xmlrequest了,代码如下所示:
public class xmlrequest extends request<xmlpullparser> { private final listener<xmlpullparser> mlistener; public xmlrequest(int method, string url, listener<xmlpullparser> listener, errorlistener errorlistener) { super(method, url, errorlistener); mlistener = listener; } public xmlrequest(string url, listener<xmlpullparser> listener, errorlistener errorlistener) { this(method.get, url, listener, errorlistener); } @override protected response<xmlpullparser> parsenetworkresponse(networkresponse response) { try { string xmlstring = new string(response.data, httpheaderparser.parsecharset(response.headers)); xmlpullparserfactory factory = xmlpullparserfactory.newinstance(); xmlpullparser xmlpullparser = factory.newpullparser(); xmlpullparser.setinput(new stringreader(xmlstring)); return response.success(xmlpullparser, httpheaderparser.parsecacheheaders(response)); } catch (unsupportedencodingexception e) { return response.error(new parseerror(e)); } catch (xmlpullparserexception e) { return response.error(new parseerror(e)); } } @override protected void deliverresponse(xmlpullparser response) { mlistener.onresponse(response); } }
可以看到,其实并没有什么太多的逻辑,基本都是仿照stringrequest写下来的,xmlrequest也是继承自request类的,只不过这里指定的泛型类是xmlpullparser,说明我们准备使用pull解析的方式来解析xml。在parsenetworkresponse()方法中,先是将服务器响应的数据解析成一个字符串,然后设置到xmlpullparser对象中,在deliverresponse()方法中则是将xmlpullparser对象进行回调。
下面我们尝试使用这个xmlrequest来请求一段xml格式的数据,http://flash.weather.com.cn/wmaps/xml/china.xml这个接口会将中国所有的省份数据以xml格式进行返回,如下所示:
xmlrequest xmlrequest = new xmlrequest("http://flash.weather.com.cn/wmaps/xml/china.xml", new response.listener<xmlpullparser>() { @override public void onresponse(xmlpullparser response) { try { int eventtype = response.geteventtype(); while (eventtype != xmlpullparser.end_document) { switch (eventtype) { case xmlpullparser.start_tag: string nodename = response.getname(); if ("city".equals(nodename)) { string pname = response.getattributevalue(0); string cname = response.getattributevalue(2); showlog("省份:" + pname + " 城市:" + cname); } break; } eventtype = response.next(); } } catch (xmlpullparserexception e) { e.printstacktrace(); } catch (ioexception e) { e.printstacktrace(); } } }, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { showlog(error.getmessage()); } }); mqueue.add(xmlrequest);
2. 自定义gsonrequest
jsonrequest的数据解析是利用android本身自带的jsonobject和jsonarray来实现的,配合使用jsonobject和jsonarray就可以解析出任意格式的json数据。不过也许你会觉得使用jsonobject还是太麻烦了,还有很多方法可以让json数据解析变得更加简单,比如说gson对象。遗憾的是,volley中默认并不支持使用自家的gson来解析数据,不过没有关系,通过上面的学习,相信你已经知道了自定义一个request是多么的简单,那么下面我们就来举一反三一下,自定义一个gsonrequest。
首先我们需要把gson的jar包导入到项目当中,接着定义一个gsonrequest继承自request,代码如下所示:
public class gsonrequest<t> extends request<t> { private final listener<t> mlistener; private gson mgson; private class<t> mclass; public gsonrequest(int method, string url, class<t> clazz, listener<t> listener, errorlistener errorlistener) { super(method, url, errorlistener); mgson = new gson(); mclass = clazz; mlistener = listener; } public gsonrequest(string url, class<t> clazz, listener<t> listener, errorlistener errorlistener) { this(method.get, url, clazz, listener, errorlistener); } @override protected response<t> parsenetworkresponse(networkresponse response) { try { string jsonstring = new string(response.data, httpheaderparser.parsecharset(response.headers)); return response.success(mgson.fromjson(jsonstring, mclass), httpheaderparser.parsecacheheaders(response)); } catch (unsupportedencodingexception e) { return response.error(new parseerror(e)); } } @override protected void deliverresponse(t response) { mlistener.onresponse(response); } }
gsonrequest是继承自request类的,并且同样提供了两个构造函数。在parsenetworkresponse()方法中,先是将服务器响应的数据解析出来,然后通过调用gson的fromjson方法将数据组装成对象。在deliverresponse方法中仍然是将最终的数据进行回调。
下面我们就来测试一下这个gsonrequest能不能够正常工作吧,同样调用http://www.weather.com.cn/data/sk/101020100.html这个接口可以得到一段json格式的天气数据,如下所示:
{"weatherinfo":{"city":"上海","city_en":"","cityid":101020100,"date":"","date_y":"2016年09月20日","fchh":0,"fl1":"","fl2":"","fl3":"","fl4":"","fl5":"","fl6":"","fx1":"","fx2":"","img1":"1","img10":"1","img11":"1","img12":"1","img2":"1","img3":"1","img4":"1","img5":"1","img6":"1","img7":"1","img8":"1","img9":"1","img_single":0,"img_title1":"","img_title10":"","img_title11":"","img_title12":"","img_title2":"","img_title3":"","img_title4":"","img_title5":"","img_title6":"","img_title7":"","img_title8":"","img_title9":"","img_title_single":"","index":"","index48":"","index48_d":"","index48_uv":"","index_ag":"","index_cl":"","index_co":"","index_d":"","index_ls":"","index_tr":"","index_uv":"","index_xc":"","st1":0,"st2":0,"st3":0,"st4":0,"st5":0,"st6":0,"temp1":"20℃~28℃","temp2":"20℃~26℃","temp3":"19℃~26℃","temp4":"21℃~26℃","temp5":"23℃~28℃","temp6":"22℃~27℃","tempf1":"","tempf2":"","tempf3":"","tempf4":"","tempf5":"","tempf6":"","weather1":"多云","weather2":"多云","weather3":"多云","weather4":"多云","weather5":"多云","weather6":"多云","week":"","wind1":"","wind2":"","wind3":"","wind4":"","wind5":"","wind6":""}}
我们需要使用对象的方式将这段json字符串表示出来。下面新建两个bean文件:
public class weather { public weatherinfo weatherinfo; } public class weatherinfo { public string city; public string cityid; public string date_y; public string temp1; public string weather1; }
下面就是用gsonrequest请求json数据了
gsonrequest<weather> gsonrequest = new gsonrequest<weather>( "http://weather.51wnl.com/weatherinfo/getmoreweather?citycode=101020100&weathertype=0", weather.class, new response.listener<weather>() { @override public void onresponse(weather weather) { weatherinfo weatherinfo = weather.weatherinfo; showlog("city is " + weatherinfo.city); showlog("cityid is " + weatherinfo.cityid); showlog("date_y is " + weatherinfo.date_y); showlog("temp1 is " + weatherinfo.temp1); showlog("weather1 is " + weatherinfo.weather1); } }, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { showlog(error.getmessage()); } }); mqueue.add(gsonrequest);
这里onresponse()方法的回调中直接返回了一个weather对象,我们通过它就可以得到weatherinfo对象,接着就能从中取出json中的相关数据了。运行一下程序,打印log如下:
3. 自定义gsonrequestwithauth
上面自定义的request并没有携带参数,如果我们访问服务器时需要传参呢?譬如通过客户端访问服务器,服务器对客户端进行身份校验后,返回用户信息,客户端直接拿到对象。
先写bean文件:
public class user { private string name; private int age; }
自定义gsonrequestwithauth:
public class gsonrequestwithauth<t> extends request<t> { private final gson gson = new gson(); private final class<t> clazz; private final listener<t> listener; private map<string, string> mheader = new hashmap<string, string>(); private string mbody; /** http请求编码方式 */ private static final string protocol_charset = "utf-8"; /** 设置访问自己服务器时必须传递的参数,密钥等 */ static { mheader.put("app-key", "key"); mheader.put("app-secret", "secret"); } /** * @param url * @param clazz 我们最终的转化类型 * @param listener * @param appendheader 附加头数据 * @param body 请求附带消息体 * @param errorlistener */ public gsonrequestwithauth(string url, class<t> clazz, listener<t> listener, map<string, string> appendheader, string body, errorlistener errorlistener) { super(method.post, url, errorlistener); this.clazz = clazz; this.listener = listener; mheader.putall(appendheader); mbody = body; } @override public map<string, string> getheaders() throws authfailureerror { // 默认返回 return collections.emptymap(); return mheader; } @override public byte[] getbody() { try { return mbody == null ? null : mbody.getbytes(protocol_charset); } catch (unsupportedencodingexception uee) { volleylog.wtf("unsupported encoding while trying to get the bytes of %s using %s", musername, protocol_charset); return null; } } @override protected void deliverresponse(t response) { listener.onresponse(response); } @override protected response<t> parsenetworkresponse(networkresponse response) { try { /** 得到返回的数据 */ string jsonstr = new string(response.data, httpheaderparser.parsecharset(response.headers)); /** 转化成对象 */ return response.success(gson.fromjson(jsonstr, clazz), httpheaderparser.parsecacheheaders(response)); } catch (unsupportedencodingexception e) { return response.error(new parseerror(e)); } catch (jsonsyntaxexception e) { return response.error(new parseerror(e)); } } }
服务器代码:
public class testservlet extends httpservlet { public void doget(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception { this.dopost(request, response); } public void dopost(httpservletrequest request, httpservletresponse response) throws servletexception, ioexception { request.setcharacterencoding("utf-8"); /**获取app-key和app-secret */ string appkey = request.getheader("app-key"); string appsecret = request.getheader("app-secret"); /**获取用户名、密码 */ string username = request.getheader("username"); string password = request.getheader("password"); /**获取消息体 */ int size = request.getcontentlength(); inputstream is = request.getinputstream(); byte[] reqbodybytes = readbytes(is, size); string body = new string(reqbodybytes); if ("admin".equals(username) && "123".equals(password) && "getuserinfo".equals(body)) { response.setcontenttype("text/plain;charset=utf-8"); printwriter out = response.getwriter(); out.print("{\"name\":\"watson\",\"age\":28}"); out.flush(); } } }
使用gsonrequestwithauth和服务器交互请求信息:
map<string, string> appendheader = new hashmap<string, string>(); appendheader.put("username", "admin"); appendheader.put("password", "123"); string url = "http://172.27.35.1:8080/webtest/testservlet"; gsonrequestwithauth<user> userrequest = new gsonrequestwithauth<user>(url, user.class, new listener<user>() { @override public void onresponse(user response) { log.e("tag", response.tostring()); } }, appendheader, "getuserinfo", null); mqueue.add(userrequest);
延伸:
看到没有,我们上面写服务器端代码时,有一句代码是设置服务器返回数据的字符集为utf-8
response.setcontenttype("text/plain;charset=utf-8");
大部分服务器端都会在返回数据的header中指定字符集,如果在服务器端没有指定字符集那么就会默认使用 iso-8859-1 字符集。
iso-8859-1的别名叫做latin1。这个字符集支持部分是用于欧洲的语言,不支持中文,这就会导致服务器返回的中文数据乱码,很不能理解为什么将这个字符集作为默认的字符集。volley这个框架可是要用在网络通信的环境中的。吐槽也没有用,我们来看一下如何来解决中文乱码的问题。有以下几种解决方式:
在服务器的返回的数据的header的中contenttype加上charset=utf-8的声明。
当你无法修改服务器程序的时候,可以定义一个新的子类。覆盖parsenetworkresponse这个方法,直接使用utf-8对服务器的返回数据进行转码。
public class charsetstringrequest extends stringrequest { public charsetstringrequest(string url, listener<string> listener, errorlistener errorlistener) { super(url, listener, errorlistener); } public charsetstringrequest(int method, string url, listener<string> listener, errorlistener errorlistener) { super(method, url, listener, errorlistener); } @override protected response<string> parsenetworkresponse(networkresponse response) { string str = null; try { str = new string(response.data,"utf-8"); //在此处强制utf-8编码 } catch (unsupportedencodingexception e) { e.printstacktrace(); } return response.success(str, httpheaderparser.parsecacheheaders(response)); } }
使用charsetstringrequest请求数据:
charsetstringrequest stringrequest = new charsetstringrequest("http://www.weather.com.cn/data/sk/101010100.html", new response.listener<string>() { @override public void onresponse(string response) { showlog(response); } }, new response.errorlistener() { @override public void onerrorresponse(volleyerror error) { showlog(error.getmessage()); } }); mqueue.add(userrequest);
volley架构解析
1. 总体设计图
上面是 volley 的总体设计图,主要是通过两种diapatch thread不断从requestqueue中取出请求,根据是否已缓存调用cache或network这两类数据获取接口之一,从内存缓存或是服务器取得请求的数据,然后交由responsedelivery去做结果分发及回调处理。
2. volley中的概念
简单介绍一些概念,在详细设计中会仔细介绍。
volley 的调用比较简单,通过 newrequestqueue(…) 函数新建并启动一个请求队列requestqueue后,只需要往这个requestqueue不断 add request 即可。
volley:volley 对外暴露的 api,通过 newrequestqueue(…) 函数新建并启动一个请求队列requestqueue。
request:表示一个请求的抽象类。stringrequest、jsonrequest、imagerequest都是它的子类,表示某种类型的请求。
requestqueue:表示请求队列,里面包含一个cachedispatcher(用于处理走缓存请求的调度线程)、networkdispatcher数组(用于处理走网络请求的调度线程),一个responsedelivery(返回结果分发接口),通过 start() 函数启动时会启动cachedispatcher和networkdispatchers。
cachedispatcher:一个线程,用于调度处理走缓存的请求。启动后会不断从缓存请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给responsedelivery去执行后续处理。当结果未缓存过、缓存失效或缓存需要刷新的情况下,该请求都需要重新进入networkdispatcher去调度处理。
networkdispatcher:一个线程,用于调度处理走网络的请求。启动后会不断从网络请求队列中取请求处理,队列为空则等待,请求处理结束则将结果传递给responsedelivery去执行后续处理,并判断结果是否要进行缓存。
responsedelivery:返回结果分发接口,目前只有基于executordelivery的在入参 handler 对应线程内进行分发。
httpstack:处理 http 请求,返回请求结果。目前 volley 中有基于 httpurlconnection 的hurlstack和 基于 apache httpclient 的httpclientstack。
network:调用httpstack处理请求,并将结果转换为可被responsedelivery处理的networkresponse。
cache:缓存请求结果,volley 默认使用的是基于 sdcard 的diskbasedcache。networkdispatcher得到请求结果后判断是否需要存储在 cache,cachedispatcher会从 cache 中取缓存结果。
3. 流程图
volley 请求流程图
其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用requestqueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送http请求,解析响应结果,写入缓存,并回调主线程。
4. 源码分析
使用volley的第一步,首先要调用volley.newrequestqueue(context)方法来获取一个requestqueue对象,那么我们自然要从这个方法开始看起了,代码如下所示:
public static requestqueue newrequestqueue(context context) { return newrequestqueue(context, null); } public static requestqueue newrequestqueue(context context, httpstack stack) { file cachedir = new file(context.getcachedir(), default_cache_dir); string useragent = "volley/0"; try { string packagename = context.getpackagename(); packageinfo info = context.getpackagemanager().getpackageinfo(packagename, 0); useragent = packagename + "/" + info.versioncode; } catch (namenotfoundexception e) { } //如果stack是等于null的,则去创建一个httpstack对象,手机系统版本号是大于9的,则创建一个hurlstack的实例,否则就创建一个httpclientstack的实例,hurlstack的内部就是使用httpurlconnection进行网络通讯的,而httpclientstack的内部则是使用httpclient进行网络通讯的 if (stack == null) { if (build.version.sdk_int >= 9) { stack = new hurlstack(); } else { stack = new httpclientstack(androidhttpclient.newinstance(useragent)); } } //创建了一个network对象,它是用于根据传入的httpstack对象来处理网络请求的 network network = new basicnetwork(stack); requestqueue queue = new requestqueue(new diskbasedcache(cachedir), network); queue.start(); return queue; }
最终会走到requestqueue的start()方法,然后将requestqueue返回。去看看requestqueue的start()方法内部到底执行了什么?
public void start() { stop(); // make sure any currently running dispatchers are stopped. //先是创建了一个cachedispatcher的实例,然后调用了它的start()方法 mcachedispatcher = new cachedispatcher(mcachequeue, mnetworkqueue, mcache, mdelivery); mcachedispatcher.start(); //for循环创建networkdispatcher的实例,并分别调用它们的start()方法 for (int i = 0; i < mdispatchers.length; i++) { networkdispatcher networkdispatcher = new networkdispatcher(mnetworkqueue, mnetwork, mcache, mdelivery); mdispatchers[i] = networkdispatcher; networkdispatcher.start(); } }
cachedispatcher和networkdispatcher都是继承自thread的,而默认情况下for循环会执行四次,也就是说当调用了volley.newrequestqueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来,其中cachedispatcher是缓存线程,networkdispatcher是网络请求线程。
得到了requestqueue之后,我们只需要构建出相应的request,然后调用requestqueue的add()方法将request传入就可以完成网络请求操作了,来看看add()方法吧:
public <t> request<t> add(request<t> request) { // tag the request as belonging to this queue and add it to the set of current requests. request.setrequestqueue(this); synchronized (mcurrentrequests) { mcurrentrequests.add(request); } // process requests in the order they are added. request.setsequence(getsequencenumber()); request.addmarker("add-to-queue"); //判断当前的请求是否可以缓存,如果不能缓存则直接将这条请求加入网络请求队列 if (!request.shouldcache()) { mnetworkqueue.add(request); return request; } // insert request into stage if there's already a request with the same cache key in flight. synchronized (mwaitingrequests) { string cachekey = request.getcachekey(); if (mwaitingrequests.containskey(cachekey)) { // there is already a request in flight. queue up. queue<request<?>> stagedrequests = mwaitingrequests.get(cachekey); if (stagedrequests == null) { stagedrequests = new linkedlist<request<?>>(); } stagedrequests.add(request); mwaitingrequests.put(cachekey, stagedrequests); if (volleylog.debug) { volleylog.v("request for cachekey=%s is in flight, putting on hold.", cachekey); } } else { //当前的请求可以缓存的话则将这条请求加入缓存队列 mwaitingrequests.put(cachekey, null); mcachequeue.add(request); } return request; } }
在默认情况下,每条请求都是可以缓存的,当然我们也可以调用request的setshouldcache(false)方法来改变这一默认行为。既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们看下cachedispatcher中的run()方法
public class cachedispatcher extends thread { …… @override public void run() { if (debug) volleylog.v("start new dispatcher"); process.setthreadpriority(process.thread_priority_background); // make a blocking call to initialize the cache. mcache.initialize(); while (true) { try { // get a request from the cache triage queue, blocking until // at least one is available. final request<?> request = mcachequeue.take(); request.addmarker("cache-queue-take"); // if the request has been canceled, don't bother dispatching it. if (request.iscanceled()) { request.finish("cache-discard-canceled"); continue; } //尝试从缓存当中取出响应结果 cache.entry entry = mcache.get(request.getcachekey()); if (entry == null) { request.addmarker("cache-miss"); // 如何为空的话则把这条请求加入到网络请求队列中 mnetworkqueue.put(request); continue; } // 如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中 if (entry.isexpired()) { request.addmarker("cache-hit-expired"); request.setcacheentry(entry); mnetworkqueue.put(request); continue; } //没有过期就认为不需要重发网络请求,直接使用缓存中的数据即可 request.addmarker("cache-hit"); //对数据进行解析 response<?> response = request.parsenetworkresponse( new networkresponse(entry.data, entry.responseheaders)); request.addmarker("cache-hit-parsed"); if (!entry.refreshneeded()) { // completely unexpired cache hit. just deliver the response. mdelivery.postresponse(request, response); } else { // soft-expired cache hit. we can deliver the cached response, // but we need to also send the request to the network for // refreshing. request.addmarker("cache-hit-refresh-needed"); request.setcacheentry(entry); // mark the response as intermediate. response.intermediate = true; // post the intermediate response back to the user and have // the delivery then forward the request along to the network. mdelivery.postresponse(request, response, new runnable() { @override public void run() { try { mnetworkqueue.put(request); } catch (interruptedexception e) { // not much we can do about this. } } }); } } catch (interruptedexception e) { // we may have been interrupted because it was time to quit. if (mquit) { return; } continue; } } } }
来看一下networkdispatcher中是怎么处理网络请求队列的
public class networkdispatcher extends thread { …… @override public void run() { process.setthreadpriority(process.thread_priority_background); request<?> request; while (true) { try { // take a request from the queue. request = mqueue.take(); } catch (interruptedexception e) { // we may have been interrupted because it was time to quit. if (mquit) { return; } continue; } try { request.addmarker("network-queue-take"); // if the request was cancelled already, do not perform the // network request. if (request.iscanceled()) { request.finish("network-discard-cancelled"); continue; } addtrafficstatstag(request); //调用network的performrequest()方法来去发送网络请求 networkresponse networkresponse = mnetwork.performrequest(request); request.addmarker("network-http-complete"); // if the server returned 304 and we delivered a response already, // we're done -- don't deliver a second identical response. if (networkresponse.notmodified && request.hashadresponsedelivered()) { request.finish("not-modified"); continue; } // parse the response here on the worker thread. response<?> response = request.parsenetworkresponse(networkresponse); request.addmarker("network-parse-complete"); // write to cache if applicable. // todo: only update cache metadata instead of entire record for 304s. if (request.shouldcache() && response.cacheentry != null) { mcache.put(request.getcachekey(), response.cacheentry); request.addmarker("network-cache-written"); } // post the response back. request.markdelivered(); mdelivery.postresponse(request, response); } catch (volleyerror volleyerror) { parseanddelivernetworkerror(request, volleyerror); } catch (exception e) { volleylog.e(e, "unhandled exception %s", e.tostring()); mdelivery.posterror(request, new volleyerror(e)); } } } }
调用network的performrequest()方法来去发送网络请求 ,而network是一个接口,这里具体的实现是basicnetwork,我们来看下它的performrequest()方法
public class basicnetwork implements network { …… @override public networkresponse performrequest(request<?> request) throws volleyerror { long requeststart = systemclock.elapsedrealtime(); while (true) { httpresponse httpresponse = null; byte[] responsecontents = null; map<string, string> responseheaders = new hashmap<string, string>(); try { // gather headers. map<string, string> headers = new hashmap<string, string>(); addcacheheaders(headers, request.getcacheentry()); //调用了httpstack的performrequest()方法,这里的httpstack就是在一开始调用newrequestqueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的hurlstack对象,否则创建httpclientstack对象 httpresponse = mhttpstack.performrequest(request, headers); statusline statusline = httpresponse.getstatusline(); int statuscode = statusline.getstatuscode(); responseheaders = convertheaders(httpresponse.getallheaders()); // handle cache validation. if (statuscode == httpstatus.sc_not_modified) { //将服务器返回的数据组装成一个networkresponse对象进行返回 return new networkresponse(httpstatus.sc_not_modified, request.getcacheentry() == null ? null : request.getcacheentry().data, responseheaders, true); } // some responses such as 204s do not have content. we must check. if (httpresponse.getentity() != null) { responsecontents = entitytobytes(httpresponse.getentity()); } else { // add 0 byte response as a way of honestly representing a // no-content request. responsecontents = new byte[0]; } // if the request is slow, log it. long requestlifetime = systemclock.elapsedrealtime() - requeststart; logslowrequests(requestlifetime, request, responsecontents, statusline); if (statuscode < 200 || statuscode > 299) { throw new ioexception(); } return new networkresponse(statuscode, responsecontents, responseheaders, false); } catch (exception e) { …… } } } }
在networkdispatcher中收到了networkresponse这个返回值后又会调用request的parsenetworkresponse()方法来解析networkresponse中的数据,以及将数据写入到缓存,这个方法的实现是交给request的子类来完成的,因为不同种类的request解析的方式也肯定不同。还记得自定义request的方式吗?其中parsenetworkresponse()这个方法就是必须要重写的。
在解析完了networkresponse中的数据之后,又会调用executordelivery的postresponse()方法来回调解析出的数据
public void postresponse(request<?> request, response<?> response, runnable runnable) { request.markdelivered(); request.addmarker("post-response"); mresponseposter.execute(new responsedeliveryrunnable(request, response, runnable)); }
在mresponseposter的execute()方法中传入了一个responsedeliveryrunnable对象,就可以保证该对象中的run()方法就是在主线程当中运行的了,我们看下run()方法中的代码是什么样的:
private class responsedeliveryrunnable implements runnable { private final request mrequest; private final response mresponse; private final runnable mrunnable; public responsedeliveryrunnable(request request, response response, runnable runnable) { mrequest = request; mresponse = response; mrunnable = runnable; } @suppresswarnings("unchecked") @override public void run() { // if this request has canceled, finish it and don't deliver. if (mrequest.iscanceled()) { mrequest.finish("canceled-at-delivery"); return; } // deliver a normal response or error, depending. if (mresponse.issuccess()) { mrequest.deliverresponse(mresponse.result); } else { mrequest.delivererror(mresponse.error); } // if this is an intermediate response, add a marker, otherwise we're done // and the request can be finished. if (mresponse.intermediate) { mrequest.addmarker("intermediate-response"); } else { mrequest.finish("done"); } // if we have been provided a post-delivery runnable, run it. if (mrunnable != null) { mrunnable.run(); } } }
其中在第22行调用了request的deliverresponse()方法,有没有感觉很熟悉?没错,这个就是我们在自定义request时需要重写的另外一个方法,每一条网络请求的响应都是回调到这个方法中,最后我们再在这个方法中将响应的数据回调到response.listener的onresponse()方法中就可以了。
下一篇: Java垃圾回收之复制算法详解