Android——Volley框架学习总结 - changel - 博客园

249 阅读12分钟
原文链接: www.cnblogs.com

转载请注明本文出自changel的博客(www.cnblogs.com/caichongyan… ),请尊重他人的辛勤劳动成果,谢谢!

Volley框架特点:

  • 适用于频繁请求而每次请求数据量不会很大;
  • 在请求的基础上做了磁盘缓存;
  • 防止多次相同请求浪费资源;
  • 提供String、Json、图片异步下载;
  • 网络请求的优先级处理;
  • 图片请求无需担心生命周期问题。

Volley框架使用:

  1. 首先,通过Volley的静态方法new一个请求队列
    1 RequestQueue mQueue = Volley.newRequestQueue(context); 
  2. 假如我们创建一个StringRequest实例(Volley提供,StringRequest、ImageRequest、JsonRequest。)
    StringRequest stringRequest = new StringRequest("http://www.baidu.com",  
                            new Response.Listener<String>() {  
                                @Override  
                                public void onResponse(String response) {  
                                    Log.d("TAG", response);  
                                }  
                            }, new Response.ErrorListener() {  
                                @Override  
                                public void onErrorResponse(VolleyError error) {  
                                    Log.e("TAG", error.getMessage(), error);  
                                }  
                            });  
  3. 将XXXRequest对象添加进队列中
    mQueue.add(stringRequest); 
  4. 调用RequestQueue的start方法就可以开始一条网络请求了
    mQueue.start();
  5. 当然我们可以设置请求的方式:
    StringRequest stringRequest = new StringRequest(Method.POST, url,  listener, errorListener); 
  6. 设置提交的参数:
    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;  
        }  
    }; 
  7. 至于其他的请求,读者可以自己去尝试。比如图片和Json的请求。(注意Volley还提供了自己的控件,com.android.volley.NetworkImageView,使用时我们无需担心相关网络请求的生命周期问题。)代码如下:
    mImageLoader = new ImageLoader(mRequestQueue, new BitmapLruCache());
     
    if(holder.imageRequest != null) {
        holder.imageRequest.cancel();
    }
    holder.imageRequest = mImageLoader.get(BASE_UR + item.image_url, holder.imageView, R.drawable.loading, R.drawable.error);
    mImageView.setImageUrl(url, imageLoader) //这里使用的imageView的控件是Volley提供的

Volley框架时序图:

自己画了一个Volley框架的时序图,帮助理解。Volley框架实现原理:

下面给出自己画的一张UML帮助理解,画得可能不是很准确。但至少整体的框架还是明确的。

这个框架我觉得最核心的部分就是RequestQueue这个请求队列,首先通过Volley.newRequestQueue(context)的方法就可以拿到一个RequestQueue对象。

这个请求队列主要是由两部分构成,一个是cache缓存,一个network网络请求。其实这两个只是一个接口而已。

我们一开始要往请求队列里面添加请求的任务,比如请求一些文字信息,那么就创建一个StringRequest对象,通过构造函数,将两个接口对象传递进去,进行绑定。

一个是当请求成功返回的数据,通过回调这个接口对象的方法传递出来。一个是回调显示错误信息的接口。   接下来将该请求对象添加进RequestQueue中,其实内部是保存在来一个按优先级排序的PriorityBlockingQueue的对象中。以下是源码
 1     /**
 2      * Adds a Request to the dispatch queue.
 3      * @param request The request to service
 4      * @return The passed-in request
 5      */
 6     public <T> Request<T> add(Request<T> request) {
 7         // Tag the request as belonging to this queue and add it to the set of current requests.
 8         request.setRequestQueue(this);
 9         synchronized (mCurrentRequests) {
10             mCurrentRequests.add(request);
11         }
12 
13         // Process requests in the order they are added.
14         request.setSequence(getSequenceNumber());
15         request.addMarker("add-to-queue");
16 
17         // If the request is uncacheable, skip the cache queue and go straight to the network.
18         if (!request.shouldCache()) {
19             mNetworkQueue.add(request);
20             return request;
21         }
22 
23         // Insert request into stage if there's already a request with the same cache key in flight.
24         synchronized (mWaitingRequests) {
25             String cacheKey = request.getCacheKey();
26             if (mWaitingRequests.containsKey(cacheKey)) {
27                 // There is already a request in flight. Queue up.
28                 Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
29                 if (stagedRequests == null) {
30                     stagedRequests = new LinkedList<Request<?>>();
31                 }
32                 stagedRequests.add(request);
33                 mWaitingRequests.put(cacheKey, stagedRequests);
34                 if (VolleyLog.DEBUG) {
35                     VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
36                 }
37             } else {
38                 // Insert 'null' queue for this cacheKey, indicating there is now a request in
39                 // flight.
40                 mWaitingRequests.put(cacheKey, null);
41                 mCacheQueue.add(request);
42             }
43             return request;
44         }
45     }

