Android每周一轮子:Volley

作者: Jensen95 | 来源:发表于2018-03-05 16:34 被阅读94次
    Volley

    序言

    2018年谈Volley,可以说是too yong, too simple了,对于网络库,现在使用最多的莫过于OkHttp了,接触使用Volley应该还是大二的时候了。之后也看过其源码,但是在不久前面试的时候,被问到一个Volley库的问题,就是Volley中请求的优先级是如何调度的,却卡住了,当时对于源码的阅读大多只是停留在对于其实现的流程和项目的结构上,而对于其具体的特性和其如何实现了这些特性却没有去了解,相比于功能实现的流程,特性的实现细节也是不可忽略的,甚至可以说这才是一个库的精华之所在,同时对于该网络库的缺陷在于那里,通过了解其优势和缺陷,我们可以更好的扬长避短,充分利用该库。本着该原则,准备对于之前阅读的代码进行一个重新的回顾,暂定的计划为一周拆一个轮子。

    将以其实现流程,特性实现和其缺陷作为主要切入点,进行代码分析。

    Volley 基础使用

    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<String>() {
        @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);
    
    • 创建RequestQueue 请求队列。
    • 创建Request,Volley提供了String,JsonObject等类型,用户可自己继承Reqeust实现自己定义的返回结果类型。
    • 将请求添加到请求队列中。

    经过以上三步,我们就完成了一次网络请求,在注册的监听器的onResponse方法中我们可以拿到请求成功的返回结果和在onErrorResponse方法中得到出错的信息。

    Volley实现

    Volley实现结构图
    • 请求队列的创建
    private static RequestQueue newRequestQueue(Context context, Network network) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();
        return queue;
    }
    
    • 请求添加到队列中
    public <T> Request<T> add(Request<T> request) {
        //请求添加到mCurrentRequests中
        request.setRequestQueue(this);
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }
        //设置请求的Sequence,后期用来比较请求的优先级
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");
    
        //如果请求不需要缓存,直接加入网络请求队列
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }
        //如果需要缓存加入到缓存队列中
        mCacheQueue.add(request);
        return request;
     }
    
    • 调用队列的开启
    public void start() {
        stop(); 
        //创建缓存Dispatcher,并启动
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();
    
        // 根据线程池设置的数目,创建网络请求Dispatcher,并启动
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }
    

    这里首先会关掉之前的Dispatcher,然后重新创建并开启Dispatcher

    • 创建并开启CacheDispatcher
    public void run() {
        mCache.initialize();
        while (true) {
                 //取出请求
                final Request<?> request = mCacheQueue.take();
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }
                //判断Cache未命中,这加入到网络请求队列中
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // Cache miss; send off to the network dispatcher.
                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                        mNetworkQueue.put(request);
                    }
                    continue;
                }
                //缓存过期了,拿到缓存之后,再将该请求放置到网络请求队列中
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                //判断是否需要加入到等待队列,如果需要则不加入网络请求队列
                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                        mNetworkQueue.put(request);
                    }
                    continue;
                }
              //Cache 未过期,Cache命中,这将其包装成NetworkResponse
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");
              //如果缓存不需要更新,直接将结果抛回去,否则检测其是否在等待请求队列中,如果不在执行请求,否则直接抛回
              if (!entry.refreshNeeded()) {
                    mDelivery.postResponse(request, response);
                } else {
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);
                    response.intermediate = true;
    
                    if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
            
                        mDelivery.postResponse(request, response, new Runnable() {
                            @Override
                            public void run() {
                                try {
                                    mNetworkQueue.put(request);
                                } catch (InterruptedException e) {
                                    // Restore the interrupted status
                                    Thread.currentThread().interrupt();
                                }
                            }
                        });
                    } else {
                        mDelivery.postResponse(request, response);
                    }
                }
        }
    }
    
    

    当缓存没有命中的时候,需要发起网络请求,这个时候,通过WaitingRequestManager来进行管理,其维护了一个Map来放置响应的请求,键为请求CacheKey,值为请求。如果Map中不包含该CacheKey,这将其加入,并将值置为Null,如果有,则直接将其加入。第一次置为Null的原因是为了防止在请求归来时,在NetworkDispatcher中执行了一次数据的异步传递,在缓存请求队列处理时再次被处理,通过这种方式也保证了对于同一个请求,只有可能被HttpStack执行一次,而每一个请求设置的成功失败回调都会被用到。

    private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
        String cacheKey = request.getCacheKey();
          //如果请求队列包含该Cachekey,表示已经执行过网络请求
        if (mWaitingRequests.containsKey(cacheKey)) {
            // There is already a request in flight. Queue up.
            List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
            if (stagedRequests == null) {
                stagedRequests = new ArrayList<Request<?>>();
            }
            request.addMarker("waiting-for-response");
            stagedRequests.add(request);
            mWaitingRequests.put(cacheKey, stagedRequests);
            if (VolleyLog.DEBUG) {
                VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
            }
            return true;
        } else {
            //将Value置为null,因为其回调在NetworkDispatcher中被执行了
            mWaitingRequests.put(cacheKey, null);
            request.setNetworkRequestCompleteListener(this);
            if (VolleyLog.DEBUG) {
            return false;
        }
    }
    

    通过为该Request设置setNetworkRequestCompleteListener,当网络请求执行完成的时候,其onResponseReceived函数会被回调到。

    public void onResponseReceived(Request<?> request, Response<?> response) {
        if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
            onNoUsableResponseReceived(request);
            return;
        }
        String cacheKey = request.getCacheKey();
        List<Request<?>> waitingRequests;
        synchronized (this) {
            waitingRequests = mWaitingRequests.remove(cacheKey);
        }
        if (waitingRequests != null) {
            // 将等待的请求结果进行传递
            for (Request<?> waiting : waitingRequests) {
                mCacheDispatcher.mDelivery.postResponse(waiting, response);
            }
        }
    }
    

    这个时候,排队的请求的回调则会被执行。

    • 创建并开启NetworkDispatcher
    public void run() {
        while (true) {
          //取出请求
          Request<?> request = mQueue.take();
            //判断请求是否需要取消
            if (request.isCanceled()) {
                 request.finish("network-discard-cancelled");
                 request.notifyListenerResponseNotUsable();
                  continue;
             }
            //执行网络请求
             NetworkResponse networkResponse = mNetwork.performRequest(request);
            //解析网络请求结果
             Response<?> response = request.parseNetworkResponse(networkResponse);
            //判断是否需要加入缓存
            if (request.shouldCache() && response.cacheEntry != null) {
                 mCache.put(request.getCacheKey(), response.cacheEntry);
                 request.addMarker("network-cache-written");
             }
            //通过Delivery将响应结果传递出去
            mDelivery.postResponse(request, response);
            request.notifyListenerResponseReceived(response);
    
      }
    }
    
    • 请求处理到产生响应
    Volley请求处理过程

    Volley 特性和缺陷

    特性

    • 自动调度网络请求,支持多并发网络请求
    • 透明的磁盘内存响应结果缓存
    • 支持请求优先级调整
    • 支持取消请求,并提供相应API
    • 高度可拓展
    • 网络请求结果异步回调

    缺陷

    • 不适合做大的网络下载请求

    接下来,针对Volley的特性和缺陷,分别展开,从源码进行分析。

    1. 自动调度网络请求,支持多并发网络请求

    在RequestQueue方法中,开启了多个NetworkDispatcher,而每一个Dispatcher都是一个线程。

    NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
     mDispatchers[i] = networkDispatcher;
    networkDispatcher.start();
    

    对于网络请求通过一个优先级阻塞队列存放,每一个线程可以从队列中获取请求,然后执行,最后将响应结果返回。Volley内部管理了这些线程,无需开发者关心。

    1. 透明的磁盘内存响应结果缓存

    对于缓存,Volley支持用户制定自己的缓存规则,通过实现Cache接口,实现自己的缓存。同时提供了一个默认的缓存实现DiskBasedCache。其是如何实现透明的磁盘内存响应的呢?

    首先对于所有的请求,在判断为设置了缓存的,将会加入到缓存队列中,然后从缓存中去取,如果缓存中有则返回,如果没有或者过期则将其加入到网络请求队列中。

    在CacheDispatcher中,我们首先调用了Cache的初始化方法

    mCache.initialize();
    

    之后通过请求的CacheKey从Cache的get方法中获取缓存。其内部实现是内存中维护了一个LinkedHashMap,以请求的CacheKey作为Key,CacheHeader作为值,对于每一个请求内容通过文件的形式存放在磁盘文件中,初始化的时候,读取缓存文件目录,将其加载到内存中,查询的时候,根据内存中的缓存进行判断,从磁盘中加载数据。

    1. 优先级实现

    Volley支持优先级的调整,通过一个PriorityblockingQueue队列,进行调度,将请求加入进来,该队列会根据其存放的Obejct的compare方法对其进行优先级的比较,来确定其先后顺序。

    @Override
    public int compareTo(Request<T> other) {
        Priority left = this.getPriority();
        Priority right = other.getPriority();
    
        // High-priority requests are "lesser" so they are sorted to the front.
        // Equal priorities are sorted by sequence number to provide FIFO ordering.
        return left == right ?
                this.mSequence - other.mSequence :
                right.ordinal() - left.ordinal();
    }
    

    首先根据请求设置的优先级,然后根据其Sequence值,这个值是在时间顺序上递增的。

    1. 任意的取消网络请求

    通过为请求设置cacel字段,在请求执行的时候,对其进行判断,来确定是否为取消了,然后再次执行。

    1. 高度可拓展

    Volley支持对于Cache的拓展,HttpStack的拓展,也就是对于网络请求具体执行类的拓展,这里提供了HttpClient和HttpUrlConnection。支持对于网络请求回调的自定义。支持网络请求解析的自定义。用户可以根据自己的需求,进行响应的插桩拓展。

    1. 网络请求结果异步回调
    • 创建调度器
    new ExecutorDelivery(new Handler(Looper.getMainLooper()));
    
    public ExecutorDelivery(final Handler handler) {
        // Make an Executor that just wraps the handler.
        mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };
    }
    
    • 传递数据
    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }
    

    调度器持有了主线程的Handler,通过Handler的post将我们的数据透传到主线程之中。通过这种方式从而实现异步回调。

    1. 不适合大数据量传递

    在返回的结果,通过byte[]字段中,然后存放在内存之中,然后获取相应的编码方式,将其转化为字符串,这样就导致当网络请求数目增加,返回内容增加的时候,导致内存中的数据量增加,因此Volley不适合进行大数据量的传递,而实现小而频繁的请求。

    new NetworkResponse(statusCode, responseContents, responseHeaders, false,
                            SystemClock.elapsedRealtime() - requestStart);
    

    这里的responseContents为一个byte类型数组。

    总结

    Volley的源码实现比较简单,代码量也不是很大,比较容易阅读和理解,所以Volley作为成了新年以来拆的第一个轮子。拆轮子的过程可以帮助我们学习到功能的实现,同时也可以从中学习到一些设计思想。接下来的计划是每周拆一个轮子,可能是Android也可能是其它方面的。时间充裕,可能会找一些代码量大的,时间少的话,可能就找一些简单轮子来拆。拆分思路为其基础使用,实现原理梳理,特性和缺陷分析。通过这三方面来彻底了解一个轮子的实现。

    相关文章

      网友评论

        本文标题:Android每周一轮子:Volley

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