美文网首页Android开发经验谈Android开发Android知识
Volley源码阅读——(一)为何适用于多而小

Volley源码阅读——(一)为何适用于多而小

作者: lanzry | 来源:发表于2017-09-30 15:56 被阅读208次

    Volley算是比较简单的http库,所以决定从Volley入手去开始读源码之路。
    打算写一系列Volley源码阅读的文章,顺序按照我的源码阅读顺序。这是第一章,为何适用于多而小。
    本章主要介绍我阅读源码的起始过程,理解Volley的初始化工作和Volley中请求线程的工作原理。

    1. 初步浏览 - 接口

    接口 实现类 主要方法/作用
    Cache NoCache
    NetWork BasikNetWork performRequest()
    MockNetWork
    ResponseDelivery ExecutorDelivery postResponse()/postError()
    RetryPolicy DefaultRetryPolicy
    toolbox.HttpStack HttpClientStack 使用HttpClient来performRequest
    HurlStack 使用HttpURLConnection来performRequest
    MockHttpStack
    toolbox.Authenticator DefaultAuthenticator

    Volley源码算是比较少的,我也没有看源码的经验。于是先将接口列出,试图知道设计人员一开始都想做些啥,这个方法卓有成效。

    接口列出如上,如何快速找到接口呢?我用的Idea看的源码,里面非常清晰,如下图:



    图中1处绿色写着“I”就是Interface接口,甚至还有2处圆圈两边灰色的,打开Request类会发现这是个抽象类。
    那下面把抽象类也列一下:

    抽象类 实现类 主要方法/作用
    Request ClearCacheRequest
    ImageRequest
    StringRequest
    JsonRequest JsonArrayRequest
    JsonObjectRequest

    比较特殊的是JsonRequest也是Request的子类,同时它也是一个抽象类,再分别由JsonArrayRequest和JsonObjectRequest实现。

    这些东西列出来,可能就对框架已经有了初步的了解,比如:

    • 这个框架可能做了缓存 —— 基于Cache接口
    • 用NetWork还是HttpStack来实现请求? —— 基于二者都有的performRequest()方法
    • 应答有一个分发机制 —— 基于ResponseDelivery
    • 有重发机制 —— 基于RetryPolicy
    • 可能还有认证机制 —— 基于Authenticator

    这些东西先有个印象,有个印象能加快后面的进度。

    2. 简单使用

    网上一搜一大把,这里建议大家看博客的时候,找一些排版比较好的。那些连代码格式都没有的就不要去看了。

    参考Android Volley完全解析(一),初识Volley的基本用法,这个是我搜到的百度的第一个。

    第三条 StringRequest介绍到:

    (1). 初始化,创建RequestQueue对象

    RequestQueue mQueue = Volley.newRequestQueue(context);  
    

    (2). 创建StringRequest请求,并add进RequestQueue

    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);  
                                }  
                            });  
    mQueue.add(stringRequest);  
    

    3. 参考简单使用理解初始化工作

    看2中代码,初始化仅仅创建了一个RequestQueue,这里面完成了什么工作呢?

    43行:初始化了默认的缓存目录
    53-59行:根据不同的sdk版本创建不同的HttpStack(主要的实现网络请求的对象)
    63行:以HttpStack对象作为参数新建NetWork对象

    猜想:这应该是NetWork对象调用HttpStack对象实现网络请求了

    65行:以DiskBasedCache对象和netWork对象作为参数创建了RequestQueue对象
    66行:RequestQueue执行start方法

    理一理思绪:

    • 简单说就是创建了缓存对象DiskBasedCache和网络请求对象BasicNetwork,然后传给RequestQueue的构造器创建了RequestQueue对象。
    • BasicNetWork对象好像代理了HttpStack对象来执行http请求的具体实现
    • 非常贴心地可以传入HttpStack对象,表示开发人员可以自己选择http请求的具体实现
    • RequestQueue对象执行了start方法,不知道做了什么操作

    现在来找一些比较关心的事情。

    Q: 网络请求到底怎么执行的?NetWork和HttpStack到底是如何工作的?

    Volley只是一个封装库,底层的http请求还是调用JDK接口。基于这样的背景,去看一下NetWork和HttpStack的performRequest()方法,这个方法一看起来就像是执行http请求的。

    BasicNetWork的performRequest()


    发现关键的httpResponse还是由HttpStack实现的,所以只要看HttpStack。

    HurlStack的performRequest()
    ps. 由于14号字体无法截全方法,不得不缩小字体

    可以不去管其中的具体实现,找到了其中比较重点的部分:responseStatus和response是依靠HttpURLConnection获得的。我一开始学习Android,就是手写HttpURLConnection来实现get和post方法的。
    反正response是HttpURLConnection实现的,暂时先不求甚解,假装已经拿到了response。

    RequestQueue 构造器做了什么工作?

    StringRequest例子里面是两个参数的RequestQueue构造器

    两个参数的构造器调用了3个参数的构造器方法,并将第三个参数设置为默认4。看注释我们就知道,这个4是4个用来执行请求的线程,大胆猜想,RequestQueue中有一个线程池,默认线程数量是4。
    看3个参数的构造器


    第三个参数确实命名为threadPoolSize。同时加上了第4个参数默认值,一个ResponseDelivery的唯一实现ExecutorDelivery的对象。这个类我们在一开始整理接口的时候就见过,暂时只需要知道用来分发response。

    再看最终的4个参数的构造器


    原来只是赋值给成员变量而已。而且心心念念的线程池也仅仅是一个线程对象数组。
    很好,其实可以发现,一开始整理出来的接口,有3个已经被RequestQueue成员变量实现了:Cache、NetWork、和ResponseDelivery

    queue.start();完成了什么工作?

    start之前都是对象的创建,到这一步,终于要运行起来了吧!


    142行:备注已经说明了,确保关闭dispatcher线程。具体逻辑肯定和start相反,那么就先不care吧
    144-145行:这里居然偷偷创建了一个缓存分发器,进入实现会发现这继承了线程。
    148-152行:这里估计就是关键了,for循环一个一个地创建NetworkDispatcher对象将线程池数组填满,然后再将线程运行起来。

    看到这里想必和我一样不耐烦了,这个dispatcher是个什么东西,线程现在就运行起来,岂不是占资源,而且,怎么保证线程一直在等用户发请求?

    所以还是先看一看,networkDispatcher.start();之后会有什么发生?
    这既然是继承了线程,那直接找到run()方法,好在一开始就看到了关键:

    1. while(true)这是一个死循环
    2. 第二个红框里面应该就是从队列里面拿请求了。所以不断从队列里面拿请求,然后执行。

    这个mQueue是什么对象?队列里面要是没有请求存在又怎么办?
    再看start方法中NetworkDispatcher对象的构造器参数


    这个mNetworkQueue是一个优先级阻塞队列,保存的是Request对象


    NetworkDispatcher中的mQueue确实就是这个mNetworkQueue

    那么不难猜想,mQueue(优先级阻塞队列PriorityBlockingQueue)在为空的时候take会阻塞线程,直到queue不为空。那么进入take()方法去验证一下这个猜想:
    直接点击take方法会跳入BlockingQueue接口里面的方法,如下

    点击左边那个向下箭头就能看到进入实现,我们选择PriorityBlockingQueue:


    我们看到关键的一行,dequeue()方法肯定就是队列弹出一个对象(可以自己去看看具体实现),为空的话,就会await()。
    其中notEmpty对象是一个条件对象,这设计了多线程的锁。


    在PriorityBlockingQueue的构造器内初始化


    这里面的原理需要再开一篇来细讲。这里我们只需要知道阻塞队列为空,当前线程就会被阻塞await()。如何唤醒线程,后文讲一个请求的发送流程继续讲。

    但是默认有4个线程,4个线程都会被阻塞吗?

    我们不妨进入await()方法里面去再深入看一看:
    这里我们需要换一种寻找实现方法的方式,找到notEmpty的初始化



    进入newCondition()方法


    再找到sync对象的声明地方


    !!!这是可重入锁!而且下面就是Sync抽象类的声明,找到其中newCondition()方法


    发现返回的是一个ConditionObject对象,找到这个类的声明并找到其中的await()方法:


    2031行:将当前线程加入条件等待队列
    2034行:检查当前线程node是否还在等待条件,需要等就进入循环执行LovkSupport.Park()来阻塞线程。
    看一看这部分源码,可以理解,线程来一个阻塞一个,就是一个阻塞队列。

    初始化工作也是为了一般工作能够正常进行,这里也保留一个印象,便于后面具体工作展开的理解。

    4. 参考简单使用理解请求如何发送

    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);  
                                }  
                            });  
    mQueue.add(stringRequest);  
    

    再看一看基本使用的代码,我已经写累了,太长了。太长不想看,我也不想写了。看,里面requestQueue.add(request)这样就把请求发送出去了。前面我们知道线程被阻塞住了。那当有请求add进来的时候,是如何唤醒线程的呢?

    找到RequestQueue类中的add方法:


    简单分析一下:
    229:让request对象持有当前RequestQueue对象的引用
    236:addMarker,注意request执行到每一个步骤都会设置一个marker,用来标志请求进行的阶段
    239:是否需要缓存的条件语句,当然先从不需要的看起
    240:mNetworkQueue.add,这个就是关键。
    那240这里add进去了,怎么唤醒线程去执行呢?看源码


    看到最后一个signal方法,这就是和await相对应的唤醒方法。假如还是不太理解这个阻塞和唤醒到底怎么回事,建议和我一样,写一个小demo看看

    打印出来的效果是


    不会无限打印print,也不会打印print finally

    结合初始化那部分,就完整地呈现了多线程如何去实时发送新增的请求的流程。

    结论
    这也解释了为何Volley有利于多而小的请求,毕竟有4个线程可以使用。反而大的请求其实并没有做什么优化,用Volley执行大请求并没有什么优势,反而可能因为太耗时而占用了线程,导致其他小的请求无法及时执行。

    相关文章

      网友评论

        本文标题:Volley源码阅读——(一)为何适用于多而小

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