聊聊Volley源码(缓存流程)

更多关于安卓源码分析文章,请看:安卓源码分析

Volley源码分析系列:
1.聊下Volley源码(整体流程)
2.聊聊Volley源码(网络请求过程)
3.聊聊Volley源码(缓存流程)

上一下谈了Volley网络请求流程聊聊Volley源码(网络请求过程),今天来谈下请求的缓存流程。

首先必须明确的是缓存的概念:缓存是“存贮数据(使用频繁的数据)的临时地方,因为取原始数据的代价太大了,可以取得快一些。”

在讲缓存流程之前,首先需要说明下Http的缓存机制,只有熟悉了Http的缓存机制,才可以理解Volley的缓存机制,因为Volley 构建了一套相对完整的符合 Http 语义的缓存机制。

首先看下Http换缓存相关首部的表:

简单概括缓存机制是这样的:
首次请求得到的响应头有Cache-Control或Expires(前者优先级高于后者,即同时出现以Cache-Control为准),则以后对相同链接再次请求:
Cache-Control的max-age加响应Date首部时间或Expires对比当前时间点,当前时间小于则取本地缓存,否则发起请求。
如果发起请求,则将首次请求得到的响应头Last-Modified的值放入If-Modified-Since中,ETag的值放入If-None-Match进行请求,
服务器根据这两个首部,判断客户端的缓存是否过期,是则返回资源数据,否则返回304响,响应体为客户端本地的缓存数据。

说完了Http缓存机制,继续Volley源码。
一切又是要从RequestQueue的add说起:
不复制整个add方法代码了,直接上重点代码:

 // Insert request into stage if there's already a request with the same cache key in flight.
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new LinkedList<Request<?>>();
                }
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
                }
            } else {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }

当请求需要缓存时(默认需要缓存),就会执行以上代码。mWaitingRequests是存放重复的请求HashMap,如果发现当前的请求还在其中,则将请求添加入该HashMap中,如果当前请求不在该HashMap中,则将对应的cacheKey(就是请求的url)添加到mWaitingRequests中,然后将请求添加到缓存队列 CacheQueue中。(貌似这个上一篇已经讲过了,当做复习。。)

这里请求只是添加到缓存队列,并没有添加到请求队列中。所以下一步就是看 CacheDispatcher中如何处理CacheQueue中的请求。

 @Override
    public void run() {
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // Make a blocking call to initialize the cache.
        mCache.initialize();

        while (true) {
            try {
                // Get a request from the cache triage queue, blocking until
                // at least one is available.
                final Request<?> request = mCacheQueue.take();
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // Attempt to retrieve this item from cache.
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // Cache miss; send off to the network dispatcher.
                    mNetworkQueue.put(request);
                    continue;
                }

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

                // We have a cache hit; parse its data for delivery back to the request.
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // Completely unexpired cache hit. Just deliver the response.
                    mDelivery.postResponse(request, response);
                } else {
                    // Soft-expired cache hit. We can deliver the cached response,
                    // but we need to also send the request to the network for
                    // refreshing.
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }

            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
        }
    }

和网络请求分发类NetworkDispatcher中取请求的流程非常相似,在第一篇Volley源码分析文章中也有讲到,也是循环将请求从缓存队列中取出,执行相应的处理。

扫描二维码关注公众号,回复: 10749059 查看本文章

首先看第七行的:

mCache.initialize();

mCache就是进行缓存的核心类,请求都是通过该类缓存在指定的地方,这里是对其进行初始化工作。这里默认为DiskBasedCache,原理是将数据以流的形式写入到磁盘文件。具体文件位置可以指定,默认在Volley类的newRequestQueue方法创建RequestQueue的时候指定:

 File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);

这里的DEFAULT_CACHE_DIR为“volley”,即在当前应用的Cache目录下创建了volley文件作为DiskBasedCache的缓存目录。

然后在循环中取出请求后,也是判断请求是否被取消,取消则将请求标记为取消并继续取下一个请求。

