当前位置: 移动技术网 > IT编程>移动开发>Android > Android Volley框架全面解析

Android Volley框架全面解析

2019年07月24日  | 移动技术网IT编程  | 我要评论

我们约会吧杨宏志,刀尖txt,平和岛结希

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

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网