(其实内部并不是简单的将Request的子类直接添加到队列中,而是通过mWaitingRequests,这个是一个MAP的集合,通过containsKey(cacheKey)来判断任务队列中是否已经存在了该请求任务,主要是避免重复请求。)

其实所有的请求对象都是继承于Request的抽象类,该抽象类还实现了Comparable 的接口,该接口是将该对象添加进PriorityBlockingQueue中必须实现的,然后Request的子类实现Request类中parseNetworkResponse()的抽象方法,这个方法是对请求返回的数据进行解析。我们可以将数据封装为一个对象,进行使用。

比如StringRequest实现的parseNetworkResponse方法
 1     @Override
 2     protected Response<String> parseNetworkResponse(NetworkResponse response) {
 3         String parsed;
 4         try {
 5             parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
 6         } catch (UnsupportedEncodingException e) {
 7             parsed = new String(response.data);
 8         }
 9         return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
10     }
 

当调用RequestQueue对象内的start()方法的时候,具体是通过CacheDispatcher缓存调度和NetworkDispatcher网络调度去实现的。NetworkDispatcher这个就是网络调度的核心,它其实是一个Thread的子类。而CacheDispatcher也是一个Thread的子类。简单点说,NetworkDispatcher 线程的run()方法就是把请求队列取出一个Request,让Network去执行一次请求。这里说的并不是很准确,因为其实Cache和Network只是两个接口,NetWork接口定义了一个请求的方法,真正的实现类还是BasicNetwork,所以具体的网络请求是在BasicNetwork里面的performRequest方法去实现的。下面我贴出这个方法的源码:

  1  public NetworkResponse performRequest(Request<?> request) throws VolleyError {
  2         long requestStart = SystemClock.elapsedRealtime();
  3         while (true) {
  4             HttpResponse httpResponse = null;
  5             byte[] responseContents = null;
  6             Map<String, String> responseHeaders = Collections.emptyMap();
  7             try {
  8                 // Gather headers.
  9                 Map<String, String> headers = new HashMap<String, String>();
 10                 addCacheHeaders(headers, request.getCacheEntry());
 11                 httpResponse = mHttpStack.performRequest(request, headers);
 12                 StatusLine statusLine = httpResponse.getStatusLine();
 13                 int statusCode = statusLine.getStatusCode();
 14 
 15                 responseHeaders = convertHeaders(httpResponse.getAllHeaders());
 16                 // Handle cache validation.
 17                 if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
 18 
 19                     Entry entry = request.getCacheEntry();
 20                     if (entry == null) {
 21                         return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
 22                                 responseHeaders, true,
 23                                 SystemClock.elapsedRealtime() - requestStart);
 24                     }
 25 
 26                     // A HTTP 304 response does not have all header fields. We
 27                     // have to use the header fields from the cache entry plus
 28                     // the new ones from the response.
 29                     // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
 30                     entry.responseHeaders.putAll(responseHeaders);
 31                     return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
 32                             entry.responseHeaders, true,
 33                             SystemClock.elapsedRealtime() - requestStart);
 34                 }
 35                 
 36                 // Handle moved resources
 37                 if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
 38                     String newUrl = responseHeaders.get("Location");
 39                     request.setRedirectUrl(newUrl);
 40                 }
 41 
 42                 // Some responses such as 204s do not have content.  We must check.
 43                 if (httpResponse.getEntity() != null) {
 44                   responseContents = entityToBytes(httpResponse.getEntity());
 45                 } else {
 46                   // Add 0 byte response as a way of honestly representing a
 47                   // no-content request.
 48                   responseContents = new byte[0];
 49                 }
 50 
 51                 // if the request is slow, log it.
 52                 long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
 53                 logSlowRequests(requestLifetime, request, responseContents, statusLine);
 54 
 55                 if (statusCode < 200 || statusCode > 299) {
 56                     throw new IOException();
 57                 }
 58                 return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
 59                         SystemClock.elapsedRealtime() - requestStart);
 60             } catch (SocketTimeoutException e) {
 61                 attemptRetryOnException("socket", request, new TimeoutError());
 62             } catch (ConnectTimeoutException e) {
 63                 attemptRetryOnException("connection", request, new TimeoutError());
 64             } catch (MalformedURLException e) {
 65                 throw new RuntimeException("Bad URL " + request.getUrl(), e);
 66             } catch (IOException e) {
 67                 int statusCode = 0;
 68                 NetworkResponse networkResponse = null;
 69                 if (httpResponse != null) {
 70                     statusCode = httpResponse.getStatusLine().getStatusCode();
 71                 } else {
 72                     throw new NoConnectionError(e);
 73                 }
 74                 if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
 75                         statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
 76                     VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
 77                 } else {
 78                     VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
 79                 }
 80                 if (responseContents != null) {
 81                     networkResponse = new NetworkResponse(statusCode, responseContents,
 82                             responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
 83                     if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
 84                             statusCode == HttpStatus.SC_FORBIDDEN) {
 85                         attemptRetryOnException("auth",
 86                                 request, new AuthFailureError(networkResponse));
 87                     } else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
 88                                 statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
 89                         attemptRetryOnException("redirect",
 90                                 request, new AuthFailureError(networkResponse));
 91                     } else {
 92                         // TODO: Only throw ServerError for 5xx status codes.
 93                         throw new ServerError(networkResponse);
 94                     }
 95                 } else {
 96                     throw new NetworkError(networkResponse);
 97                 }
 98             }
 99         }
