再讀Volley-緩存策略

再讀Volley

第一次看Volley的代碼的時候只是大概地理清了它的結(jié)構(gòu)而沒有做細節(jié)上的記錄,但是看了這兩篇文章之后嗦篱,面試后的總結(jié)Android網(wǎng)絡(luò)請求心路歷程冰单,發(fā)現(xiàn)Volley還有很多可以學習的地方,所以再次研讀

HTTP緩存機制

下面的頭信息中涉及到的時間的字段都是RFC1123格式灸促,Android開發(fā)可以用這方法來解析org.apache.http.impl.cookie.DateUtils.parseDate(dateStr).getTime()

相關(guān)的Request字段

請求頭字段 意義
If-None-Match: "737060cd8c284d8af7ad3082f209582d" 緩存文件的Etag(Hash)值诫欠,與服務(wù)器回應(yīng)的Etag比較判斷是否改變
If-Modified-Since: Sat, 29 Oct 2010 19:43:31 GMT 緩存文件的最后修改時間

相關(guān)的Response字段

響應(yīng)字段 意義
Cache-Control: max-age=600, stale-while-revalidate=30 告訴所有的緩存機制是否可以緩存及哪種類型
Expires: Thu, 01 Dec 2010 16:00:00 GMT 響應(yīng)過期的日期和時間
Last-Modified: Tue, 15 Nov 2010 12:45:26 GMT 請求資源的最后修改時間
ETag: "737060cd8c284d8af7ad3082f209582d" 請求變量的實體標簽的當前值

HTTP 304狀態(tài)分析

所請求的資源未修改,服務(wù)器返回此狀態(tài)碼時浴栽,不會返回任何資源荒叼。客戶端通常會緩存訪問過的資源典鸡,通過提供一個頭信息指出客戶端希望只返回在指定日期之后修改的資源((一般是提供If-Modified-Since頭表示客戶只想比指定日期更新的文檔)

Volley是如何處理Http緩存機制

請求返回階段

網(wǎng)絡(luò)(或者獲取緩存)請求成功后會調(diào)用parseNetworkResponse方法被廓,需要用戶把NetworkResponse對象轉(zhuǎn)換成Response<T>對象,除了目標的結(jié)果類型T外萝玷,還需要一個Cache.Entity類型的參數(shù)來構(gòu)造一個Response對象嫁乘,一般只需要使用Volley提供的Api來生成Cache.Entity昆婿,如下是lib提供的StringRequest類的parseNetworkResponse方法實現(xiàn):

StringRequest.java
@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(response)方法用來構(gòu)造Cache.Entity對象,下面是它的實現(xiàn)蜓斧,這個過程計算并記錄了兩個時間仓蛆,softExpirefinalExpire,分別控制的是緩存需要刷新的時間和最終過期的時間:

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;

    String serverEtag = null;
    String headerValue;

    headerValue = headers.get("Date");//請求發(fā)送的日期和時間
    if (headerValue != null) {
        serverDate = parseDateAsEpoch(headerValue);
    }
    //對于這樣的Cache-Control:  max-age=600, stale-while-revalidate=30挎春,指示著在600秒內(nèi)數(shù)據(jù)是最新的看疙,
    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;    //no-cache和no-store代表服務(wù)器五支持緩存,直接返回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")) {
                maxAge = 0;
            }
        }
    }

    headerValue = headers.get("Expires");//響應(yīng)過期的日期和時間
    if (headerValue != null) {
        serverExpires = parseDateAsEpoch(headerValue);
    }

    headerValue = headers.get("Last-Modified");//請求資源的最后修改時間
    if (headerValue != null) {
        lastModified = 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) {//是否有Cache-Control頭直奋,且非no-cache和no-store方式
        softExpire = now + maxAge * 1000;
        finalExpire = softExpire + staleWhileRevalidate * 1000;
    } else if (serverDate > 0 && serverExpires >= serverDate) {
        // Default semantic for Expire header in HTTP specification is softExpire.
        softExpire = now + (serverExpires - serverDate);
        finalExpire = softExpire;
    }

    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;
}