然后通过CacheKey判断下是否请求已经在缓存中,如果在的话则取出缓存的数据。

 Cache.Entry entry = mCache.get(request.getCacheKey());

首先要说的是请求的缓存都是以Cache接口的内部类Entry 缓存起来的。

public static class Entry {
        /** The data returned from cache. */
        public byte[] data;

        /** ETag for cache coherency. */
        public String etag;

        /** Date of this response as reported by the server. */
        public long serverDate;

        /** The last modified date for the requested object. */
        public long lastModified;

        /** TTL for this record. */
        public long ttl;

        /** Soft TTL for this record. */
        public long softTtl;

        /** Immutable response headers as received from server; must be non-null. */
        public Map<String, String> responseHeaders = Collections.emptyMap();

        /** True if the entry is expired. */
        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        /** True if a refresh is needed from the original data source. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

Entry 是一个很简单的实体类,存储的是和请求判断是否过期相关的属性,那这里的缓存过程是在哪里进行的呢?这很容易猜到,在网络请求成功后。上一篇在网络请求成功后,跳过了缓存的过程,现在就来谈下。

其实在NetworkDispatcher请求成功后,会执行以下语句:

   // Parse the response here on the worker thread.
                Response<?> response = request.parseNetworkResponse(networkResponse);

这是将得到的NetWorkResponse对象解析为具体对象的Response,在这里,具体的Request对象会将其中的数据转化为Cache的Entry对象,比如StringRequest:

@Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

最后一句调用了HttpHeaderParser的parseCacheHeaders,进去看:

public static Cache.Entry parseCacheHeaders(NetworkResponse response) {
        long now = System.currentTimeMillis();

        Map<String, String> headers = response.headers;

        long serverDate = 0;
        long lastModified = 0;
        long serverExpires = 0;
        long softExpire = 0;
        long finalExpire = 0;
        long maxAge = 0;
        long staleWhileRevalidate = 0;
        boolean hasCacheControl = false;
        boolean mustRevalidate = false;

        String serverEtag = null;
        String headerValue;

        //提取出响应头Date,若存在则保存在serverDate
        headerValue = headers.get("Date");
        if (headerValue != null) {
            serverDate = parseDateAsEpoch(headerValue);
        }
        //提取出响应头Cache-Control
        headerValue = headers.get("Cache-Control");
        if (headerValue != null) {
            hasCacheControl = true;
            //取出Cache-Control头相应的值
            String[] tokens = headerValue.split(",");
            for (int i = 0; i < tokens.length; i++) {
                String token = tokens[i].trim();
                //不取缓存数据或不进行缓存
                if (token.equals("no-cache") || token.equals("no-store")) {
                    return null;
                    //最大的有效时间
                } else if (token.startsWith("max-age=")) {
                    try {
                        maxAge = Long.parseLong(token.substring(8));
                    } catch (Exception e) {
                    }
                } else if (token.startsWith("stale-while-revalidate=")) {
                    try {
                        staleWhileRevalidate = Long.parseLong(token.substring(23));
                    } catch (Exception e) {
                    }
                } else if (token.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                    mustRevalidate = true;
                }
            }
        }
        //提取出响应头Expires,若存在则保存在serverExpires
        headerValue = headers.get("Expires");
        if (headerValue != null) {
            serverExpires = parseDateAsEpoch(headerValue);
        }
        //提取出响应头Last-Modified,若存在则保存在lastModified
        headerValue = headers.get("Last-Modified");
        if (headerValue != null) {
            lastModified = parseDateAsEpoch(headerValue);
        }
        //提取出响应头ETag,若存在则保存在serverEtag
        serverEtag = headers.get("ETag");

        // Cache-Control takes precedence over an Expires header, even if both exist and Expires
        // is more restrictive.
        //有Cache-Control响应头的情况下
        if (hasCacheControl) {
            //响应数据缓存最迟有效时间
            softExpire = now + maxAge * 1000;
               //需要进行新鲜度验证最迟时间
            finalExpire = mustRevalidate
                    ? softExpire
                    : softExpire + staleWhileRevalidate * 1000;
          //如果没有Cache-Control响应头,则以Expires的时间为过期时间
        } else if (serverDate > 0 && serverExpires >= serverDate) {
            // Default semantic for Expire header in HTTP specification is softExpire.
            softExpire = now + (serverExpires - serverDate);
            finalExpire = softExpire;
        }
        //将响应体和响应头等相关数据保存在Entry中
        Cache.Entry entry = new Cache.Entry();
        entry.data = response.data;
        entry.etag = serverEtag;
        entry.softTtl = softExpire;
        entry.ttl = finalExpire;
        entry.serverDate = serverDate;
        entry.lastModified = lastModified;
        entry.responseHeaders = headers;

        return entry;
    }

结合注释和前面说的Http缓存机制应该就可以理解。然后在将结果传递到客户端之前执行:

 if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

这样就把生成的Cache的Entry以request的cacheKey为key缓存起来了。

回到CacheDispather的run方法,如果获取到的entry为null,说明当前请求没有被缓存过,就直接将其添加到请求队列中,走上一篇讲到的网络请求流程去验证响应数据新鲜度,然后取缓存队列下一个请求。

如果获取到的Entry对象为不为null,则说明当前请求有缓存结果,所以在30行判断entry.isExpired(),即缓存是否过期(根据前面在HttpHeaderParser 解析的响应头算出来的属性ttl与当前时间的比较),如果过期,执行:

request.setCacheEntry(entry);

将该Entry传递给request,然后将请求重新添加到请求队列中,重新请求。

如果没有过期,再判断是否需要验证新鲜度entry.refreshNeeded()(softTtl与当前时间的比较),不用验证新鲜度则直接将缓存的数据传递到客户端线程,需要刷新则还是将将该Entry传递给request,然后请求添加到请求队列去验证响应数据新鲜度。

执行请求的BasicNetwork的 performRequest方法中,调用了 addCacheHeaders方法:

private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
        // If there's no cache entry, we're done.
        if (entry == null) {
            return;
        }

        if (entry.etag != null) {
            headers.put("If-None-Match", entry.etag);
        }

        if (entry.lastModified > 0) {
            Date refTime = new Date(entry.lastModified);
            headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
        }
    }

就是在请求存在Entry对象的情况下(即请求是由缓存中取出),添加If-None-Match请求头,值为原来响应头Etag的值,以及If-Modified-Since请求头,值为原来响应头Last-Modified的值。

通过这两个请求头,就像前面讲Http缓存机制一样,服务端和资源的更新时间进行比较,如果发现资源的Etag和Last-Modified一致,则认定缓存有效,则返回响应码为304的响应,并且客户端会将请求携带的Entry中的数据(响应实体)和响应头作为新的响应返回:

 responseHeaders = convertHeaders(httpResponse.getAllHeaders());
                // Handle cache validation.
                if (statusCode == HttpStatus.SC_NOT_MODIFIED) {

                    Entry entry = request.getCacheEntry();
                    if (entry == null) {
                        return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
                                responseHeaders, true,
                                SystemClock.elapsedRealtime() - requestStart);
                    }

                    // A HTTP 304 response does not have all header fields. We
                    // have to use the header fields from the cache entry plus
                    // the new ones from the response.
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
                    entry.responseHeaders.putAll(responseHeaders);
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
                            entry.responseHeaders, true,
                            SystemClock.elapsedRealtime() - requestStart);
                }

这里就是取出新响应的状态码后执行的代码,HttpStatus.SC_NOT_MODIFIED就是304,所以在拿到304响应状态码后,利用原来缓存的响应实体和头构建一个NetworkResponse返回。

剩下的工作就和网络请求成功后的流程一样了。
介于个人对于Http缓存机制还不是很熟悉,可能有说错或者遗漏额地方,希望各位指正。

发布了69 篇原创文章 · 获赞 76 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/sinat_23092639/article/details/57599954