Volley网络缓存详解

Volley网络缓存详解

我为何会研究Volley中的网络缓存?

因为我这里做一个网络操作,由于服务器端实现实在是太垃圾,一个接口获取数据需要一两秒,然后产品需求是一次性需要查七次,那么一次弄下来就会耗时10秒左右,但是如果有看过Volley代码的朋友都会知道,Volley是默认4个线程同时从队列中take出Request进行网络请求的,理论上肯定是7个请求,只用分两次就会全部发出去的,但是实际操作中会发现只会有一两个线程在串行的进行网络请求。所以这里我花了一天的时间来仔细研究了Volley中的网络缓存操作。颇有收获,与大家分享一二,如有不完善或错误,请指出一起讨论。

如何解决本来应该并行但是确串行的网络请求?

先说一下解决方案,一共两种方法解决:
第一种:给Request对象设置不允许缓存,调用setShouldCache(boolean shouldCache)方法,Request中mShouldCache值默认为true,设置为false即可。
第二种:重写Request中的getCacheKey()方法,为每一个请求设置不同的CacheKey,因为如果不重写的话,这个方法默认返回url作为CacheKey,而我之前每一个请求都是访问的同一个url,所以这里修改之后能够解决这个问题,当然为什么需要修改,请继续往下看。

这里还有问题

上面说到的解决方法,第一种是不允许缓存,第二种是允许缓存,但是你实际上如果使用第二种方法的话,你会发现实际上你的缓存数据根本没用的,也就是说,其实Volley给你写到本地缓存上面了,但是读取出来是无效的。这里因为Cache.Entry.ttl值为0,这个值是缓存有效期,单位为毫秒,Entry中对缓存是否有效的判断实现代码如下:

   public boolean isExpired() {
       return this.ttl < System.currentTimeMillis();
   }

如果小于当前系统时间,则为失效。这个ttl的值是Volley根据发出网络请求的时间加上Response中header里面cache-control: max-age的值相加得出的有效期,原本来说这里的cache-control应该是从服务器返回的,但是实际应用中,很难找到后台开发人员专门为前端返回相应的字段,所以这里本地也应该要有解决方法,其实你应该也猜到需要怎么做了,我们这里只要在本地得到Response后去写入缓存这里的过程中改变Response的header值即可。具体方法最简单应该是在初始化Volley队列的时候传入自定义的HttpStack,继承BasicNetwork,重写方法如下:

      @Override
      public NetworkResponse performRequest(Request<?> request) throws VolleyError {
          long maxAge = 0;
          try {
               String age = request.getHeaders().get("max-age");
               if (!AntiDataUtils.isEmpty(age)) {
                   maxAge = Long.parseLong(request.getHeaders().get("max-age"));
               }
           } catch (Exception e) {
               e.printStackTrace();
           }
           NetworkResponse response = super.performRequest(request);
           List<Header> headerList = new ArrayList<>();
           headerList.addAll(response.allHeaders);
           headerList.add(new Header("Cache-Control", "max-age=" + maxAge));
           return new NetworkResponse(response.statusCode, response.data, response.notModified, response.networkTimeMs, headerList);
      }

Volley网络缓存流程

首先需要了解Volley整体结构,Volley的整体结构简单而巧妙,通过两个同步队列实现多线程的网络请求。使用过Volley的朋友都应该了解,在使用Volley进行网络请求之前,需要初始化一个RequestQueue,在RequestQueue对象中主要成员如下:

    //取缓存的队列,如果当前网络请求需要缓存,则添加到此队列
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
            new PriorityBlockingQueue<>();

    //取网络结果的队列,如果当前网络请求不需要缓存则直接添加到此队列
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
            new PriorityBlockingQueue<>();

    //发送网络请求的线程数组,这代表支持多线程网络,可以通过构造方法传入数组长度,即为支持的线程个数,默认为4
    private final NetworkDispatcher[] mDispatchers;

    //取缓存的线程
    private CacheDispatcher mCacheDispatcher;

如果说是不用缓存的网络请求,在放进队列之后,这里的流程很简单,在NetworkDispatcher中会马上被take出,取得网络结果后给出回调,如果是需要缓存的网络请求,这里就会比较复杂一点。
首先是CacheDispatcher会先从mCacheQueue中take一个request,然后根据CacheKey(如果没有则默认为url)读取本地缓存:

  • 如果没有缓存文件,则Entry==null,在这时,会将request直接再放入mNetworkQueue,NetworkDispatcher会马上去take出来进行正常的网络请求。
  • 如果有缓存文件:
    • 如果缓存有效,则回调缓存结果
    • 如果缓存过期,则将request放入 mNetworkQueue。

这里是最简单的逻辑,但是似乎会有问题,如果我同时放10个CacheKey(url)相同的request进来,那按照这样的做法,如果这10个request都是可以缓存的数据,比如说查询一个当日有效的结果,同时查询几次,其实这里除了第一个需要从网络取数据之外,其余的都可以等待第一个request取到结果并写入缓存后直接从缓存中读取,这样效率明显会高很多。这样一个常识性的操作,难道google大佬没想到吗?那答案肯定是否定的,大佬终究是大佬,早就实现这一点了:

        //取得缓存数据
        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);
            }
            return;
        }

        // If it is completely expired, just send it to the network.
        if (entry.isExpired()) {
            request.addMarker("cache-hit-expired");
            request.setCacheEntry(entry);
            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                mNetworkQueue.put(request);
            }
            return;
        }