解析成Response對象之后能庆,就是把需要緩存的實體放入緩存,具體實現(xiàn)在BasicNetwork類中:

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

請求階段

對于需要使用緩存的請求會想加入到緩存隊列脚线,由CacheDispatcher來分發(fā)處理搁胆,其處理過程,分了三種情況

  • 沒有緩存邮绿,那就把請求加入到網(wǎng)絡(luò)請求隊列
  • 緩存過期丰涉,那就把請求加入到網(wǎng)絡(luò)請求隊列,還添加了必要的緩存信息斯碌,用于控制請求頭信息
  • 緩存需要更新了一死,先馬上返回數(shù)據(jù)給用戶讓用戶展示,然后接著就把請求加入到網(wǎng)絡(luò)請求隊列
CacheDispatcher.java

@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();
            //...省略部分代碼...
            // 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()) {//判斷依據(jù):this.ttl < System.currentTimeMillis()
                request.addMarker("cache-hit-expired");
                //雖然失效了傻唾,但還是為請求設(shè)置了緩存實體投慈,用來在請求的時候控制請求頭信息
                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()) {//判斷依據(jù)this.softTtl < System.currentTimeMillis()
                // 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;
        }
    }
}

從上面可知,當緩存過期或者需要更新的時候冠骄,會再次請求網(wǎng)絡(luò)請求伪煤,這時候會通過記錄的緩存實體的信息構(gòu)造請求頭,告訴服務(wù)器一些信息判斷是否需要返回新的消息實體

添加Cache相關(guān)頭信息
BasicNetwork.java

//這個是在請求可以從緩存查到凛辣,但是需要更新抱既,構(gòu)造和cache相關(guān)的header
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));
    }
}
處理網(wǎng)絡(luò)請求結(jié)果

這過程會根據(jù)Cache.Entity實體添加頭信息,處理304返回碼更新緩存頭信息扁誓,還有重試策略

@Override
public NetworkResponse performRequest(Request<?> request) throws VolleyError {
    long requestStart = SystemClock.elapsedRealtime();
    while (true) {
        HttpResponse httpResponse = null;
        byte[] responseContents = null;
        Map<String, String> responseHeaders = Collections.emptyMap();
        try {
            // Gather headers.
            Map<String, String> headers = new HashMap<String, String>();
            //在請求可以從緩存查到防泵,但是需要更新,構(gòu)造和cache相關(guān)的header
            addCacheHeaders(headers, request.getCacheEntry());
            httpResponse = mHttpStack.performRequest(request, headers);
            StatusLine statusLine = httpResponse.getStatusLine();
            int statusCode = statusLine.getStatusCode();

            responseHeaders = convertHeaders(httpResponse.getAllHeaders());
            // Handle cache validation.304
            if (statusCode == HttpStatus.SC_NOT_MODIFIED) {
                //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);
            }

            // 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,
                    SystemClock.elapsedRealtime() - requestStart);
        } 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, SystemClock.elapsedRealtime() - requestStart);
                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);
            }
        }
    }
}

本地緩存的分析

默認的緩存實現(xiàn)使用DiskBasedCache類蝗敢,需要在構(gòu)造的時候指定緩存目錄和緩存大薪菖ⅰ(默認5M),內(nèi)部維護了一個LinkedHashMap<String, CacheHeader>類型的緩存表寿谴,在初始化的時候遍歷讀取緩存目錄下所有文件锁右,以一定的格式讀取(靜態(tài)方法readHeader)構(gòu)造成CacheHeader對象,并保存在LinkedHashMap中咏瑟,另外一些需要注意的是每次的put都是直接先寫文件拂到,然后再存到LinkedHashMapremove則是刪除文件码泞,再移除出LinkedHashMap谆焊,所以盡量在非UI線程上操作

public static CacheHeader readHeader(InputStream is) throws IOException {
    CacheHeader entry = new CacheHeader();
    int magic = readInt(is);
    if (magic != CACHE_MAGIC) {
        // don't bother deleting, it'll get pruned eventually
        throw new IOException();
    }
    entry.key = readString(is);
    entry.etag = readString(is);
    if (entry.etag.equals("")) {
        entry.etag = null;
    }
    entry.serverDate = readLong(is);
    entry.ttl = readLong(is);
    entry.softTtl = readLong(is);
    entry.responseHeaders = readStringStringMap(is);

    try {
        entry.lastModified = readLong(is);
    } catch (EOFException e) {
        // the old cache entry format doesn't know lastModified
    }

    return entry;
}

