Volley源码解析

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,548评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,497评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,990评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,618评论 1 296
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,618评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,246评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,819评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,725评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,268评论 1 320
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,356评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,488评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,181评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,862评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,331评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,445评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,897评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,500评论 2 359

推荐阅读更多精彩内容