100     }

而且这个方法有两种访问服务器的方法,一种是基于HttpClient去实现的,一种是HttpURLConnection去实现的。这么做是为了适配不同Sdk版本的对网络请求的要求。在创建BasicNetwork的时候,会判断当前的版本,去选择哪种方式。在Volley类的静态方法里面进行判断。源码如下:

 1     public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
 2         File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
 3 
 4         String userAgent = "volley/0";
 5         try {
 6             String packageName = context.getPackageName();
 7             PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
 8             userAgent = packageName + "/" + info.versionCode;
 9         } catch (NameNotFoundException e) {
10         }
11 
12         if (stack == null) {
13             if (Build.VERSION.SDK_INT >= 9) {
14                 stack = new HurlStack();
15             } else {
16                 // Prior to Gingerbread, HttpUrlConnection was unreliable.
17                 // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
18                 stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
19             }
20         }
21 
22         Network network = new BasicNetwork(stack);
23         
24         RequestQueue queue;
25         if (maxDiskCacheBytes <= -1)
26         {
27             // No maximum size specified
28             queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
29         }
30         else
31         {
32             // Disk cache size specified
33             queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
34         }
35 
36         queue.start();
37 
38         return queue;
39     }

 

而Cache接口定义了一些读取和写入文件的方法,具体的实现类是DiskBasedCache,主要是用来查询磁盘中有没有对应的请求数据还有对第一次请求回来的数据进行存储。主要有两个方法,一个是put(),还有一个是get();
 1     @Override
 2     public synchronized void put(String key, Entry entry) {
 3         pruneIfNeeded(entry.data.length);
 4         File file = getFileForKey(key);
 5         try {
 6             FileOutputStream fos = new FileOutputStream(file);
 7             CacheHeader e = new CacheHeader(key, entry);
 8             boolean success = e.writeHeader(fos);
 9             if (!success) {
10                 fos.close();
11                 VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
12                 throw new IOException();
13             }
14             fos.write(entry.data);
15             fos.close();
16             putEntry(key, e);
17             return;
18         } catch (IOException e) {
19         }
20         boolean deleted = file.delete();
21         if (!deleted) {
22             VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
23         }
24     }
 1     @Override
 2     public synchronized Entry get(String key) {
 3         CacheHeader entry = mEntries.get(key);
 4         // if the entry does not exist, return.
 5         if (entry == null) {
 6             return null;
 7         }
 8 
 9         File file = getFileForKey(key);
10         CountingInputStream cis = null;
11         try {
12             cis = new CountingInputStream(new FileInputStream(file));
13             CacheHeader.readHeader(cis); // eat header
14             byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
15             return entry.toCacheEntry(data);
16         } catch (IOException e) {
17             VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString());
18             remove(key);
19             return null;
20         } finally {
21             if (cis != null) {
22                 try {
23                     cis.close();
24                 } catch (IOException ioe) {
25                     return null;
26                 }
27             }
28         }
29     }
  其实在每次请求网络的时候,都是从mNetworkQueue里面取出任务然后进行请求的,而mNetworkQueue里面的对象是怎么来的? 他其实是在CacheDispatcher执行的时候,添加进来的。因为mNetworkQueue是PriorityBlockingQueue类型的,该类型对象的操作是线程安全的(通过ReentrantLock加锁),而且是阻塞型的,只要queue里面没有对象,那么再take()的时候就会阻塞在那里。这也解释了为什么用一个循环,不断的向队列里面取出请求对象的问题。大家仔细看下CacheDispatcher的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();
                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");
                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;
            }
        }
    }

