参考了郭霖大神的系列文章,将原作的4篇博客汇总了一下,阅读起来比较方便。
原文地址:http://blog.csdn.net/guolin_blog/article/details/17482095
1. Volley简介
HttpURLConnection和HttpClient的用法还是稍微有些复杂的,如果不进行适当封装的话,很容易就会写出不少重复代码。于是乎,一些Android网络通信框架也就应运而生,比如说AsyncHttpClient,它把HTTP所有的通信细节全部封装在了内部,我们只需要简单调用几行代码就可以完成通信操作了。再比如Universal-Image-Loader,它使得在界面上显示网络图片的操作变得极度简单,开发者不用关心如何从网络上获取图片,也不用关心开启线程、回收图片资源等细节,Universal-Image-Loader已经把一切都做好了。
Android开发团队也是意识到了有必要将HTTP的通信操作再进行简单化,于是在2013年GoogleI/O大会上推出了一个新的网络通信框架——Volley。Volley可是说是把AsyncHttpClient和Universal-Image-Loader的优点集于了一身,既可以像AsyncHttpClient一样非常简单地进行HTTP通信,也可以像Universal-Image-Loader一样轻松加载网络上的图片。除了简单易用之外,Volley在性能方面也进行了大幅度的调整,它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。
2.StringRequest的用法
Volley的用法非常简单,那么我们就从最基本的HTTP通信开始学习吧,即发起一条HTTP请求,然后接收HTTP响应。
首先需要获取到一个RequestQueue对象,可以调用如下方法获取到:
RequestQueuemQueue = Volley.newRequestQueue(context);
注意这里拿到的RequestQueue是一个请求队列对象,它可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求。RequestQueue内部的设计就是非常合适高并发的,因此我们不必为每一次HTTP请求都创建一个RequestQueue对象,这是非常浪费资源的,基本上在每一个需要和网络交互的Activity中创建一个RequestQueue对象就足够了。
接下来为了要发出一条HTTP请求,我们还需要创建一个StringRequest对象,如下所示:
StringRequeststringRequest = new StringRequest("http://www.baidu.com",
newResponse.Listener<String>() {
@Override
public voidonResponse(String response) {
Log.d("TAG", response);
}
}, newResponse.ErrorListener() {
@Override
public voidonErrorResponse(VolleyError error) {
Log.e("TAG", error.getMessage(), error);
}
});
可以看到,这里new出了一个StringRequest对象,StringRequest的构造函数需要传入三个参数,第一个参数就是目标服务器的URL地址,第二个参数是服务器响应成功的回调,第三个参数是服务器响应失败的回调。其中,目标服务器地址我们填写的是百度的首页,然后在响应成功的回调里打印出服务器返回的内容,在响应失败的回调里打印出失败的详细信息。
最后,将这个StringRequest对象添加到RequestQueue里面就可以了,如下所示:
mQueue.add(stringRequest);
一个最基本的HTTP发送与响应的功能就完成了。你会发现根本还没写几行代码就轻易实现了这个功能,主要就是进行了以下三步操作:
1. 创建一个RequestQueue对象。
2. 创建一个StringRequest对象。
3. 将StringRequest对象添加到RequestQueue里面。
不过大家都知道,HTTP的请求类型通常有两种,GET和POST,刚才我们使用的明显是一个GET请求,那么如果想要发出一条POST请求应该怎么做呢?StringRequest中还提供了另外一种四个参数的构造函数,其中第一个参数就是指定请求类型的,我们可以使用如下方式进
StringRequeststringRequest = new StringRequest(Method.POST, url,listener,errorListener);
可是这只是指定了HTTP请求方式是POST,那么我们要提交给服务器的参数又该怎么设置呢?很遗憾,StringRequest中并没有提供设置POST参数的方法,但是当发出POST请求的时候,Volley会尝试调用StringRequest的父类——Request中的getParams()方法来获取POST参数,那么解决方法自然也就有了,我们只需要在StringRequest的匿名类中重写getParams()方法,在这里设置POST参数就可以了,代码如下所示:
StringRequest stringRequest = newStringRequest(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;
}
};
你可能会说,每次都这样用起来岂不是很累?连个设置POST参数的方法都没有。但是不要忘记,Volley是开源的,只要你愿意,你可以自由地在里面添加和修改任何的方法,轻松就能定制出一个属于你自己的Volley版本。
3. JsonRequest的用法
类似于StringRequest,JsonRequest也是继承自Request类的,不过由于JsonRequest是一个抽象类,因此我们无法直接创建它的实例,那么只能从它的子类入手了。JsonRequest有两个直接的子类,JsonObjectRequest和JsonArrayRequest,从名字上你应该能就看出它们的区别了吧?一个是用于请求一段JSON数据的,一个是用于请求一段JSON数组的。
至于它们的用法也基本上没有什么特殊之处,先new出一个JsonObjectRequest对象,如下所示:
JsonObjectRequest jsonObjectRequest =
newJsonObjectRequest("http://m.weather.com.cn/data/101010100.html",null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
Log.d("TAG",response.toString());
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG",error.getMessage(), error);
}
});
这里我们填写的URL地址是http://m.weather.com.cn/data/101010100.html,这是中国天气网提供的一个查询天气信息的接口,响应的数据就是以JSON格式返回的,最后再将这个JsonObjectRequest对象添加到RequestQueue里就可以了,如下所示:
mQueue.add(jsonObjectRequest);
这样当HTTP通信完成之后,服务器响应的天气信息就会回调到onResponse()方法中,并打印出来。
4. ImageRequest的用法
ImageRequest也是继承自Request的,因此它的用法也是基本相同的,首先需要获取到一个RequestQueue对象,可以调用如下方法获取到:
RequestQueuemQueue = Volley.newRequestQueue(context);
接下来new出一个ImageRequest对象代码如下所示:
ImageRequest imageRequest = newImageRequest(
"http://developer.android.com/images/home/aw_dac.png",
new Response.Listener<Bitmap>() {
@Override
public void onResponse(Bitmap response) {
imageView.setImageBitmap(response);
}
}, 0, 0, Config.RGB_565,
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
imageView.setImageResource(R.drawable.default_image);
}
});
可以看到,ImageRequest的构造函数接收六个参数,第一个参数就是图片的URL地址,这个没什么需要解释的。第二个参数是图片请求成功的回调,这里我们把返回的Bitmap参数设置到ImageView中。第三第四个参数分别用于指定允许图片最大的宽度和高度,如果指定的网络图片的宽度或高度大于这里的最大值,则会对图片进行压缩,指定成0的话就表示不管图片有多大,都不会进行压缩。第五个参数用于指定图片的颜色属性,Bitmap.Config下的几个常量都可以在这里使用,其中ARGB_8888可以展示最好的颜色属性,每个图片像素占据4个字节的大小,而RGB_565则表示每个图片像素占据2个字节大小。第六个参数是图片请求失败的回调,这里我们当请求失败时在ImageView中显示一张默认图片。
最后将这个ImageRequest对象添加到RequestQueue里就可以了,如下所示:
mQueue.add(imageRequest);
现在如果运行一下程序,并尝试发出这样一条网络请求,很快就能看到网络上的图片在ImageView中显示出来了。
5. ImageLoader的用法
Volley在请求网络图片方面可以做到的还远远不止这些, ImageLoader也可以用于加载网络上的图片,并且它的内部也是使用ImageRequest来实现的,不过ImageLoader明显要比ImageRequest更加高效,因为它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求。
由于ImageLoader已经不是继承自Request的了,所以它的用法也和我们之前学到的内容有所不同,总结起来大致可以分为以下四步:
1. 创建一个RequestQueue对象。
2. 创建一个ImageLoader对象。
3. 获取一个ImageListener对象。
4. 调用ImageLoader的get()方法加载网络上的图片。
下面我们就来按照这个步骤,学习一下ImageLoader的用法吧。首先第一步的创建RequestQueue对象我们已经写过很多遍了,相信已经不用再重复介绍了,那么就从第二步开始学习吧,新建一个ImageLoader对象,代码如下所示:
ImageLoader imageLoader = newImageLoader(mQueue, new ImageCache() {
@Override
public void putBitmap(String url, Bitmap bitmap) {
}
@Override
public Bitmap getBitmap(String url) {
return null;
}
});
可以看到,ImageLoader的构造函数接收两个参数,第一个参数就是RequestQueue对象,第二个参数是一个ImageCache对象,这里我们先new出一个空的ImageCache的实现即可。
接下来需要获取一个ImageListener对象,代码如下所示:
ImageListener listener =ImageLoader.getImageListener(imageView,
R.drawable.default_image, R.drawable.failed_image);
我们通过调用ImageLoader的getImageListener()方法能够获取到一个ImageListener对象,getImageListener()方法接收三个参数,第一个参数指定用于显示图片的ImageView控件,第二个参数指定加载图片的过程中显示的图片,第三个参数指定加载图片失败的情况下显示的图片。
最后,调用ImageLoader的get()方法来加载图片,代码如下所示:
imageLoader.get("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",listener);
get()方法接收两个参数,第一个参数就是图片的URL地址,第二个参数则是刚刚获取到的ImageListener对象。当然,如果你想对图片的大小进行限制,也可以使用get()方法的重载,指定图片允许的最大宽度和高度,如下所示:
imageLoader.get("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",listener, 200, 200);
现在运行一下程序并开始加载图片,你将看到ImageView中会先显示一张默认的图片,等到网络上的图片加载完成后,ImageView则会自动显示该图。
虽然现在我们已经掌握了ImageLoader的用法,但是刚才介绍的ImageLoader的优点却还没有使用到。为什么呢?因为这里创建的ImageCache对象是一个空的实现,完全没能起到图片缓存的作用。其实写一个ImageCache也非常简单,但是如果想要写一个性能非常好的ImageCache,最好就要借助Android提供的LruCache功能了,如果你对LruCache还不了解,可以参考我之前的一篇博客Android高效加载大图、多图解决方案,有效避免程序OOM(博客地址是:http://blog.csdn.net/guolin_blog/article/details/9316683)。
这里我们新建一个BitmapCache并实现了ImageCache接口,如下所示:
public class BitmapCache implementsImageCache {
private LruCache<String, Bitmap> mCache;
public BitmapCache() {
int maxSize = 10 * 1024 * 1024;
mCache = new LruCache<String, Bitmap>(maxSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() *bitmap.getHeight();
}
};
}
@Override
public Bitmap getBitmap(String url) {
return mCache.get(url);
}
@Override
public void putBitmap(String url, Bitmap bitmap) {
mCache.put(url, bitmap);
}
}
可以看到,这里我们将缓存图片的大小设置为10M。重写的sizeOf方法是判定一个图像的尺寸,这里使用bitmap.getRowBytes() * bitmap.getHeight()与使用bitmap.getByteCount()是一样的(getByteCount方法的源码就是直接返回getRowBytes()* getHeight())。
接着修改创建ImageLoader实例的代码,第二个参数传入BitmapCache的实例,如下所示:
ImageLoader imageLoader = newImageLoader(mQueue, new BitmapCache());
这样我们就把ImageLoader的功能优势充分利用起来了。
6.NetworkImageView的用法
除了以上两种方式之外,Volley还提供了第三种方式来加载网络图片,即使用NetworkImageView。不同于以上两种方式,NetworkImageView是一个自定义控制,它是继承自ImageView的,具备ImageView控件的所有功能,并且在原生的基础之上加入了加载网络图片的功能。NetworkImageView控件的用法要比前两种方式更加简单,大致可以分为以下五步:
1. 创建一个RequestQueue对象。
2. 创建一个ImageLoader对象。
3. 在布局文件中添加一个NetworkImageView控件。
4. 在代码中获取该控件的实例。
5. 设置要加载的图片地址。
其中,第一第二步和ImageLoader的用法是完全一样的,因此这里我们就从第三步开始学习了。首先修改布局文件中的代码,在里面加入NetworkImageView控件,如下所示:
<LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Send Request" />
<com.android.volley.toolbox.NetworkImageView
android:id="@+id/network_image_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="center_horizontal"
/>
</LinearLayout>
接着在Activity获取到这个控件的实例,这就非常简单了,代码如下所示:
networkImageView = (NetworkImageView)findViewById(R.id.network_image_view);
得到了NetworkImageView控件的实例之后,我们可以调用它的setDefaultImageResId()方法、setErrorImageResId()方法和setImageUrl()方法来分别设置加载中显示的图片,加载失败时显示的图片,以及目标图片的URL地址,如下所示:
networkImageView.setDefaultImageResId(R.drawable.default_image);
networkImageView.setErrorImageResId(R.drawable.failed_image);
networkImageView.setImageUrl("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg",imageLoader);
其中,setImageUrl()方法接收两个参数,第一个参数用于指定图片的URL地址,第二个参数则是前面创建好的ImageLoader对象。
好了,就是这么简单,现在重新运行一下程序,你将看到和使用ImageLoader来加载图片一模一样的效果。
这时有的朋友可能就会问了,使用ImageRequest和ImageLoader这两种方式来加载网络图片,都可以传入一个最大宽度和高度的参数来对图片进行压缩,而NetworkImageView中则完全没有提供设置最大宽度和高度的方法,那么是不是使用NetworkImageView来加载的图片都不会进行压缩呢?其实并不是这样的,NetworkImageView并不需要提供任何设置最大宽高的方法也能够对加载的图片进行压缩。这是由于NetworkImageView是一个控件,在加载图片的时候它会自动获取自身的宽高,然后对比网络图片的宽度,再决定是否需要对图片进行压缩。也就是说,压缩过程是在内部完全自动化的,并不需要我们关心,NetworkImageView会始终呈现给我们一张大小刚刚好的网络图片,不会多占用任何一点内存,这也是NetworkImageView最简单好用的一点吧。当然了,如果你不想对图片进行压缩的话,其实也很简单,只需要在布局文件中把NetworkImageView的layout_width和layout_height都设置成wrap_content就可以了,这样NetworkImageView就会将该图片的原始大小展示出来,不会进行任何压缩。
7.自定义XMLRequest
我们已经掌握了Volley各种Request的使用方法,包括StringRequest、JsonRequest、ImageRequest等。其中StringRequest用于请求一条普通的文本数据,JsonRequest(JsonObjectRequest、JsonArrayRequest)用于请求一条JSON格式的数据,ImageRequest则是用于请求网络上的一张图片。可是Volley提供给我们的Request类型就只有这么多,而我们都知道,在网络上传输的数据通常有两种格式,JSON和XML,那么如果想要请求一条XML格式的数据该怎么办呢?其实很简单,Volley提供了非常强的扩展机制,使得我们可以很轻松地定制出任意类型的Request,这也就是本篇文章的主题了。
参考一下Volley的源码,看一看StringRequest是怎么实现的,然后就可以模仿着写出XMLRequest了。首先看下StringRequest的源码,如下所示:
/**
* Acanned request for retrieving the response body at a given URL as a String.
*/
public class StringRequest extendsRequest<String> {
private final Listener<String> mListener;
/**
* Creates a new request with the given method.
*
* @param method the request {@link Method} to use
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
public StringRequest(int method, String url, Listener<String>listener,
ErrorListener errorListener) {
super(method, url, errorListener);
mListener = listener;
}
/**
* Creates a new GET request.
*
* @param url URL to fetch the string at
* @param listener Listener to receive the String response
* @param errorListener Error listener, or null to ignore errors
*/
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(NetworkResponseresponse) {
String parsed;
try {
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的源码很简练,根本就没几行代码,我们一起来分析下。
首先StringRequest是继承自Request类的,Request可以指定一个泛型类,这里指定的当然就是String了,接下来StringRequest中提供了两个有参的构造函数,参数包括请求类型,请求地址,以及响应回调等,由于我们已经很熟悉StringRequest的用法了,相信这几个参数的作用都不用再解释了吧。但需要注意的是,在构造函数中一定要调用super()方法将这几个参数传给父类,因为HTTP的请求和响应都是在父类中自动处理的。
另外,由于Request类中的deliverResponse()和parseNetworkResponse()是两个抽象方法,因此StringRequest中需要对这两个方法进行实现。deliverResponse()方法中的实现很简单,仅仅是调用了mListener中的onResponse()方法,并将response内容传入即可,这样就可以将服务器响应的数据进行回调了。parseNetworkResponse()方法中则应该对服务器响应的数据进行解析,其中数据是以字节的形式存放在NetworkResponse的data变量中的,这里将数据取出然后组装成一个String,并传入Response的success()方法中即可。
了解了StringRequest的实现原理,下面我们就可以动手来尝试实现一下XMLRequest了,代码如下所示:
public class XMLRequest extendsRequest<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 xmlRequest = newXMLRequest(
"http://flash.weather.com.cn/wmaps/xml/china.xml",
new Response.Listener<XmlPullParser>() {
@Override
public voidonResponse(XmlPullParser response) {
try {
int eventType =response.getEventType();
while (eventType !=XmlPullParser.END_DOCUMENT) {
switch (eventType){
caseXmlPullParser.START_TAG:
String nodeName =response.getName();
if("city".equals(nodeName)) {
String pName =response.getAttributeValue(0);
Log.d("TAG", "pName is " + pName);
}
break;
}
eventType =response.next();
}
} catch (XmlPullParserExceptione) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG",error.getMessage(), error);
}
});
mQueue.add(xmlRequest);
可以看到,这里XMLRequest的用法和StringRequest几乎是一模一样的,我们先创建出一个XMLRequest的实例,并把服务器接口地址传入,然后在onResponse()方法中解析响应的XML数据,并把每个省的名字打印出来,最后将这个XMLRequest添加到RequestQueue当中。
8.自定义GsonRequest
JsonRequest的数据解析是利用Android本身自带的JSONObject和JSONArray来实现的,配合使用JSONObject和JSONArray就可以解析出任意格式的JSON数据。不过也许你会觉得使用JSONObject还是太麻烦了,还有很多方法可以让JSON数据解析变得更加简单,比如说GSON。遗憾的是,Volley中默认并不支持使用自家的GSON来解析数据,不过没有关系,通过上面的学习,相信你已经知道了自定义一个Request是多么的简单,那么下面我们就来举一反三一下,自定义一个GsonRequest。
public class GsonRequest<T> extendsRequest<T> {
private final Listener<T> mListener;
privateGson 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(NetworkResponseresponse) {
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/101010100.html这个接口可以得到一段JSON格式的天气数据,如下所示:
{"weatherinfo":{"city":"北京","cityid":"101010100","temp":"19","WD":"南风","WS":"2级","SD":"43%","WSE":"2","time":"19:45","isRadar":"1","Radar":"JC_RADAR_AZ9010_JB"}}
接下来我们使用对象的方式将这段JSON字符串表示出来。新建一个Weather类,代码如下所示:
public class Weather {
private WeatherInfo weatherinfo;
public WeatherInfo getWeatherinfo() {
return weatherinfo;
}
public void setWeatherinfo(WeatherInfo weatherinfo) {
this.weatherinfo = weatherinfo;
}
}
Weather类中只是引用了WeatherInfo这个类。接着新建WeatherInfo类,代码如下所示:
public class WeatherInfo {
private String city;
private String temp;
private String time;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getTemp() {
return temp;
}
public void setTemp(String temp) {
this.temp = temp;
}
public String getTime() {
return time;
}
public void setTime(String time) {
this.time = time;
}
}
WeatherInfo类中含有city、temp、time这几个字段。下面就是如何调用GsonRequest了,其实也很简单,代码如下所示:
GsonRequest<Weather> gsonRequest =new GsonRequest<Weather>(
"http://www.weather.com.cn/data/sk/101010100.html",Weather.class, new Response.Listener<Weather>() {
@Override
public void onResponse(Weather weather) {
WeatherInfo weatherInfo =weather.getWeatherinfo();
Log.d("TAG","city is " + weatherInfo.getCity());
Log.d("TAG","temp is " + weatherInfo.getTemp());
Log.d("TAG","time is " + weatherInfo.getTime());
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.e("TAG",error.getMessage(), error);
}
});
mQueue.add(gsonRequest);
可以看到,这里onResponse()方法的回调中直接返回了一个Weather对象,我们通过它就可以得到WeatherInfo对象,接着就能从中取出JSON中的相关数据了。
9.自定义的MyJsonRequest
在使用JsonObjectRequest对象提交一个post请求时,如果有参数需要提交,则该参数会以JSONObject的json串格式提交。但并不是所有的服务器都能接受这样格式的请求参数。此时,我们就写一个自定义的Request,以普通的方式提交参数,将接收到的服务器返回的数据变为JsonObject。
public class MyJsonRequest extendsRequest<JSONObject>{
privateListener<JSONObject> mListener;
publicMyJsonRequest(int method, String url, Listener<JSONObject>listener,ErrorListener errListener) {
super(method,url, errListener);
this.mListener= listener;
}
publicMyJsonRequest(String url, Listener<JSONObject> listener,ErrorListenererrListener) {
super(Method.GET,url, errListener);
this.mListener= listener;
}
@Override
protectedvoid deliverResponse(JSONObject arg0) {
mListener.onResponse(arg0);
}
@Override
protectedResponse<JSONObject> parseNetworkResponse(NetworkResponse arg0) {
try{
StringjsonString = new String(
arg0.data,
HttpHeaderParser.parseCharset(arg0.headers)
);
EntrycacheEntry = HttpHeaderParser.parseCacheHeaders(arg0);
returnResponse.success(new JSONObject(jsonString), cacheEntry );
}catch (UnsupportedEncodingException e) {
returnResponse.error(new ParseError(e));
}catch (JSONException e2) {
returnResponse.error(new ParseError(e2));
}
}
}
然后使用起来的方式与其它Request是一样的:
String url ="http://apis.juhe.cn/mobile/get";
MyJsonRequestmjr = new MyJsonRequest(Method.POST, url , newListener<JSONObject>() {
@Override
publicvoid onResponse(JSONObject arg0) {
Toast.makeText(MainActivity.this,arg0.toString(), Toast.LENGTH_SHORT).show();
}
},new ErrorListener() {
@Override
publicvoid onErrorResponse(VolleyError arg0) {
Toast.makeText(MainActivity.this,arg0.getMessage(), Toast.LENGTH_SHORT).show();
}
}){
@Override
protectedMap<String, String> getParams() throws AuthFailureError {
Map<String,String>maps = new HashMap<String, String>();
maps.put("key","申请的APIKEY");
maps.put("phone","1380013");
returnmaps;
}
};
mjr.setTag("myjsonrequest");
MyApplication.getRequestQueue().add(mjr);
10 . 从源码的角度理解 Volley
Volley的官方文档中本身就附有了一张Volley的工作流程图:
使用Volley的第一步,首先要调用Volley.newRequestQueue(context)方法来获取一个RequestQueue对象,那么我们自然要从这个方法开始看起了,代码如下所示:
public static RequestQueuenewRequestQueue(Context context) {
return newRequestQueue(context, null);
}
这个方法仅仅只有一行代码,只是调用了newRequestQueue()的方法重载,并给第二个参数传入null。那我们看下带有两个参数的newRequestQueue()方法中的代码,如下所示:
public static RequestQueuenewRequestQueue(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) { }
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
stack = newHttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir),network);
queue.start();
return queue;
}
可以看到,这里在第10行判断如果stack是等于null的,则去创建一个HttpStack对象,这里会判断如果手机系统版本号是大于9的,则创建一个HurlStack的实例,否则就创建一个HttpClientStack的实例。实际上HurlStack的内部就是使用HttpURLConnection进行网络通讯的,而HttpClientStack的内部则是使用HttpClient进行网络通讯的,这里为什么这样选择呢?可以参考我之前翻译的一篇文章Android访问网络,使用HttpURLConnection还是HttpClient?(blog地址:http://blog.csdn.net/guolin_blog/article/details/12452307)
创建好了HttpStack之后,接下来又创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的,紧接着new出一个RequestQueue对象,并调用它的start()方法进行启动,然后将RequestQueue返回,这样newRequestQueue()的方法就执行结束了。
那么RequestQueue的start()方法内部到底执行了什么东西呢?我们跟进去瞧一瞧:
public void start() {
stop(); // Make sure anycurrently running dispatchers are stopped.
// Create the cache dispatcher and start it.
mCacheDispatcher= new CacheDispatcher(mCacheQueue,
mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// Create network dispatchers (and corresponding threads) up to the poolsize.
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher =
new NetworkDispatcher(mNetworkQueue, mNetwork, mCache,mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
这里先是创建了一个CacheDispatcher的实例,然后调用了它的start()方法,接着在一个for循环里去创建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 ofcurrent 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 the request is uncacheable, skip the cache queue and go straightto the network.
if (!request.shouldCache()) {
mNetworkQueue.add(request);
return request;
}
// Insert request into stage if there's already a request with the samecache 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 = newLinkedList<Request<?>>();
}
stagedRequests.add(request);
mWaitingRequests.put(cacheKey, stagedRequests);
if (VolleyLog.DEBUG) {
VolleyLog.v("Request forcacheKey=
%s is in flight, putting on hold.", cacheKey);
}
} else {
// Insert 'null' queue for this cacheKey, indicating there is now arequest in
// flight.
mWaitingRequests.put(cacheKey, null);
mCacheQueue.add(request);
}
return request;
}
}
可以看到,在第11行的时候会判断当前的请求是否可以缓存,如果不能缓存则在第12行直接将这条请求加入网络请求队列,可以缓存的话则在第33行将这条请求加入缓存队列。在默认情况下,每条请求都是可以缓存的,当然我们也可以调用Request的setShouldCache(false)方法来改变这一默认行为。
OK,那么既然默认每条请求都是可以缓存的,自然就被添加到了缓存队列中,于是一直在后台等待的缓存线程就要开始运行起来了,我们看下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 cachetriage queue, blocking until
// at least one isavailable.
final Request<?> request= mCacheQueue.take();
request.addMarker("cache-queue-take");
// If the request has beencanceled, don't bother dispatching it.
if (request.isCanceled()){
request.finish("cache-discard-canceled");
continue;
}
// Attempt to retrieve thisitem from cache.
Cache.Entry entry =mCache.get(request.getCacheKey());
if (entry == null) {
request.addMarker("cache-miss");
// Cache miss; send off tothe network dispatcher.
mNetworkQueue.put(request);
continue;
}
// If it is completely expired,just send it to the network.
if (entry.isExpired()) {
request.addMarker("cache-hit-expired");
request.setCacheEntry(entry);
mNetworkQueue.put(request);
continue;
}
// We have a cache hit; parseits data for delivery back to the request.
request.addMarker("cache-hit");
Response<?> response =request.parseNetworkResponse(
newNetworkResponse(entry.data, entry.responseHeaders));
request.addMarker("cache-hit-parsed");
if (!entry.refreshNeeded()){
// Completely unexpiredcache 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 sendthe request to the network for
// refreshing.
request.addMarker("cache-hit-refresh-needed");
request.setCacheEntry(entry);
// Mark the response asintermediate.
response.intermediate =true;
// Post the intermediateresponse back to the user and have
// the delivery thenforward 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 wecan do about this.
}
}
});
}
} catch (InterruptedException e) {
// We may have been interruptedbecause it was time to quit.
if (mQuit) {
return;
}
continue;
}
}
}
}
代码有点长,我们只挑重点看。首先在11行可以看到一个while(true)循环,说明缓存线程始终是在运行的,接着在第23行会
尝试从缓存当中取出响应结果,如果为空的话则把这条请求加入到网络请求队列中,如果不为空的话再判断该缓存是否已过期,如果已经过期了则同样把这条请求加入到网络请求队列中,否则就认为不需要重发网络请求,直接使用缓存中的数据即可。
之后会在第39行调用Request的parseNetworkResponse()方法来对数据进行解析,再往后就是将解析出来的数据进行回调了,这部分代码我们先跳过,因为它的逻辑和NetworkDispatcher后半部分的逻辑是基本相同的,那么我们等下合并在一起看就好了,先来看一下NetworkDispatcher中是怎么处理网络请求队列的,代码如下所示:
public class NetworkDispatcher extendsThread {
……
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
Request<?> request;
while (true) {
try {
// Take a request from thequeue.
request = mQueue.take();
} catch (InterruptedException e) {
// We may have been interruptedbecause it was time to quit.
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
// If the request was cancelledalready, do not perform the
// network request.
if (request.isCanceled()){
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
// Perform the networkrequest.
NetworkResponse networkResponse= mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// If the server returned 304AND we delivered a response already,
// we're done -- don't delivera second identical response.
if (networkResponse.notModified&& request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
// Parse the response here onthe worker thread.
Response<?> response =request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// Write to cache ifapplicable.
// TODO: Only update cachemetadata 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, "Unhandledexception %s", e.toString());
mDelivery.postError(request,new VolleyError(e));
}
}
}
}
同样地,在第7行我们看到了类似的while(true)循环,说明网络请求线程也是在不断运行的。在第28行的时候会调用Network的performRequest()方法来去发送网络请求,而Network是一个接口,这里具体的实现是BasicNetwork,我们来看下它的performRequest()方法,如下所示:
public class BasicNetwork implementsNetwork {
……
@Override
public NetworkResponse performRequest(Request<?> request) throwsVolleyError {
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());
httpResponse =mHttpStack.performRequest(request, headers);
StatusLine statusLine =httpResponse.getStatusLine();
int statusCode =statusLine.getStatusCode();
responseHeaders =convertHeaders(httpResponse.getAllHeaders());
// Handle cachevalidation.
if (statusCode == HttpStatus.SC_NOT_MODIFIED){
return newNetworkResponse(HttpStatus.SC_NOT_MODIFIED,
request.getCacheEntry() == null ?
null :request.getCacheEntry().data,
responseHeaders,true);
}
// Some responses such as 204sdo not have content. We must check.
if (httpResponse.getEntity() !=null) {
responseContents =entityToBytes(httpResponse.getEntity());
} else {
// Add 0 byte response as away of honestly representing a
// no-content request.
responseContents = newbyte[0];
}
// if the request is slow, logit.
long requestLifetime =SystemClock.elapsedRealtime() - requestStart;
logSlowRequests(requestLifetime, request, responseContents,statusLine);
if (statusCode < 200 ||statusCode > 299) {
throw newIOException();
}
return newNetworkResponse(statusCode,
responseContents,responseHeaders, false);
} catch (Exception e) {
……
}
}
}
}
这段方法中大多都是一些网络请求细节方面的东西,我们并不需要太多关心,需要注意的是在第14行调用了HttpStack的performRequest()方法,这里的HttpStack就是在一开始调用newRequestQueue()方法是创建的实例,默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象。前面已经说过,这两个对象的内部实际就是分别使用HttpURLConnection和HttpClient来发送网络请求的,我们就不再跟进去阅读了,之后会将服务器返回的数据组装成一个NetworkResponse对象进行返回。
在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 ResponseDeliveryRunnableimplements Runnable {
private final Request mRequest;
private final Response mResponse;
private final Runnable mRunnable;
publicResponseDeliveryRunnable(
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'redone
// 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()方法中就可以了。
好了,到这里我们就把Volley的完整执行流程全部梳理了一遍,你是不是已经感觉已经很清晰了呢?对了,还记得在文章一开始的那张流程图吗,刚才还不能理解,现在我们再来重新看下这张图:
其中蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。