一、 Volley 的地位
自2013年Google I/O 大会上,Google 推出 Volley 之后,一直到至今,由于其使用简单、代码轻量、通信速度快、并发量大等特点,倍受开发者们的青睐。 先看两张图,让图片告诉我们 Volley 的用处; 第一张 Volley 的经典图
通过上图,我们可以发现 Volley适合网络通信频繁操作,并能同时实现多个网络通信。 第二张图 我们在以前在 ListView 的 item 中如果有网络请求,一般都是通过Task 异步任务来完成,并在完成之后通知 Adapter 更新数据。而Volley 不需要这么麻烦,因为里面已经为我们封装好了处理的线程,网络请求,缓存的获取,数据的回掉都是对应不同的线程。
二、Volley使用步骤及基本分析
volley 的使用遵循以下四步: 1、获取请求队里RequestQueue RequestQueue mRequestQueue = Vollay.newRequestQueue(Context context) ; 2、启动请求队列 mRequestQueue.start(); 以上这两步通常也归为一步 3、获取请求Request Request mRequest = new ObjectRequest(…) ; ObjectRequest需要根据自己请求返回的数据来定制,继承之抽象类Request,Vollay 已经为我们实现了 StringRequest、JsonArrayRequest、JsonObjectRequest、ImageRequest请求; 4、把请求添加到请求队列中 mRequestQueue.add(mRequest); 说明:在一个项目中,请求队列不需要出现多个,一般整个项目中共用同一个mRequestQueue,因为请求队列启动的时候会做以下事情
/** * Starts the dispatchers in this queue. */ public void start() { //结束队列中所有的线程 stop(); // Make sure any currently 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 pool size. //启动网络请求处理线程,默认为5个,可以自己设定 size for (int i = 0; i < mDispatchers.length; i++) { NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork, mCache, mDelivery); //保存网络请求线程 mDispatchers[i] = networkDispatcher; //启动网络请求处理线程 networkDispatcher.start(); } }
启动一个缓存mCacheDispatcher线程,用来读取缓存数据,启动若干个网络请求mDispatchers线程,用来实现网络通信。 mCacheDispatcher线程的 run 方法
@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(); //打印 log 信息 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"); Log.i("CacheDispatcher", "没有缓存数据:" + request.getUrl()); 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); Log.i("CacheDispatcher", "缓存数据过期:" + request.getUrl()); mNetworkQueue.put(request); continue; } // We have a cache hit; parse its data for delivery back to the request. // 已经获取到了有效的缓存数据,回调给 request 的parseNetworkResponse,需要自己根据需求来解析数据 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. Log.i("CacheDispatcher", "获取缓存数据:" + request.getUrl()); 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; } } }
mDispatchers线程的 run 方法
@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; } // Tag the request (if API >= 14) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { TrafficStats.setThreadStatsTag(request.getTrafficStatsTag()); } // Perform the network request. //网络请求的基本操作(核心操作),从网络中获取数据 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)); } } }
这两个线程处理类型基本相同,都是采用循环的方法,在队列中获取请求,有请求则执行相应的请求,没有则阻塞在下面两行代码中
//阻塞线程的执行 //缓存线程阻塞的地方 final Request request = mCacheQueue.take(); //网络请求阻塞的地方 request = mQueue.take();
所以我们一般只需要根据不同的接口,实例化不同的请求 Request,往队列中添加 即可,它首先判断请求是否需要缓存,如果不需要,直接添加到网络请求的队列中,结束下面的操作,如果需要缓存,则把请求添加到缓存队列中,具体看代码。
public Request add(Request 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 the request is uncacheable, skip the cache queue and go straight to the network. //判断请求是否需要缓存,如果不需要,直接添加到网络请求的队列中,结束下面的操作,如果需要缓存,则把请求添加到缓存队列中 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 { // Insert 'null' queue for this cacheKey, indicating there is now a request in // flight. mWaitingRequests.put(cacheKey, null); mCacheQueue.add(request); } return request; } }
所以如果需要缓存的话,一开始会从mCacheQueue.take()会得到执行,当不符合要求的时候,请求会添加到真正的网络请求队列中,以下是不符合要求的代码
//没有缓存 if (entry == null) { request.addMarker("cache-miss"); Log.i("CacheDispatcher", "没有缓存数据:" + request.getUrl()); 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); Log.i("CacheDispatcher", "缓存数据过期:" + request.getUrl()); mNetworkQueue.put(request); continue; }
如果缓存不符合要求,网络线程终止阻塞得到执行; 我们一般习惯用法是在 Application 中全局初始化RequestQueue mRequestQueue,并启动它,让整个应用都能获取到。具体运用将会在下面用到。
三、Volley 实战 GET 请求和 POST 请求
这里我用酷狗音乐播放器中的一个音乐搜索功能的接口来实现这两种请求 接口:http://mobilecdn.kugou.com/api/v3/search/song?keyword=冰雨 返回数据: 根据返回数据我们可以把基本数据类型定义出来
SearchResult 就是整个数据的对象,ListSong 对应的是 Data 对象,SongDetail是info 下的每个 item 对象。 1、GET 请求 第一步:在 Application 中初始化RequestQueue,
//初始化请求队列 private void initRequestQueue(){ VollayUtil.initialize(mContext); }
/** * Created by gyzhong on 15/3/1. */ public class VolleyUtil { private static RequestQueue mRequestQueue ; public static void initialize(Context context){ if (mRequestQueue == null){ synchronized (VolleyUtil.class){ if (mRequestQueue == null){ mRequestQueue = Volley.newRequestQueue(context) ; } } } mRequestQueue.start(); } public static RequestQueue getRequestQueue(){ if (mRequestQueue == null) throw new RuntimeException("请先初始化mRequestQueue") ; return mRequestQueue ; } }
第二步:定制 Request 先来分析接口所返回的数据,我们看到是一条 json 数据,虽然 Volley 中已经为我们定制好了JsonObjectRequest请求,但我们知道,在数据具体显示的时候,是需要把 json 数据转化为对象进行处理,所以这里我们可以定制通用的对象请求。如何定制呢? 先看StringRequest的实现代码
//继承Request<String>,String 为请求解析之后的数据 public class StringRequest extends Request<String> { //正确数据回调接口 private final Listener<String> mListener; public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) { super(method, url, errorListener); mListener = listener; } public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { this(Method.GET, url, listener, errorListener); } //回调解析之后的数据 @Override protected void deliverResponse(String response) { mListener.onResponse(response); } //解析数据,把网络请求,或者中缓存中获取的数据,解析成 String @Override protected Response<String> parseNetworkResponse(NetworkResponse response) { 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继承了 Request 并实现了两个抽象方法parseNetworkResponse()和 deliverResponse(),这两个方法很好理解,parseNetworkResponse()把获取到的数据解析成我们所定义的数据类型;deliverResponse()把所解析的数据通过回调接口回调给展示处。 为了简化回调接口,这里把错误回调Response.ErrorListener 和正确的数据回调Response.Listener合并成一个ResponseListener
/** * Created by gyzhong on 15/3/1. * 简化回调接口 */ public interface ResponseListener<T> extends Response.ErrorListener,Response.Listener<T> { }
根据 StringRequest,如法炮制
/** * Created by gyzhong on 15/3/1. */ public class GetObjectRequest<T> extends Request<T> { /** * 正确数据的时候回掉用 */ private ResponseListener mListener ; /*用来解析 json 用的*/ private Gson mGson ; /*在用 gson 解析 json 数据的时候,需要用到这个参数*/ private Type mClazz ; public GetObjectRequest(String url,Type type, ResponseListener listener) { super(Method.GET, url, listener); this.mListener = listener ; mGson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() ; mClazz = type ; } /** * 这里开始解析数据 * @param response Response from the network * @return */ @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { try { T result ; String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); result = mGson.fromJson(jsonString,mClazz) ; return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)); } catch (UnsupportedEncodingException e) { return Response.error(new ParseError(e)); } } /** * 回调正确的数据 * @param response The parsed response returned by */ @Override protected void deliverResponse(T response) { mListener.onResponse(response); } }
以上代码中在实例化 Gson 的时候用到的是mGson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation(),主要是用于过滤字段用的.如果有疑问的同学可以参考我前面写的一篇文章Gson 过滤字段的几种方法 第三步:获取 request
Request request = new GetObjectRequest(url,new TypeToken<SearchResult>(){}.getType(),listener) ;
1、url -> http://mobilecdn.kugou.com/api/v3/search/song?keyword=冰雨 2、new TypeToken(){}.getType() ->为 gson 解析 json 数据所要的 type 3、listener -> 为我们定义的ResponseListener回调接口 第四步:添加请求到队列中
VolleyUtil.getRequestQueue().add(request) ;
所以,此接口的代码即为
/** * 酷狗音乐搜索音乐get网络请求接口 * @param keyWord 要搜索的关键字 * @param listener 回调接口,包含错误回调和正确的数据回调 */ public static void getObjectSearchApi(String keyWord,ResponseListener listener){ String url ; try { url = Constant.Host+"?keyword="+ URLEncoder.encode(keyWord,"utf-8") ; } catch (UnsupportedEncodingException e) { e.printStackTrace(); url = Constant.Host+"?keyword="+ URLEncoder.encode(keyWord) ; } Request request = new GetObjectRequest(url,new TypeToken<SearchResult>(){}.getType(),listener) ; VollayUtil.getRequestQueue().add(request) ; }
第五步:代码测试
public class GetRequestActivity extends ActionBarActivity { private ListView mListView ; private SongAdapter mAdapter ; private List<SongDetail> mSongList ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_get); mSongList = new ArrayList<SongDetail>() ; mListView = (ListView) findViewById(R.id.id_list_view); mAdapter = new SongAdapter(this,mSongList) ; mListView.setAdapter(mAdapter); /*请求网络获取数据*/ KugouApi.getObjectSearchApi("冰雨",new ResponseListener<SearchResult>() { @Override public void onErrorResponse(VolleyError error) { Log.v("zgy","======onErrorResponse====="+error); } @Override public void onResponse(SearchResult response) { Log.v("zgy","======onResponse====="+response); mSongList.addAll(response.getData().getInfo()) ; mAdapter.notifyDataSetChanged(); } }); } }
测试效果图如下:
可以看到和我们在浏览器中请求的数据一模一样!
2、POST请求 因为在讲 get 请求的时候花了很大篇幅讲原理,所以在 post 请求的时候,需要注意的东西相对来说比较少, 不管是 get 请求还是 post 请求,实现步骤是不会变。 这里post 请求,我们也是用酷狗搜索音乐的这个 api 来测试! 在前面我们已经讲到了,在同一个应用中共用同一个 RequestQueue,所以第一步可以省略,因为我们已经实现过了。这里直接到定制Request,我们在学习网络编程的时候就已经知道,用 GET方式请求,请求的数据是直接跟在 URL的后面用”?”去分开了,如果有多个数据则用”&”分开。而 POST则把数据直接封装在HTTP的包体中,两者各有优缺点,自己衡量着用。 因为 api 接口还是同一个,所以返回的数据类型肯定是一样的,在解析数据的时候就可以和 GetObjectRequest 复用,所以 PostObjectRequest 的实现可以通过继承GetObjectRequest的方式,也可以直接拷贝一份出来,为了更好的区分,我这里就直接拷贝一份,然后再稍加修改。
/** * Created by gyzhong on 15/3/1. */ public class PostObjectRequest<T> extends Request<T> { /** * 正确数据的时候回掉用 */ private ResponseListener mListener ; /*用来解析 json 用的*/ private Gson mGson ; /*在用 gson 解析 json 数据的时候,需要用到这个参数*/ private Type mClazz ; /*请求 数据通过参数的形式传入*/ private Map<String,String> mParams; //需要传入参数,并且请求方式不能再为 get,改为 post public PostObjectRequest(String url, Map<String,String> params,Type type, ResponseListener listener) { super(Method.POST, url, listener); this.mListener = listener ; mGson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() ; mClazz = type ; setShouldCache(false); mParams = params ; } /** * 这里开始解析数据 * @param response Response from the network * @return */ @Override protected Response<T> parseNetworkResponse(NetworkResponse response) { try { T result ; String jsonString = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); Log.v("zgy", "====SearchResult===" + jsonString); result = mGson.fromJson(jsonString,mClazz) ; return Response.success(result, HttpHeaderParser.parseCacheHeaders(response)); } catch (UnsupportedEncodingException e) { return Response.error(new ParseError(e)); } } /** * 回调正确的数据 * @param response The parsed response returned by */ @Override protected void deliverResponse(T response) { mListener.onResponse(response); } //关键代码就在这里,在 Volley 的网络操作中,如果判断请求方式为 Post 则会通过此方法来获取 param,所以在这里返回我们需要的参数, @Override protected Map<String, String> getParams() throws AuthFailureError { return mParams; } }
再来看看 api 接口怎么实现,
/** * 酷狗音乐搜索音乐post网络请求接口 * @param keyword 要搜索的关键字 * @param listener 回调接口,包含错误回调和正确的数据回调 */ public static void postObjectSearchApi(String keyword,ResponseListener listener){ Map<String,String> param = new HashMap<String,String>() ; param.put("keyword",keyword) ; Request request = new PostObjectRequest(Constant.Host,param,new TypeToken<SearchResult>(){}.getType(),listener); VolleyUtil.getRequestQueue().add(request) ; }
跟 get 请求还是很相似的,只是在实例化 Request 的时候多传入了一个param参数,并且 url 不能再是包含请求数据的 url。 接口 api测试代码
public class PostRequestActivity extends ActionBarActivity { private ListView mListView ; private SongAdapter mAdapter ; private List<SongDetail> mSongList ; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_post); mSongList = new ArrayList<SongDetail>() ; mListView = (ListView) findViewById(R.id.id_list_view); mAdapter = new SongAdapter(this,mSongList) ; mListView.setAdapter(mAdapter); /*请求网络获取数据*/ KugouApi.postObjectSearchApi("冰雨", new moon.vollay.network.ResponseListener<SearchResult>() { @Override public void onErrorResponse(VolleyError error) { Log.v("zgy", "======onErrorResponse=====" + error); } @Override public void onResponse(SearchResult response) { Log.v("zgy", "======onResponse=====" + response); mSongList.addAll(response.getData().getInfo()); mAdapter.notifyDataSetChanged(); } }); } }
post测试效果图如下:
数据显示跟 get 请求完全相同;ok,以上就是 Volley GET请求和 POST请求的全部内容!接下来又到了总结的时候
四、总结
1、volley 适用于轻量高并发的网络请求,这里补充一个知识点,因为 Volley 请求网络的数据全部保存在内存中,所以 volley 不适合请求较大的数据,比如下载文件,下载大图片等。
2、volley 的使用遵循四个步骤 a、RequestQueue mRequestQueue = Vollay.newRequestQueue(Context context) ; b、mRequestQueue.start() c、Request mRequest = new ObjectRequst(…) d、mRequestQueue.add(mRequest)
3、同一个程序中最好共用一个 RequestQueue。
4、可以根据接口的放回数据类型定制任意的 Request,volley 已经默认为我们实现了 StringRequest、JsonArrayRequest、JsonObjectRequest、ImageRequest四个请求类型。
最后如果觉得有用请继续关注我的 blog,我将会在下篇 blog 中讲解,Volley 如何通过 post实现表单的提交。