天天看點

深入volley(二)volley緩存細節http緩存機制Volley緩存處理

上一章講解了volley緩存檔案的結構與内容,沒有看過的朋友請先閱讀 深入Volley(一)volley緩存檔案結構

本章主要内容來講解volley的緩存細節,即volley如何處理c/s結構的http互動;

http緩存機制

要弄明白volley緩存機制,那麼肯定是和浏覽器的緩存機制有關了,簡單來說volley整套架構要做的事都是模拟浏覽器來進行一次次的http互動

     Cache-Control緩存

Cache-Control緩存屬于浏覽器傳回的響應頭字段,他是優先級最高的緩存字段,主要用來控制資源的有效時期,進而決定是浏覽器直接拿緩存資料還是重發請求 其值有: public、private、no-cache、no- store、no-transform、must-revalidate、proxy-revalidate、max-age

深入volley(二)volley緩存細節http緩存機制Volley緩存處理

(圖檔來自網絡) 1.0

            依賴性Last-Modified/If-Modified-Since

Last-Modified:主要用來标記最後修改的時間,屬于相應頭之中 If-Modified-Since:當資源過期的時候(所有緩存過期),響應頭發現有Last-Modified字段,則這時請求頭中帶上該字段,表示請求時間,這時伺服器會将該字段時間和最後修改時間進行一個比對操作,如果發現最後修改時間新一點就是http 200響應,反之則為http 304響應;

           依賴性Etag/If-None-Match(tomcat 7.0新引入)

相比于Last-modified/If-Modified-Since字段來說,則相對優先級比較低一點,它的操作同樣是用來進行确定是200 還是 304; Etag:位于響應頭中,他是伺服器資源唯一的标示符,預設計算是通過對檔案的索引節(INode),大小(Size)和最後修改時間(MTime)進行Hash後得到的。   If-None-Match : 當資源過期時,發現資源是存在Etag響應字段的則再次向伺服器請求時通過該字段帶上Etag的值,伺服器收到後對兩者進行比對,用來決定傳回200還是304; Etag存在的原因:Etag和Last-Modified完全是兩種不同的緩存模式,如果Last-Modified會因為refresh而進行定期重新整理的話、或者說伺服器時間緩存是用的不準确,不推薦用Last-Modified字段的話,那麼用Etag就是最好的選擇;

    Expires緩存

Expires緩存是Http1.0提出來的緩存概念,他的優先級是低于Cache-Control 有朋友之前讨論的時候問我:http1.0的東西在http1.1是不是失效了? 我拿Fidder抓Chrome包

深入volley(二)volley緩存細節http緩存機制Volley緩存處理

對比一下抓IE 8包

深入volley(二)volley緩存細節http緩存機制Volley緩存處理

後面一堆長的省略了 奇怪了:chrome緩存是失效的,IE緩存是有效的,這是因為Chrome抽分的請求加上了cache字段,這些字段導緻了Expires不可用,然後我發現隻要有Expires字段的響應頭,他居然都給加上這樣的東西;具體chrome為什麼我也搞不太懂(搜尋了很久沒找到答案),希望懂的朋友指點一二; Expires具體緩存:Expires位于響應頭裡面,在響應http請求時告訴浏覽器在過期時間前浏覽器可以直接從浏覽器緩存取資料,而無需再次請求。

     浏覽器緩存流程

深入volley(二)volley緩存細節http緩存機制Volley緩存處理

(圖檔來源:http://www.cnblogs.com/skynet/archive/2012/11/28/2792503.html)  這裡還要注明一點:在判斷是否有緩存的時候是先Cache-Control再Expires的如果都沒有就進行依賴性的兩字段判斷操作;

Volley緩存處理

分析了那麼多終于要到正文了,(必須要吐槽下,最近很莫名其妙啊) 這裡必須再強調一點:一切在client有關http頭的應用都是進行模拟浏覽器的操作;是以具體流程細節我會注重描述緩存這一過程; 代碼從RequestQueue的add方法開始,當你add進來一個request的時候,其會根據request.shouldCache進行分發決定是交給NetworkDispatcher還是CacheDispatcher處理

NetworkDispatcher處理

NetworkDispatcher接收到request有兩種途徑: 1.直接接收,通過RequestQueue的分發,一般是第一次請求或者之前請求沒有設定過緩存 2.在CacheDispatcher中處理過的request(其内Cache.Entry被處理過,不為空)發現緩存過期了會丢給NetworkDispatcher;這裡就和304有關 當NetworkDispatcher接收到一個request的時候,其會交給Network子類BasicNetwork調用performRequest處理

NetworkResponse networkResponse = mNetwork.performRequest(request);
           

在BasicNetwork.performRequest重點關注addCacheHeaders(Map<String, String> headers, Cache.Entry entry) 這個方法的調用

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.serverDate > 0) {
            Date refTime = new Date(entry.serverDate);
            headers.put("If-Modified-Since", DateUtils.formatDate(refTime));
        }
    }
           

