欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android Volley框架全面解析

程序员文章站 2024-03-06 08:36:07
 volley简介 我们平时在开发android应用的时候不可避免地都需要用到网络技术,而多数情况下应用程序都会使用http协议来发送和接收网络数据。andro...

 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>,然后根据不同的响应数据来进行特殊的处理。

Android Volley框架全面解析

来看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展示出来:

Android Volley框架全面解析 

没错,百度返回给我们的就是这样一长串的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)

上面我们请求的地址是*天气预报的上海天气,看一下运行效果:

Android Volley框架全面解析 

可以看出,服务器返回给我们的数据确实是json格式的,并且onresponse()方法中携带的参数也正是一个jsonobject对象,之后只需要从jsonobject对象取出我们想要得到的那部分数据就可以了。

Android Volley框架全面解析

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);

看运行效果图:

Android Volley框架全面解析

加载图片— 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再次加载该图片,会很快显示出来而看不到默认的加载过程中图片,这是因为这次的图片是从缓存中取的,速度很快。效果如下图所示。

Android Volley框架全面解析

注:上面我们只是定制了内存缓存,查看源码,可以发现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格式进行返回,如下所示:

Android Volley框架全面解析

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);

Android Volley框架全面解析

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如下:

Android Volley框架全面解析

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. 总体设计图

Android Volley框架全面解析 

上面是 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 请求流程图

Android 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()方法中就可以了。