如何控制緩存大小

這個應(yīng)該是比較好做,因為存入的數(shù)據(jù)大小我們是知道的浦夷,而且當前緩存庫總大小也是有記錄的mTotalSize字段,所以每次加入緩存前辜王,先做好判斷是否需要移除部分近期最少使用的緩存信息(LinkedHashMap可以通過最后更新時間或者插入時間的順序來遍歷的),實現(xiàn)劈狐,唯一有疑問的是為什么乘以HYSTERESIS_FACTOR,為了避免進行擴容呐馆?肥缔,暫時不得而知:

private void pruneIfNeeded(int neededSpace) {
    if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
        return;
    }
    long before = mTotalSize;
    int prunedFiles = 0;
    long startTime = SystemClock.elapsedRealtime();

    Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, CacheHeader> entry = iterator.next();
        CacheHeader e = entry.getValue();
        boolean deleted = getFileForKey(e.key).delete();
        if (deleted) {
            mTotalSize -= e.size;
        } else {
           VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",e.key, getFilenameForKey(e.key));
        }
        iterator.remove();
        prunedFiles++;
        if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
            break;
        }
    }
}

Volley緩存命中率的優(yōu)化

上面的方式是通過LRU算法來控制緩存庫大小的,如果讓你去設(shè)計Volley的緩存功能汹来,你要如何增大它的命中率续膳?面試后的總結(jié)也提出了一個很好的方案,先嘗試去刪除已經(jīng)失效的緩存收班,但這可能需要兩次遍歷操作坟岔,是否有不妥?或者增加一個TreeMap來記錄摔桦,先對TreeMap進行順序輸出來刪除(如果遇到?jīng)]過期就BREAK)社付,按照過期時間來作為KEY(KEY的選擇有問題),但是這個方案也不太好邻耕,雖然遍歷上可能可以省部分時間鸥咖,但需要同時刪除兩個MAP中的實體的時候,KEY對不上

Volley緩存文件名的計算

面試后的總結(jié)中提到是為了避免hash沖突兄世,之前面試的時候也被面試官提問過類似問題啼辣,不太了解這方面的信息,只了解HashMap是通過分離鏈接法來處理的御滩,先挖個坑

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸥拧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子削解,更是在濱河造成了極大的恐慌住涉,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钠绍,死亡現(xiàn)場離奇詭異舆声,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門媳握,熙熙樓的掌柜王于貴愁眉苦臉地迎上來碱屁,“玉大人,你說我怎么就攤上這事蛾找∶淦ⅲ” “怎么了?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵打毛,是天一觀的道長柿赊。 經(jīng)常有香客問我,道長幻枉,這世上最難降的妖魔是什么碰声? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮熬甫,結(jié)果婚禮上胰挑,老公的妹妹穿的比我還像新娘。我一直安慰自己椿肩,他們只是感情好瞻颂,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著郑象,像睡著了一般贡这。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上厂榛,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天藕坯,我揣著相機與錄音,去河邊找鬼噪沙。 笑死炼彪,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的正歼。 我是一名探鬼主播辐马,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼局义!你這毒婦竟也來了喜爷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤萄唇,失蹤者是張志新(化名)和其女友劉穎檩帐,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體另萤,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡湃密,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年诅挑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片泛源。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡拔妥,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出达箍,到底是詐尸還是另有隱情没龙,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布缎玫,位于F島的核電站硬纤,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏赃磨。R本人自食惡果不足惜筝家,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望煞躬。 院中可真熱鬧,春花似錦逸邦、人聲如沸恩沛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽雷客。三九已至,卻和暖如春桥狡,著一層夾襖步出監(jiān)牢的瞬間搅裙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工裹芝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留部逮,地道東北人。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓嫂易,卻偏偏與公主長得像兄朋,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子怜械,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內(nèi)容