這裡就和304有關,這裡傳遞進來的request如果Entry 不為空(通過第二種擷取request的方法交給 NetworkDispatcher消費),他就會進行是否有Etag和 Last-Modified設定的操作; 在BasicNetwork調用完addCacheHeaders之後會得到一個Map(即傳遞進去第一個參數,該方法将其裝載),然後他又調用HttpStack的子類(友善起見隻談HurlStack)的performRequest将request和之前得到的map裝進去,

@Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {
        String url = request.getUrl();
        HashMap<String, String> map = new HashMap<String, String>();
        map.putAll(request.getHeaders());
        map.putAll(additionalHeaders);
        if (mUrlRewriter != null) {
            String rewritten = mUrlRewriter.rewriteUrl(url);
            if (rewritten == null) {
                throw new IOException("URL blocked by rewriter: " + url);
            }
            url = rewritten;
        }
        URL parsedUrl = new URL(url);
        HttpURLConnection connection = openConnection(parsedUrl, request);
        for (String headerName : map.keySet()) {
            connection.addRequestProperty(headerName, map.get(headerName));
        }
        setConnectionParametersForRequest(connection, request);
        // Initialize HttpResponse with data from the HttpURLConnection.
        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if (responseCode == -1) {
            // -1 is returned by getResponseCode() if the response code could not be retrieved.
            // Signal to the caller that something was wrong with the connection.
            throw new IOException("Could not retrieve response code from HttpUrlConnection.");
        }
        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                connection.getResponseCode(), connection.getResponseMessage());
        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
        response.setEntity(entityFromConnection(connection));
        for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
            if (header.getKey() != null) {
                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                response.addHeader(h);
            }
        }
        return response;
    }
           

這裡可以看到該方法将所有request的請求頭都裝到了一個map裡面去,之後通過HttpUrlRequest請求得到了一個BasicHttpResponse,這個類不是android封包的類,我看了下,他是屬于org.apache.http.message.BasicHttpResponse;這個包裡面,也就是apache封包的類;之是以這麼做應該是為了與另外httpClient那邊統一一個接口,要不然可複用的代碼會降得很低;不過不管他封裝成什麼類都沒關系,因為之後他會重新封裝; 繼續可以看到從HttpStack傳回的一個HttpResponse在BasicNetwork将會被重新封裝

public BasicNetwork(HttpStack httpStack, ByteArrayPool pool) {
        mHttpStack = httpStack;
        mPool = pool;
    }

    @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        long requestStart = SystemClock.elapsedRealtime();
        while (true) {
            HttpResponse httpResponse = null;
            byte[] responseContents = null;
            Map<String, String> responseHeaders = new HashMap<String, String>();
            try {
                // Gather headers.
                Map<String, String> headers = new HashMap<String, String>();
                addCacheHeaders(headers, request.getCacheEntry());
                httpResponse = mHttpStack.performRequest(request, headers);
                StatusLine statusLine = httpResponse.getStatusLine();
                int statusCode = statusLine.getStatusCode();

                responseHeaders = convertHeaders(httpResponse.getAllHeaders());
                // Handle cache validation.
                if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
                            request.getCacheEntry() == null ? null : request.getCacheEntry().data,
                            responseHeaders, true);
                }

                // Some responses such as 204s do not have content.  We must check.
                if (httpResponse.getEntity() != null) {
                  responseContents = entityToBytes(httpResponse.getEntity());
                } else {
                  // Add 0 byte response as a way of honestly representing a
                  // no-content request.
                  responseContents = new byte[0];
                }

                // if the request is slow, log it.
                long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
                logSlowRequests(requestLifetime, request, responseContents, statusLine);

                if (statusCode < 200 || statusCode > 299) {
                    throw new IOException();
                }
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
            } catch (SocketTimeoutException e) {
                attemptRetryOnException("socket", request, new TimeoutError());
            } catch (ConnectTimeoutException e) {
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (MalformedURLException e) {
                throw new RuntimeException("Bad URL " + request.getUrl(), e);
            } catch (IOException e) {
                int statusCode = 0;
                NetworkResponse networkResponse = null;
                if (httpResponse != null) {
                    statusCode = httpResponse.getStatusLine().getStatusCode();
                } else {
                    throw new NoConnectionError(e);
                }
                VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
                if (responseContents != null) {
                    networkResponse = new NetworkResponse(statusCode, responseContents,
                            responseHeaders, false);
                    if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
                            statusCode == HttpStatus.SC_FORBIDDEN) {
                        attemptRetryOnException("auth",
                                request, new AuthFailureError(networkResponse));
                    } else {
                        // TODO: Only throw ServerError for 5xx status codes.
                        throw new ServerError(networkResponse);
                    }
                } else {
                    throw new NetworkError(networkResponse);
                }
            }
        }
    }
           

