Volley源码解析

作者: cgzysan | 来源:发表于2016-10-30 17:28 被阅读102次

    Volley介绍

    Volley 是 Google 在 2013 Google I/O 大会上发布推出的 Android 异步网络请求框架和图片加载框架,其最明显的一个优点就是特别适合数据量不大但是通信频繁的场景,最明显的缺点就是大数据传输表现的很糟糕。(适合于数据量小,通信频繁的网络操作)

    Volley提供了以下的便利功能

    • JSON数据和图像等的异步下载;
    • 网络请求排序(scheduling);
    • 网络请求优先级处理;
    • 缓存;
    • 多级别取消请求;
    • 与Activity生命周期联动(Activity结束时同时取消所有网络请求);

    newRequestQueue的使用

    Volley的用法很简单,发起一条HTTP GET请求,然后接收HTTP响应。首先需要获取到一个RequestQueue对象,可以调用如下方法获取到:

    RequestQueue newQueue = Volley.newRequestQueue(context);
    

    这里拿到的RequestQueue是一个请求队列对象,它可以缓存所有的HTTP请求,然后按照一定的算法并发地发出这些请求。

    RequestQueue内部的设计就是非常合适高并发的,因此我们不必为每一次HTTP请求都创建一个RequestQueue对象,这是非常浪费资源的,基本上在每一个需要和网络交互的Activity中创建一个RequestQueue对象就足够了。

    Request的使用

    Request是请求的基类,拥有以下子类:

    • ClearCacheRequest
    • JsonRequest
    • ImageRequest
    • StringRequest

    在基类中定义了一个内部接口类 Method 分别定义了集中常见的请求方式

    public interface Method {
        int DEPRECATED_GET_OR_POST = -1;
        int GET = 0;
        int POST = 1;
        int PUT = 2;
        int DELETE = 3;
        int PATCH = 4;
    }
    

    同时还定义了一个枚举 Priority 定义了一些优先级的常量

    public static enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE;
    
        private Priority() {
        }
    }
    

    StringRequestd的使用

    StringRequest的构造函数需要传入四个参数:

    public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) {
        super(method, url, errorListener);
        this.mListener = listener;
    }
    
    • 第一个参数是表示请求的方式 默认是0 也就是 GET 请求
    • 第二个参数是目标服务器的URL地址
    • 第三个参数是服务器响应成功的回调
    • 第四个参数是服务器响应失败的回调

    将这个StringRequest对象添加到RequestQueue里面就可以了。使用Volley时,可以从任何线程开始请求,但响应始终传递到了主线程上。
    以下是阅读Android Developer文档中的VolleyDemo:

    final TextView mTextView = (TextView) findViewById(R.id.text);
    ...
    
    // Instantiate the RequestQueue.
    RequestQueue queue = Volley.newRequestQueue(this);
    String url ="http://www.google.com";
    
    // Request a string response from the provided URL.
    StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
                new Response.Listener() {
        @Override
        public void onResponse(String response) {
            // Display the first 500 characters of the response string.
            mTextView.setText("Response is: "+ response.substring(0,500));
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            mTextView.setText("That didn't work!");
        }
    });
    // Add the request to the RequestQueue.
    queue.add(stringRequest);
    

    以上就用Volley发起了一个简单的HTTP 的 GET 请求。

    Volley发出Request

    Volley发出网络请求,阻塞I/O和解析数据都是在子线程中完成的,您可以在任何线程添加网络请求,但是响应始终都是传递到主线程上。
    分析一下 Android Developer 上的这幅图:

    volley-request.png
    • RequestQueue会维护一个缓存调度线程(CacheDispatcher)
    • 同时还会维护一网络调度线程池(NetworkDispatcher[])->线程池默认大小为4
    • Volley.newRequestQueue(context);的代码中在data/data/包名/cache/目录下创建了一个volley的文件夹用于存放缓存(本地缓存)
    • 同时创建一个 RequestQueue 类,并调用了其start() 方法
    BasicNetwork network1 = new BasicNetwork((HttpStack)stack);//将在后文提到
    RequestQueue queue1 = new RequestQueue(new DiskBasedCache(cacheDir), network1);
    queue1.start();
    
    • start()方法看上去像是线程开启的方法,但是 RequestQueue 并不是一个Thread类,不过在当中它干的就是开启线程的事。
    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();
        }
    }
    

    没错,它逐一开启了缓存调度线程和所有的网络调度线程

    • 每一条线程就开始不断的循环读取cache队列或者net队列的头

    • 当一个Request被添加到队列中的时候,cache线程会首先对这个请求进行筛选,如果这个请求的内筒可以在缓存中找到,并且没有过期,cache线程会自己解析响应的内容,并分发到主线程(UI);

    • 如果缓存中没有,这个request就会被加入到另一个NetworkQueue,所有真正准备进行网络通信的request都在这里,第一个可用的net线程会从NetworkQueue中拿出一个request扔向服务器。当响应获得数据之后,这个net线程会解析原始响应数据,写入缓存,并把解析后的结果返回给主线程。

    取消一个Request

    要取消一个请求,调用cancel()即可。一旦取消,Volley保证你的响应处理程序将永远不会被调用。这意味着在实践中,你可以取消所有待定的请求在你Activity的onStop()方法中,你不必乱抛垃圾的响应处理程序或者检查getActivity() == NULL,无论onSaveInstanceState()是否已经被调用。

    你发出去的请求必须要自己保证是可控的,在这里还有一个更简单的方法:你可以标记发送的每个请求对象,然后你可以使用这个标签来提供请求取消的范围。

    下面是一个使用字符串标签的例子:

    1. 定义您的标签同时将其添加到Request中
      public static final String TAG = "MyTag";
      StringRequest stringRequest; // Assume this exists.
      RequestQueue mRequestQueue; // Assume this exists.

       // Set the tag on the request.
       stringRequest.setTag(TAG);
       
       // Add the request to the RequestQueue.
       mRequestQueue.add(stringRequest);
      
    2. 在您的Activity的 onStop() 方法中,取消所有被标记的Request

       @Override
       protected void onStop () {
           super.onStop();
           if (mRequestQueue != null) {
               mRequestQueue.cancelAll(TAG);
           }
       }
      

    取消请求时要小心。如果你是根据你的响应处理程序来推进一个状态或启动另一个进程,你需要考虑到这个。同样,响应处理程序将不会被调用。

    BasicNetwork

    现在我们来说说 BasicNetwork,前文在讲 RequestQueue 的时候有所提到,其中 BasicNetwork 是创建 RequestQueue 对象时当中的一个参数

    • BasicNetwork 是Volley的一个默认网络实现,App连接HTTP客户端必须要对其初始化。最主要的是实现了 Network 接口中的 NetworkResponse performRequest(Request<?> var1) throws VolleyError; 方法,其实这个接口中就只有这一个方法。
      @Override
      public NetworkResponse performRequest(Request<?> request) throws VolleyError {
      long requestStart = SystemClock.elapsedRealtime();

            while(true) {
                HttpResponse httpResponse = null;
                Object responseContents = null;
                HashMap responseHeaders = new HashMap();
      
                try {
                    HashMap e = new HashMap();
                    this.addCacheHeaders(e, request.getCacheEntry());
                    httpResponse = this.mHttpStack.performRequest(request, e);
                    StatusLine statusCode2 = httpResponse.getStatusLine();
                    int networkResponse1 = statusCode2.getStatusCode();
                    Map responseHeaders1 = convertHeaders(httpResponse.getAllHeaders());
                    if(networkResponse1 == 304) {
                        return new NetworkResponse(304, request.getCacheEntry().data, responseHeaders1, true);
                    }
      
                    byte[] responseContents1 = this.entityToBytes(httpResponse.getEntity());
                    long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
                    this.logSlowRequests(requestLifetime, request, responseContents1, statusCode2);
                    if(networkResponse1 != 200 && networkResponse1 != 201 && networkResponse1 != 202 && networkResponse1 != 204) {
                        throw new IOException();
                    }
      
                    return new NetworkResponse(networkResponse1, responseContents1, responseHeaders1, false);
                } catch (SocketTimeoutException var12) {
                    attemptRetryOnException("socket", request, new TimeoutError());
                } catch (ConnectTimeoutException var13) {
                    attemptRetryOnException("connection", request, new TimeoutError());
                } catch (MalformedURLException var14) {
                    throw new RuntimeException("Bad URL " + request.getUrl(), var14);
                } catch (IOException var15) {
                    boolean statusCode = false;
                    NetworkResponse networkResponse = null;
                    if(httpResponse == null) {
                        throw new NoConnectionError(var15);
                    }
      
                    int statusCode1 = httpResponse.getStatusLine().getStatusCode();
                    VolleyLog.e("Unexpected response code %d for %s", new Object[]{Integer.valueOf(statusCode1), request.getUrl()});
                    if(responseContents == null) {
                        throw new NetworkError(networkResponse);
                    }
      
                    networkResponse = new NetworkResponse(statusCode1, (byte[])responseContents, responseHeaders, false);
                    if(statusCode1 != 401 && statusCode1 != 403) {
                        if(statusCode1 != 400 && statusCode1 != 404 && statusCode1 != 406 && statusCode1 != 501) {
                            throw new ServerError(networkResponse);
                        }
        
                            throw new ClientError(networkResponse);
                        }
        
                    attemptRetryOnException("auth", request, new AuthFailureError(networkResponse));
                }
                }
           }
      
    • 这个方法是网络请求的具体实现,以一个while大循环包裹,其中httpResponse = this.mHttpStack.performRequest(request, e);是主要的网络请求代码,由HttpStack发出。在创建对象的时候传入的对象。以下具体说明

    • 创建 BasicNetwork 对象的时候,需要传入一个HttpStack的对象。
      HttpStack stack;
      ...
      if(stack == null) {
      if(VERSION.SDK_INT >= 9) {
      stack = new HurlStack();
      } else {
      stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
      }
      }

        BasicNetwork network1 = new BasicNetwork((HttpStack)stack);
      
    • 可以看出在 Froyo(2.2) 之前,采用基于 HttpClient 的 HttpClientStack,之后使用基于 HttpURLConnection 的 HurlStack,原因呢是因为Froyo(2.2) HttpURLConnection 有个重大 Bug,调用 close() 函数会影响连接池,导致连接复用失效,所以在 Froyo 之前使用 HttpURLConnection 需要关闭 keepAlive。

        private void disableConnectionReuseIfNecessary() {    
        // 这是一个2.2版本之前的bug    
        if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {    
            System.setProperty("http.keepAlive", "false");    
                }    
        }
      
    • 另外 Gingerbread(2.3) HttpURLConnection 默认开启了 gzip 压缩,提高了 HTTPS 的性能,Ice Cream Sandwich(4.0) HttpURLConnection 支持了请求结果缓存。再加上 HttpURLConnection 本身 API 相对简单,所以对 Android 来说,在 2.3 之后建议使用 HttpURLConnection,之前建议使用 AndroidHttpClient。下面我们来具体说说网络请求接口HttpStack。

    HttpStack接口

    HttpStack接口中也只有一个方法HttpResponse performRequest(Request<?> var1, Map<String, String> var2) throws IOException, AuthFailureError;
    在前文我们提到了HttpStack的两个实现类 HttpClientStack 和 HurlStack,在实现的具体方法中真正进行了网络请求。

    • HurlStack中封装了HttpURLConnection

      ...
      URL parsedUrl1 = new URL(url);
      HttpURLConnection connection = this.openConnection(parsedUrl1, request);
      Iterator responseCode = map.keySet().iterator();
      ...

    • HttpClientStack中封装了HttpClient,也就是传进来的AndroidHttpClient。

    Request的parseNetworkResponse方法

    以上步骤,在NetworkDispatcher中收到了NetworkResponse这个返回值后又会调用Request的parseNetworkResponse()方法来解析NetworkResponse中的数据,同时将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也肯定不同。

    ResponseDelivery接口

    在获取了解析后的数据之后,NetworkDispatcher的run()方法最后调用了this.mDelivery.postResponse(request, response);将数据post到主线程(UI线程),其中的 mDelivery 就是 ResponseDelivery接口的具体实现类 ExecutorDelivery对象,具体的方法如下:

    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }
    

    ExecutorDelivery 的构造方法如下:

    public ExecutorDelivery(final Handler handler) {
        this.mResponsePoster = new Executor() {
            public void execute(Runnable command) {
                handler.post(command);
            }
        };
    }
    

    在创建其实例的时候传入了 RequestQueue 实例的 mDelivery :

    NetworkDispatcher networkDispatcher = new NetworkDispatcher(this.mNetworkQueue, this.mNetwork, this.mCache, this.mDelivery);
    

    而 mDelivery 就是创建 RequestQueue 时创建的主线程Handler:

    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }
    

    如此就保证了 ExecutorDelivery 对象中的 run() 方法在主线程中运行。

    而在 run() 方法中最核心的代码就是:

    if(this.mResponse.isSuccess()) {
        this.mRequest.deliverResponse(this.mResponse.result);
    } else {
        this.mRequest.deliverError(this.mResponse.error);
    }
    

    调用了Request的 deliverResponse(this.mResponse.result)mRequest.deliverError(this.mResponse.error) 方法,其中就调用了需要重写的实现Listener接口方法,将反馈发送到回调到UI线程。

    这是后来想起加的一张NetworkDispatcher的工作流程图:

    NetworkDispatcher.png

    有什么不对还请指出,谢谢指教。

    相关文章

      网友评论

        本文标题:Volley源码解析

        本文链接:https://www.haomeiwen.com/subject/ohluuttx.html