在阅读的时候,注意下request.addMarker("*********");这些方法的调用,其实是对request设置一些标签,这个在后面会依据这些标签,对request的状态进行判断,主要的思路就是先对请求的任务在磁盘中查找,查找不到就将这个请求任务put进网络请求的队列中(在这里我们可以对这个框架进行优化,在堆磁盘查找的前,我们可以用一个数据结构来将一些数据保存在内存中,每次查找时先查询内存缓存,没有再在磁盘中查找。)request.addMarker("cache-hit-expired");这个就是标志着在磁盘中没有该缓存,所以接下来的代码会将请求添加到网络请求队列中。 mNetworkQueue.put(request);

NetworkDispatcher的run()方法和CacheDispatcher很相似,这里就不贴出来了。读者可以自己查看理解。 而当通过请求网络获取数据,或者从缓存中读取数据,最后都是将数据包装成Response类型的,然后调用request实现的parseNetworkResponse进行解析,最后将解析后返回的Response类型的对象,传递给ExecutorDelivery的对象,这个类实现了ResponseDelivery接口。 这个类的主要的功能就是去判断获取数据是否成功并且通过异步的方式去通知请求者,成功的话就把获取得到信息通过调用request的deliverResponse方法传递出去,在deliverResponse这个方法里面再通过之前绑定的接口将信息传递在最外面。我们在外面实现了该接口的方法里就能拿到这些信息。如果获取失败的话,则调用request的deliverError,将错误信息传递出去,最后也是通过调用接口对象的方法,将这些信息显示出来。 下面试的的一个实现了Runnable接口的对象,里面的成员方法就是回调了request对象中的mRequest.deliverResponse(mResponse.result);或者 mRequest.deliverError(mResponse.error);的方法将结果返回到外面。
 1     /**
 2      * A Runnable used for delivering network responses to a listener on the
 3      * main thread.
 4      */
 5     @SuppressWarnings("rawtypes")
 6     private class ResponseDeliveryRunnable implements Runnable {
 7         private final Request mRequest;
 8         private final Response mResponse;
 9         private final Runnable mRunnable;
10 
11         public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
12             mRequest = request;
13             mResponse = response;
14             mRunnable = runnable;
15         }
16 
17         @SuppressWarnings("unchecked")
18         @Override
19         public void run() {
20             // If this request has canceled, finish it and don't deliver.
21             if (mRequest.isCanceled()) {
22                 mRequest.finish("canceled-at-delivery");
23                 return;
24             }
25 
26             // Deliver a normal response or error, depending.
27             if (mResponse.isSuccess()) {
28                 mRequest.deliverResponse(mResponse.result);
29             } else {
30                 mRequest.deliverError(mResponse.error);
31             }
32 
33             // If this is an intermediate response, add a marker, otherwise we're done
34             // and the request can be finished.
35             if (mResponse.intermediate) {
36                 mRequest.addMarker("intermediate-response");
37             } else {
38                 mRequest.finish("done");
39             }
40 
41             // If we have been provided a post-delivery runnable, run it.
42             if (mRunnable != null) {
43                 mRunnable.run();
44             }
45        }
46     }

值得注意的是,ExecutorDelivery对象里面还有一个handler,它是获取了主线程的looper,主要是为了及时实现与UI界面进行交互。比如在我们请求获取图片类型数据的时候。

Volley框架的不足:

  • 它只适用于频繁而数据量小的请求。(当请求的是比较大的数据时,这个Volley框架发挥的作用就不是很大了。)
  • 它只实现了一级缓存(磁盘缓存)。(这样做虽然节省内存资源的消耗,但是读写数据的速度会比较慢。我们可以在这个基础上进行优化,多加一级内存缓存,实现多级缓存。读者可以自行对这个框架进行优化。对于缓存机制,个人觉得可以学习下Universal image loader这个框架,里面有几种缓存机制写得很好。)。

好了,以上就是我对Volley框架的学习和理解,希望这篇文章能帮到那些对Volley这个框架还不是很熟悉的朋友们,当然也希望可以抛砖引玉的作用。大家对这篇文章有什么看法或者评价,欢迎评论交流交流。