看代碼可以看到通過HttpResponse重新封裝,得到三個最有用的内容statusCode,responseContents和responseHeaders:并通過這三個内容封裝成了一個NetworkResponse返還給了NetworkDispatcher;

當NetworkDispatcher接收到NetworkResponse後,先就是進行判斷304的操作

// 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");
                    continue;
                }
           

如果有304的話這次請求就會被消耗掉,響應也是會直接從之前的内容取 如果沒有304的話那麼就需要進行一次新的請求,開始回調request的parseNetworkResponse方法,因為這裡它需要得到一個Response類

@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(NetworkResponse response)靜态方法的調用; parseCacheHeaders的處理其實很簡單,就是将NetworkResponse進行封裝成一個Cache.Entry對象

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

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

        long serverDate = 0;
        long serverExpires = 0;
        long softExpire = 0;
        long maxAge = 0;
        boolean hasCacheControl = false;

        String serverEtag = null;
        String headerValue;

        headerValue = headers.get("Date");
        if (headerValue != null) {
            serverDate = parseDateAsEpoch(headerValue);
        }

        headerValue = headers.get("Cache-Control");
        if (headerValue != null) {
            hasCacheControl = true;
            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.equals("must-revalidate") || token.equals("proxy-revalidate")) {
                    maxAge = 0;
                }
            }
        }

        headerValue = headers.get("Expires");
        if (headerValue != null) {
            serverExpires = parseDateAsEpoch(headerValue);
        }

        serverEtag = headers.get("ETag");

        // Cache-Control takes precedence over an Expires header, even if both exist and Expires
        // is more restrictive.
        if (hasCacheControl) {
            softExpire = now + maxAge * 1000;
        } else if (serverDate > 0 && serverExpires >= serverDate) {
            // Default semantic for Expire header in HTTP specification is softExpire.
            softExpire = now + (serverExpires - serverDate);
        }

        Cache.Entry entry = new Cache.Entry();
        entry.data = response.data;
        entry.etag = serverEtag;
        entry.softTtl = softExpire;
        entry.ttl = entry.softTtl;
        entry.serverDate = serverDate;
        entry.responseHeaders = headers;

        return entry;
    }
           

在request回調parseNetworkResponse的拿到Cache.Entry後封裝成Response傳回NetworkDispatcher; 在 NetworkDispatcher中,再根據其shouldCache和是否有緩存實體來判斷是否要進行緩存操作

// 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");
                }
           

     CacheDispatcher的處理

CacheDispatcher處理就簡單多了

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;

<span style="white-space:pre">		</span>    // 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;
            }
        }
    }
           

當收到一個request之後,會先到Cache裡面去取,看下是否有緩存, 當出現沒有緩存 和 緩存過期的情況就直接丢給 NetworkDispatcher來處理, NetworkDispatcher再會進行304判斷; 如果緩存沒過期,直接拿的緩存實體丢給request的parseNetworkResponse方法這裡調用就和 NetworkDispatcher裡面處理差不多了;

轉載請注明出處: http://blog.csdn.net/u013178436/article/details/38441835

架構示意圖:

深入volley(二)volley緩存細節http緩存機制Volley緩存處理