Retrofit和OkHttp使用網(wǎng)絡(luò)緩存數(shù)據(jù)

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的策略


Cache.png
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的整個工作流程

image

流程也很清晰明了了,簡單的說及時通過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()
            }
        })

    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末球凰,一起剝皮案震驚了整個濱河市扑浸,隨后出現(xiàn)的幾起案子澡为,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡源葫,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門砖瞧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來息堂,“玉大人,你說我怎么就攤上這事块促∪傺撸” “怎么了?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵褂乍,是天一觀的道長持隧。 經(jīng)常有香客問我,道長逃片,這世上最難降的妖魔是什么屡拨? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮褥实,結(jié)果婚禮上呀狼,老公的妹妹穿的比我還像新娘。我一直安慰自己损离,他們只是感情好哥艇,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著僻澎,像睡著了一般貌踏。 火紅的嫁衣襯著肌膚如雪十饥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天祖乳,我揣著相機與錄音逗堵,去河邊找鬼。 笑死眷昆,一個胖子當著我的面吹牛蜒秤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亚斋,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼作媚,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了帅刊?” 一聲冷哼從身側(cè)響起纸泡,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎厚掷,沒想到半個月后弟灼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡冒黑,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了勤哗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抡爹。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖芒划,靈堂內(nèi)的尸體忽然破棺而出冬竟,到底是詐尸還是另有隱情,我是刑警寧澤民逼,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布泵殴,位于F島的核電站,受9級特大地震影響拼苍,放射性物質(zhì)發(fā)生泄漏笑诅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一疮鲫、第九天 我趴在偏房一處隱蔽的房頂上張望吆你。 院中可真熱鬧,春花似錦俊犯、人聲如沸妇多。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽者祖。三九已至立莉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間七问,已是汗流浹背蜓耻。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留烂瘫,地道東北人媒熊。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像坟比,于是被迫代替她去往敵國和親芦鳍。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

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