再讀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)蜓斧,這個過程計算并記錄了兩個時間仓蛆,softExpire
和finalExpire
,分別控制的是緩存需要刷新的時間和最終過期的時間:
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
都是直接先寫文件拂到,然后再存到LinkedHashMap
,remove
則是刪除文件码泞,再移除出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
是通過分離鏈接法來處理的御滩,先挖個坑