OkHttp緩存優(yōu)化你的應(yīng)用
Okhttp緩存原理
我們先從HTTP協(xié)議開始入手,關(guān)于緩存的HTTP請求/返回頭由以下幾個,我列了張表格一一解釋
請求頭/返回頭 | 含義 |
---|---|
Cache-Control | 這個字段用于指定所有緩存機制在整個請求/響應(yīng)鏈中 必須服從的指令。 |
Pragma | 與Cache-Control一樣,是兼容HTTP1.0的頭部 |
Expires | 資源過期時間 |
Last-Modified | 資源最后修改的時間 |
If-Modified-Since | 在請求頭中指定一個日期,若資源最后更新時間超過該日期, 則服務(wù)器接受請求,相反的頭為If-Unmodified-Since |
ETag | 識別內(nèi)容版本的唯一字符串,與資源關(guān)聯(lián)的記號 |
與緩存最相關(guān)的Cache-Control有多條指令,并且在請求或返回頭中的效果不一樣
在請求頭中Cache-Control的指令
指令 | 參數(shù) | 說明 |
---|---|---|
no-cache | 無 | 緩存必須向服務(wù)器確認是否過期候才能使用, 即不接受過期緩存,并非不緩存 |
no-store | 無 | 真正意義上的不緩存 |
max-age=[秒] | 必須 | 響應(yīng)的最大age值 |
max-stale=[秒] | 可忽略 | 可接受的最大過期時間 |
min-fresh=[秒] | 必須 | 詢問再過[秒]時間后資源是否過期,若過期則不返回 |
only-if-cached | 無 | 只獲取緩存的資源而不聯(lián)網(wǎng)獲取 |
在返回頭中Cache-Control的指令
指令 | 參數(shù) | 說明 |
---|---|---|
public | 無 | 可向任意方提供響應(yīng)的緩存 |
private | 無 | 向特定用戶提供響應(yīng)緩存 |
no-cache | 可省略 | 不緩存 |
no-store | 無 | 不緩存 |
max-age=[秒] | 必須 | 響應(yīng)的最大age值 |
max-stale=[秒] | 可忽略 | 可接受的最大過期時間 |
min-fresh=[秒] | 必須 | 詢問再過[秒]時間后資源是否過期,若過期則不返回 |
only-if-cached | 無 | 只獲取緩存的資源而不聯(lián)網(wǎng)獲取 |
假設(shè)Okhttp完全遵守HTTP協(xié)議(實際上應(yīng)該也是),利用Cache-Control我們可以緩存某些必要的資源.
1.有網(wǎng)絡(luò)的時候:短時間內(nèi)頻繁的請求,后面的請求使用緩存中的資源.
2.無網(wǎng)絡(luò)的時候:獲取之前緩存的數(shù)據(jù)進行暫時的頁面顯示,當網(wǎng)絡(luò)更新時對當前activity的數(shù)據(jù)進行刷新,刷新界面,避免界面空白的場景.
編寫OKHTTP網(wǎng)絡(luò)攔截器
class CacheNetworkInterceptor implements Interceptor {
public Response intercept(Interceptor.Chain chain) throws IOException {
//無緩存,進行緩存
return chain.proceed(chain.request()).newBuilder()
.removeHeader("Pragma")
//對請求進行最大60秒的緩存
.addHeader("Cache-Control", "max-age=60")
.build();
}
}
static class CacheInterceptor implements Interceptor {
public Response intercept(Interceptor.Chain chain) throws IOException {
Response resp;
Request req;
if (ok) {
//有網(wǎng)絡(luò),檢查10秒內(nèi)的緩存
req = chain.request()
.newBuilder()
.cacheControl(new CacheControl
.Builder()
.maxAge(10, TimeUnit.SECONDS)
.build())
.build();
} else {
//無網(wǎng)絡(luò),檢查30天內(nèi)的緩存,即使是過期的緩存
req = chain.request().newBuilder()
.cacheControl(new CacheControl.Builder()
.onlyIfCached()
.maxStale(30, TimeUnit.SECONDS)
.build())
.build();
}
resp = chain.proceed(req);
return resp.newBuilder().build();
}
}
配置OKHTTP中的Cache
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(httpCacheDirectory, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
//加入攔截器,注意Network與非Network的區(qū)別
.addInterceptor(new CacheInterceptor())
.addNetworkInterceptor(new CacheNetworkInterceptor())
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.build();
//最后通過使用該HTTP Client進行網(wǎng)絡(luò)請求, 就實現(xiàn)上述利用緩存優(yōu)化應(yīng)用的需求
在retrofit中使用只要將retrofit的okhttpclient換成這個帶緩存的okhttpclient即可
private val okhttpClient = OkHttpClient.Builder()
.connectTimeout(timeout, TimeUnit.MILLISECONDS)
.readTimeout(timeout, TimeUnit.MILLISECONDS)
.writeTimeout(timeout, TimeUnit.MILLISECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
.addInterceptor(CacheInterceptor())
.addNetworkInterceptor(CacheNetworkInterceptor())
.cache(Cache(File(App.app.externalCacheDir, "ok-cache"), 1024 * 1024 * 30L))
.build()
var retrofit2 = Retrofit.Builder().baseUrl(baseURL)
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.client(okhttpClient)
.build()
解釋一下上面的代碼,
CacheInterceptor主要的作用是判斷當前網(wǎng)絡(luò)是否有效,如果有效,則創(chuàng)建一個請求,
該請求能獲取一個10秒內(nèi)未過期的緩存,否則強制獲取一個緩存(過期了30天也允許).
而CacheNetworkInterceptor 主要是在緩存沒命中的情況下,請求網(wǎng)絡(luò)后,修改返回頭,加上Cache-Control,告知OKHTTP對該請求進行一個60秒的緩存.
因此,當頻繁請求的時候,OKHTTP使用10秒之內(nèi)的緩存而不重復(fù)請求網(wǎng)絡(luò).
當沒網(wǎng)絡(luò)的時候,請求會獲取30天內(nèi)的緩存,避免界面白屏.
OKHTTP關(guān)于Cache的源碼分析
分析源碼之前先看下Cache的策略
Response getResponseWithInterceptorChain() throws IOException {
// Okhttp獲取Response的入口
// 采用責(zé)任鏈模式,一層層按順序轉(zhuǎn)交Request并處理Response
List<Interceptor> interceptors = new ArrayList<>();
// 用戶定義的攔截器
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
//CacheInterceptor主要用于做緩存控制
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
//用戶定義的Network攔截器
interceptors.addAll(client.networkInterceptors());
}
// 發(fā)起實際請求的攔截器
interceptors.add(new CallServerInterceptor(forWebSocket));
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
originalRequest, this, eventListener, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);
}
這里我們主要看CacheInterceptor的實現(xiàn)
CacheInterceptor代碼比較長,我們分段來解釋
@Override public Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
// 實際上是類似map,將返回內(nèi)容的URL的MD5的值當key,返回內(nèi)容當response
// 然后從cache文件里面查詢是否存在該緩存
long now = System.currentTimeMillis();
//根據(jù)當前的時間,以及緩存策略,來獲取response
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
// 根據(jù)策略得到cacheReposne 與 NetworkRequest
// 之后的代碼就是根據(jù)這兩個東西設(shè)置返回頭
// 不進行網(wǎng)絡(luò)請求,且緩存以及過期了,返回504錯誤
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// 不進行網(wǎng)絡(luò)請求,此時緩存命中,直接返回緩存,后面的攔截器也不會調(diào)用了
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
// 否則需要請求網(wǎng)絡(luò),繼續(xù)調(diào)用責(zé)任鏈后面的攔截器,請求網(wǎng)絡(luò)并獲取response
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// 請求異常,關(guān)閉緩存避免泄漏
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// 請求了網(wǎng)絡(luò)的同時,緩存其實也找到的情況
// (比如 需要向服務(wù)器確認緩存是否可用的情況)
if (cacheResponse != null) {
// 返回了304, 我們都知道304的返回時不帶body的,此時必須向獲取cache的body
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//省略---------
}
// 緩存策略CacheStrategy主要的策略寫在該方法下
private CacheStrategy getCandidate() {
// 沒有緩存!
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// 當請求的協(xié)議是https的時候,如果cache沒有hansake就丟棄緩存
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
/// -- 省略一些代碼
// 根據(jù)緩存的緩存時間,緩存可接受最大過期時間等等HTTP協(xié)議上的規(guī)范
// 來判斷緩存是否可用,
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
}
// 請求條件, 當etag,lastModified,servedDate這三種屬性存在時
//需要向服務(wù)器確認緩存的有效性
String conditionName;
String conditionValue;
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
return new CacheStrategy(request, null); // 不存在的時候,按流程進行請求
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
// 構(gòu)造一個請求詢問服務(wù)器資源是否過期
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
借用一張圖來說明http的整個工作流程
流程也很清晰明了了,簡單的說及時通過Request創(chuàng)建RealCall對象,
經(jīng)過層層interceptor之后最終產(chǎn)生一個response.
不過值得注意的是,當CacheInterceptor命中緩存之后, 后面的攔截器將不再執(zhí)行.
這也是addInterceptor 與 addNetworkInterceptor之間的區(qū)別
最后附上當網(wǎng)絡(luò)可用的時候,自動重新請求的一個基于MVP模式的實現(xiàn)方案
NetStatusMonitor是一個單例,用于監(jiān)聽整個應(yīng)用程序的網(wǎng)絡(luò)狀態(tài)
ActivityManager也是一個單例,用來管理應(yīng)用程序的活動棧,原理Application注冊關(guān)于活動的生命周期監(jiān)聽.
基于MVP模式,給presenter的抽象基類定義一個refresh的方法
當斷網(wǎng)時間超過XX秒的時候,調(diào)用在棧頂?shù)腶ctivity的presenter進行刷新頁面
如有不足請各位大佬指正
NetStatusMonitor.setNetStatusListener(object: NetStatusMonitor.Listener {
var lostTime = 0L
override fun onLost() {
lostTime = System.currentTimeMillis()
}
override fun onAvailable() {
with(ActivityManager.peek() as BaseView<*>){
//當棧頂活動位于前臺
if(this.lifecycle.currentState == Lifecycle.State.RESUMED){
// 獲取ForegroundActivity進行刷新
// 斷線時間超過30秒重連再刷新一次
if(System.currentTimeMillis() - lostTime > 1000 * 30){
// 通知presenter刷新數(shù)據(jù)
this.presenter.refresh()
}
}
}
}
override fun onNetStateChange(oldState: Int, newState: Int) {
if(newState == NetStatusMonitor.MOBILE){
showToast("正在使用移動網(wǎng)絡(luò)")
}
}
})
object NetStatusMonitor {
interface Listener{
fun onLost()
fun onAvailable()
fun onNetStateChange(oldState: Int, newState: Int)
}
val WIFI = 1;
val MOBILE = 2;
val WIFI_MOBILE = 3;
val UNKNOW = 0
var available = false
var netState: Int by Delegates.observable(UNKNOW) { property, oldValue, newValue ->
listener?.onNetStateChange(oldValue, newValue)
}
private var listener : Listener? = null
fun setNetStatusListener(listener: Listener){
this.listener = listener
}
init {
val cm = Utils.app.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
fun setType() {
val activeNetwork = cm.activeNetworkInfo
val isMobile = activeNetwork.type == ConnectivityManager.TYPE_MOBILE
val isWifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI).isAvailable
if (isWifi && isMobile)
netState = WIFI_MOBILE
else if (isWifi && !isMobile)
netState = WIFI
else if (isMobile && !isWifi)
netState = MOBILE
else
netState = UNKNOW
}
cm.requestNetwork(NetworkRequest.Builder().build(), object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network?) {
available = true
setType()
listener?.onAvailable()
}
override fun onLost(network: Network?) {
available = false
listener?.onLost()
}
})
}
}