这里是CacheDispatcher中的操作,这里的关键就在于mWaitingRequestManager.maybeAddToWaitingRequests(request)这一句代码,如果之前还有相同CacheKey的request正在请求,那么就把当前request堆积起来,堆积起来之后呢?当然是等待上一个request完成啊。NetworkDispatcher中有如下代码

    private void processRequest() throws InterruptedException {
        // Take a request from the queue.
        Request<?> request = mQueue.take();
        long startTimeMs = SystemClock.elapsedRealtime();
        try {
            request.addMarker("network-queue-take");

            // If the request was cancelled already, do not perform the
            // network request.
            if (request.isCanceled()) {
                request.finish("network-discard-cancelled");
                request.notifyListenerResponseNotUsable();
                return;
            }

            addTrafficStatsTag(request);

            // Perform the network request.
            NetworkResponse networkResponse = mNetwork.performRequest(request);
            request.addMarker("network-http-complete");
            // If the server returned 304 AND we delivered a response already,
            // we're done -- don't deliver a second identical response.
            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                request.finish("not-modified");
                request.notifyListenerResponseNotUsable();
                return;
            }
            // Parse the response here on the worker thread.
            Response<?> response = request.parseNetworkResponse(networkResponse);
            request.addMarker("network-parse-complete");
            // Write to cache if applicable.
            // TODO: Only update cache metadata instead of entire record for 304s.
//            if (request.shouldCache() && response.cacheEntry != null) {
//                mCache.put(request.getCacheKey(), response.cacheEntry);
//                request.addMarker("network-cache-written");
//            }
            // Post the response back.
            request.markDelivered();
            mDelivery.postResponse(request, response);
            request.notifyListenerResponseReceived(response);
        } catch (VolleyError volleyError) {
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            parseAndDeliverNetworkError(request, volleyError);
            request.notifyListenerResponseNotUsable();
        } catch (Exception e) {
            VolleyLog.e(e, "Unhandled exception %s", e.toString());
            VolleyError volleyError = new VolleyError(e);
            volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
            mDelivery.postError(request, volleyError);
            request.notifyListenerResponseNotUsable();
        }
    }

注意这一句代码:request.notifyListenerResponseNotUsable();可以看到在很多地方都调用了这一句,那么这个方法的具体实现是怎样的呢?

    /**
     * Notify NetworkRequestCompleteListener that the network request did not result in a response
     * which can be used for other, waiting requests.
     */
    /* package */ void notifyListenerResponseNotUsable() {
        NetworkRequestCompleteListener listener;
        synchronized (mLock) {
            listener = mRequestCompleteListener;
        }
        if (listener != null) {
            listener.onNoUsableResponseReceived(this);
        }
    }

这里回调了listener.onNoUsableResponseReceived(this);这个方法的实现在那里呢?想必很容易能够猜到了,就在CacheDispatcher中,最后来看看CacheDispatcher中比较重要的两个方法:


        /**
         * No valid response received from network, release waiting requests.
         */
        @Override
        public synchronized void onNoUsableResponseReceived(Request<?> request) {
            String cacheKey = request.getCacheKey();
            List<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
            if (waitingRequests != null && !waitingRequests.isEmpty()) {
                if (VolleyLog.DEBUG) {
                    VolleyLog.v(
                            "%d waiting requests for cacheKey=%s; resend to network",
                            waitingRequests.size(), cacheKey);
                }
                Request<?> nextInLine = waitingRequests.remove(0);
                mWaitingRequests.put(cacheKey, waitingRequests);
                nextInLine.setNetworkRequestCompleteListener(this);
                try {
                    mCacheDispatcher.mNetworkQueue.put(nextInLine);
                } catch (InterruptedException iex) {
                    VolleyLog.e("Couldn't add request to queue. %s", iex.toString());
                    // Restore the interrupted status of the calling thread (i.e. NetworkDispatcher)
                    Thread.currentThread().interrupt();
                    // Quit the current CacheDispatcher thread.
                    mCacheDispatcher.quit();
                }
            }
        }

        /**
         * For cacheable requests, if a request for the same cache key is already in flight, add it
         * to a queue to wait for that in-flight request to finish.
         *
         * @return whether the request was queued. If false, we should continue issuing the request
         * over the network. If true, we should put the request on hold to be processed when the
         * in-flight request finishes.
         */
        private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
            String cacheKey = request.getCacheKey();
            // Insert request into stage if there's already a request with the same cache key
            // in flight.
            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 {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                request.setNetworkRequestCompleteListener(this);
                if (VolleyLog.DEBUG) {
                    VolleyLog.d("new request, sending to network %s", cacheKey);
                }
                return false;
            }
        }

这里可以看到在最开始取到Entry的时候,就调用过maybeAddToWaitingRequests()方法进行判断,并且在方法中保存了CacheKey相同的request,然后当第一个request请求完成的时候回调onNoUsableResponseReceived()方法,如果存在有效缓存则取出缓存进行结果回调,如果无有效缓存,则再继续进行网络请求。
最后也就解释了,为何使用Volley对同一个接口同时请求多次的时候网络线程并非并行,而是串行的问题了。

总结

其实每一个开源库里面的实现都是非常具有参考价值的,有时候看起来很简单,但是里面很多技术细节是需要我们注意和学习的,比如说上面代码中有一个叫做PriorityBlockingQueue的类,可能大部分人也不知道这个类的特点是什么,或许根本就没注意过这一个类。而我说来也很惭愧,如果不是遇见这个bug,可能我也不会刨根究底的来查看Volley的源码,以前看了个大概,觉得已经很了解这个库了,觉得这个库的实现是很简单的,然而很多细节的地方是真的没有注意到的。这其实也就暴露出我们绝大部分开发者的一个通病,拿着一个库只会去查看怎么使用,而很少去查看它的功能在代码中是如何实现的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值