整理来源Android 网络通信框架Volley简介
郭霖: 初识Volley的基本用法
概念
2013年的Google I/O 大会上发布了Volley.
Volley是Android平台上的网络通信库, 能使网络通信更快, 更简单, 更健壮(仅是相对,非绝对).
Volley特别适合数据量不大但是通信频繁的场景, 而对于大数据量的网络操作, 比如说下载文件等, Volley的表现就会非常糟糕.
Volley提供的功能
- JSON,图像等的异步下载;
- 网络请求的排序(scheduling)(请求队列)
- 网络请求的优先级处理
- 缓存
- 多级别取消请求
- 和Activity和生命周期的联动(Activity结束时同时取消所有网络请求)
使用三部曲
- 创建对应的请求对象(StringRequest, JsonObjectRequest,JsonArrayRequest, ImageRequest四大请求)
- 创建请求队列:RequestQueue
- 发起网络请求:queue.add(request)
导入库
三种方式
1. 直接Project Structure中联网导入
2. 粘贴lib包
3. 导入jar包
API
基本示例
StringRequest stringRequest = new StringRequest(method,url,listener,errorlistener); //创建请求对象,method参数可不写,默认GET请求. RequestQueue mQueue = Volley.newRequestQueue(context); //获取请求队列对象,它可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求。 //RequestQueue内部的设计就是非常合适高并发的,因此我们不必为每一次HTTP请求都创建一个RequestQueue对象, //这是非常浪费资源的,基本上在每一个需要和网络交互的Activity中创建一个RequestQueue对象就足够了。 mQueue.add(stringRequest); //添加到请求队列
POST请求
StringRequest stringRequest = new StringRequest(Method.POST, url, listener, errorListener);
可是这只是指定了HTTP请求方式是POST,那么我们要提交给服务器的参数又该怎么设置呢?很遗憾,StringRequest中并没有提供设置POST参数的方法,但是当发出POST请求的时候,Volley会尝试调用StringRequest的父类——Request中的getParams()方法来获取POST参数,那么解决方法自然也就有了,我们只需要在StringRequest的匿名类中重写getParams()方法,在这里设置POST参数就可以了,代码如下所示:
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; } };
你可能会说,每次都这样用起来岂不是很累?连个设置POST参数的方法都没有。但是不要忘记,Volley是开源的,只要你愿意,你可以自由地在里面添加和修改任何的方法,轻松就能定制出一个属于你自己的Volley版本。
举一反三
学完了最基本的StringRequest的用法,我们再来进阶学习一下JsonRequest的用法。类似于StringRequest,JsonRequest也是继承自Request类的,不过由于JsonRequest是一个抽象类,因此我们无法直接创建它的实例,那么只能从它的子类入手了。JsonRequest有两个直接的子类,JsonObjectRequest和JsonArrayRequest,从名字上你应该能就看出它们的区别了吧?一个是用于请求一段JSON数据的,一个是用于请求一段JSON数组的。
JsonObjectRequest的用法和StringRequest的用法基本上是完全一样的, 区别就在于
new Response.Listener<String>() { @Override public void onResponse(String response) { Log.d("TAG", response); } } -------------------------------------------------------------- new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Log.d("TAG", response.toString()); } }
- 看出区别了吧?仅仅是参数不一样, Volley的易用之处也在这里体现出来了.
网络图片请求和缓存
在此之前, 我们需要先了解关于图片等其他的一些知识.
1. LRU: 是一种算法, Least Recently Used (近期最少使用)缩写, 用于图片的缓存.
2. LruCache: 把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。为什么使用LinkedHashMap作为存储结构? 图片缓存技术:
new LinkedHashMap<K, V>(0, 0.75f, true):
0, 0.75f, true
第一个参数:初始化容器大小
第二个参数:负载因子,比如初始化容器大小16,负载因子0.75,16 * 0.75=12,当容器有 12个元素时,自动增长。
第三个参数:true代表按照最近访问顺序排序(LruCache选择LinkedHashMap的核心原因),false按照插入顺序排序
3. Bitmap.Config下的几个常量:
Config参数类型说明
Config.ALPHA_8->8位->一个字节
Config.ARGB_4444:16位 2个字节
ARGB_8888 32位 4个字节
RGB_565 16位 2个字节
有一张图片 320 * 480像素,占用内存多大呢?
RGB_565: 320 * 480 * 2字节 = 307200/1024 kb
ARGB_8888: 320* 480 * 4 字节
图片格式选择:
Volley使用RGB_565
ImageRequest
前面我们已经学习过了StringRequest和JsonRequest的用法,并且总结出了它们的用法都是非常类似的,基本就是进行三部曲操作.
我们知道StringRequest和JsonRequest都是继承自Request的,所以它们的用法才会如此类似。那么ImageRequest也是继承自Request的,因此它的用法也是基本相同的.
RequestQueue mQueue = Volley.newRequestQueue(context);
ImageRequest imageRequest = new ImageRequest(
"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);
}
});
mQueue.add(imageRequest);
ImageLoader
ImageLoader明显要比ImageRequest更加高效,因为它不仅可以帮我们对图片进行缓存,还可以过滤掉重复的链接,避免重复发送请求。
由于ImageLoader已经不是继承自Request的了,所以它的用法也和我们之前学到的内容有所不同:
创建一个RequestQueue对象。
创建一个ImageLoader对象。
ImageLoader imageLoader = new ImageLoader(mQueue, new ImageCache() { @Override public void putBitmap(String url, Bitmap bitmap) { } @Override public Bitmap getBitmap(String url) { return null; } });
获取一个ImageListener对象。
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_image, R.drawable.failed_image);
调用ImageLoader的get()方法加载网络上的图片。
imageLoader.get("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg", listener); ---------------------------------------------------------------- imageLoader.get("https://img-my.csdn.net/uploads/201404/13/1397393290_5765.jpeg", listener, 200, 200); //指定图片允许的最大宽度和高度
ImageCache接口
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 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);
}
}
第三种方式加载网络图片
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" />
- 在代码中获取该控件的实例。
设置要加载的图片地址。
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",
NetworkImageView并不需要提供任何设置最大宽高的方法也能够对加载的图片进行压缩。这是由于NetworkImageView是一个控件,在加载图片的时候它会自动获取自身的宽高,然后对比网络图片的宽度,再决定是否需要对图片进行压缩.
- 如果你不想对图片进行压缩的话,只需要在布局文件中把NetworkImageView的layout_width和layout_height都设置成wrap_content就可以了.
自定义Request
- 导入Volley框架
- 继承Request(泛型)
- 重写Request的两个抽象方法:
- deliverResponse(): 调用mListener(定义在Response中的接口)中的onResponse()方法,并将response内容传入;
- parseNetworkResponse(): 先是将服务器响应的数据解析成一个字符串,然后设置到XmlPullParser对象中;
仿照StringRequest定义两个构造函数:
public XMLRequest(int method, String url, Listener<XmlPullParser> listener, ErrorListener errorListener) { super(method, url, errorListener); mListener = listener; (mListener在这里被赋值) } public XMLRequest(String url, Listener<XmlPullParser> listener, ErrorListener errorListener) { this(Method.GET, url, listener, errorListener); }
详细代码
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); } }
深入了解Volley
从RequestQueue开始:
找到Request的newRequestQueue(Context context, HttpStack stack)方法, 看到有一行是判断如果stack等于null, 则创建一个HttpStack对象 即: 如果手机系统版本号是大于9的,则创建一个HurlStack(内部使用HttpURLConnection)的实例,否则就创建一个HttpClientStack(内部使用HttpClient)的实例.if (stack == null) { if (Build.VERSION.SDK_INT >= 9) { stack = new HurlStack(); } else { stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent)); } }
创建好了HttpStack之后,接下来又创建了一个Network对象,它是用于根据传入的HttpStack对象来处理网络请求的,紧接着new出一个RequestQueue对象,并调用它的start()方法进行启动,然后将RequestQueue返回,这样newRequestQueue()的方法就执行结束了。
那么RequestQueue的start()方法内部到底执行了什么东西呢?
public void start() { this.stop(); this.mCacheDispatcher = new CacheDispatcher(this.mCacheQueue, this.mNetworkQueue, this.mCache, this.mDelivery); //缓存线程 this.mCacheDispatcher.start(); for(int i = 0; i < this.mDispatchers.length; ++i) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(this.mNetworkQueue, this.mNetwork, this.mCache, this.mDelivery); //网络请求线程 this.mDispatchers[i] = networkDispatcher; networkDispatcher.start(); } } public void stop() { if(this.mCacheDispatcher != null) { this.mCacheDispatcher.quit(); } for(int i = 0; i < this.mDispatchers.length; ++i) { if(this.mDispatchers[i] != null) { this.mDispatchers[i].quit(); } } }
这里先是创建了一个CacheDispatcher的实例,然后调用了它的start()方法,接着在一个for循环里去创建NetworkDispatcher的实例,并分别调用它们的start()方法。这里的CacheDispatcher和NetworkDispatcher都是继承自Thread的,而默认情况下for循环会执行四次,也就是说当调用了Volley.newRequestQueue(context)之后,就会有五个线程一直在后台运行,不断等待网络请求的到来.
得到了RequestQueue之后,我们只需要构建出相应的Request,然后调用RequestQueue的add()方法将Request传入就可以完成网络请求操作了.
add()方法的内部肯定有着非常复杂的逻辑,我们来一起看一下:
public Request add(Request request) { request.setRequestQueue(this); Set var2 = this.mCurrentRequests; synchronized(this.mCurrentRequests) { this.mCurrentRequests.add(request); } request.setSequence(this.getSequenceNumber()); request.addMarker("add-to-queue"); if(!request.shouldCache()) { //判断当前的请求是否可以缓存 this.mNetworkQueue.add(request); //如果不能缓存则直接将这条请求加入网络请求队列, 结束. return request; } else { Map var7 = this.mWaitingRequests; synchronized(this.mWaitingRequests) { String cacheKey = request.getCacheKey(); if(this.mWaitingRequests.containsKey(cacheKey)) { Object stagedRequests = (Queue)this.mWaitingRequests.get(cacheKey); if(stagedRequests == null) { stagedRequests = new LinkedList(); } ((Queue)stagedRequests).add(request); this.mWaitingRequests.put(cacheKey, stagedRequests); if(VolleyLog.DEBUG) { VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", new Object[]{cacheKey}); } } else { this.mWaitingRequests.put(cacheKey, (Object)null); this.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; } // Attempt to retrieve this item from cache. /** *尝试从缓存当中取出响应结果,如何为空的话则把这条请求加入到网络请求队列中, *如果不为空的话再判断该缓存是否已过期, *如果已经过期了则同样把这条请求加入到网络请求队列中, *否则就认为不需要重发网络请求,直接使用缓存中的数据即可。 */ Cache.Entry entry = mCache.get(request.getCacheKey()); if (entry == null) { request.addMarker("cache-miss"); // Cache miss; send off to the 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; parse its data for delivery back to the request. request.addMarker("cache-hit"); /** *调用Request的parseNetworkResponse()方法来对数据进行解析 */ 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); // Perform the network request. NetworkResponse networkResponse = mNetwork.performRequest(request); //调用Network的performRequest()方法来去发送网络请求 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. /** *收到了NetworkResponse这个返回值后 *调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据, *以及将数据写入到缓存, *这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。 */ 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(); //在解析完了NetworkResponse中的数据之后,又会调用ExecutorDelivery的postResponse()方法来回调解析出的数据. 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是一个接口,这里具体的实现是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就是在一开始调用newRequestQueue()方法是创建的实例, *默认情况下如果系统版本号大于9就创建的HurlStack对象,否则创建HttpClientStack对象, *这两个对象的内部实际就是分别使用HttpURLConnection和HttpClient来发送网络请求的, *之后会将服务器返回的数据组装成一个NetworkResponse对象进行返回 */ 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) { 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(); } // 组装成一个NetworkResponse对象进行返回 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"); //传入了一个ResponseDeliveryRunnable对象,就可以保证该对象中的run()方法就是在主线程当中运行的了 mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable)); }
我们看下ResponseDeliveryRunnable 中的 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()) { /** *这个就是我们在自定义Request时需要重写的另外一个方法, *每一条网络请求的响应都是回调到这个方法中, *最后我们再在这个方法中将响应的数据回调到Response.Listener的onResponse()方法中就可以了。 */ 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(); } } }
最后
我们来整理一下思路:
蓝色部分代表主线程,绿色部分代表缓存线程,橙色部分代表网络线程。我们在主线程中调用RequestQueue的add()方法来添加一条网络请求,这条请求会先被加入到缓存队列当中,如果发现可以找到相应的缓存结果就直接读取缓存并解析,然后回调给主线程。如果在缓存中没有找到结果,则将这条请求加入到网络请求队列中,然后处理发送HTTP请求,解析响应结果,写入缓存,并回调主线程。