1. 程式人生 > >實現PHP伺服器+Android客戶端(Retrofit+RxJava)第三天Retrofit的配置以及快取的實現

實現PHP伺服器+Android客戶端(Retrofit+RxJava)第三天Retrofit的配置以及快取的實現

上一篇講了介面,這篇文章就要講客戶端網路請求部分的內容了,主要用到的就是Retrofit+RxJava,其實準確來說是Retrofit+RxJava+OkHttp,
最新的Retrofit是2.0.2版本,原始碼地址:retrofit
學習retrofit:用 Retrofit 2 簡化 HTTP 請求
大家不要覺得使用那麼多的框架好像不太好,對於個人開發者來說,一個人懂的東西還是有限的,要自己做一個完整的專案還是需要藉助一些框架,而且我覺得還是先把想要做的東西做出來再說,先會用這些框架再去學習看原始碼也不遲,在使用的過程中就已經能收穫一些,而且也幫助後續理解。
接下來的內容就來說說Retrofit+RxJava+OkHttp的簡單配置
參考了以下文章:

Retrofit2.0使用總結
Retrofit 2.0 + OkHttp 3.0 配置

依賴

compile 'com.squareup.retrofit2:retrofit:2.0.2'
compile 'com.squareup.retrofit2:converter-gson:2.0.2'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.2'
compile 'com.squareup.okhttp3:logging-interceptor:3.2.0'
compile 'com.squareup.okhttp3:okhttp:3.0.1'

OkHttp的配置

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(interceptor)
        .retryOnConnectionFailure(true)
        .connectTimeout(15, TimeUnit.SECONDS
) .addNetworkInterceptor(mTokenInterceptor) .build();
  • HttpLoggingInterceptor 是一個攔截器,用於輸出請求和響應的log,可以配置 level 為 BASIC / HEADERS / BODY
  • retryOnConnectionFailure就是出現錯誤時是否重試
  • connectTimeout超時時間
  • addNetworkInterceptor讓所有網路請求都附上你的攔截器,我這裡設定了一個 token 攔截器,就是在所有網路請求的 header 加上 token 引數。
    這裡可能會對攔截器有些疑問,攔截器一般的作用也就是重寫請求報文的頭部資訊,內容或者重寫響應報文的頭部資訊(非正規做法,可以由伺服器完成,比如做快取)。攔截器分為以下兩種
  • 應用攔截器 addInterceptor()
  • 網路攔截器 addNetworkInterceptor()
    對於這兩個的區分我也還沒有到位,大致做個區分:應用攔截器即使請求的是快取也會呼叫,而網路攔截器不會。應用攔截器在重定向的時候只調用一次,網路攔截器會呼叫兩次。一般使用的話要列印log就在應用攔截器就行,而要新增什麼頭部啊就用網路攔截器。
    可自行參考:Okhttp-wiki 之 Interceptors 攔截器

Retrofit配置

Retrofit retrofit = new Retrofit.Builder()
        .baseUrl(BASE_URL)
        .client(client)
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .addConverterFactory(GsonConverterFactory.create(gson))
        .build();
        apiService = retrofit.create(ApiService.class);

快取的實現

首先要實現快取就要先確定快取的目錄,這個目錄需要在app解除安裝之後也被刪除,所以一般就兩個目錄

  • data/$packageName/cache: Context.getCacheDir()
  • /storage/sdcard0/Andorid/data/$packageName/cache: Context.getExternalCacheDir()

其餘程式碼如下

private static final int HTTP_RESPONSE_DISK_CACHE_MAX_SIZE = 10 * 1024 * 1024;

  private Cache cache() {
         //設定快取路徑
         final File baseDir = AppUtil.getAvailableCacheDir(sContext);
         final File cacheDir = new File(baseDir, "HttpResponseCache");
         //設定快取 10M
         return new Cache(cacheDir, HTTP_RESPONSE_DISK_CACHE_MAX_SIZE);
     }

不要忘記:

OkHttpClient client = new OkHttpClient.Builder()
        .addInterceptor(interceptor)
        .retryOnConnectionFailure(true)
        .connectTimeout(15, TimeUnit.SECONDS)
        .addNetworkInterceptor(mTokenInterceptor)
        .cache(chche())
        .build();

上面的工作只是第一步,接下來才是真正快取的實現,快取實現我看到的有三種方式

第一種

有網和沒網都先讀快取,統一快取策略。
根據響應報文的Cache-Control欄位或者直接修改響應報文的Cache-Control欄位做快取(其實控制快取的不止這一個欄位,我這裡就只講這一個,方便)。
先來說說http協議吧,首先我們客戶端傳送一個http請求報文(不包含Cache-Control欄位),隨後伺服器返回http響應報文,然後我們可以根據響應報文中的Cache-Control欄位來決定是否進行快取,快取在多長時間內有效等,假設返回的Cache-Control欄位內容的如下:

Cache-Control: max-age=60, public

意思是60秒內有效,60s內再次請求此地址就直接拿快取,60s之後就再去請求伺服器,大致流程如下圖:
這裡寫圖片描述
這一種方式主要是靠伺服器來做快取策略,不過,加入我們的伺服器沒有返回快取相關的欄位如Cache-Control: max-age=60, public,我們也可以用攔截器自己在響應報文中加上,用的攔截器是網路攔截器(忘了網路攔截器的可以回頭看看),攔截器程式碼如下:

Interceptor cacheInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Response response = chain.proceed(request);

                return response.newBuilder()
                        .header("Cache-Control", "max-age=60")
                        .removeHeader("Pragma")
                        .build();
            }
        };

連著自己的伺服器測試之後,第一次請求在相應目錄下建立了快取,在我上面實現的攔截器中列印log輸出的響應首部中也添加了Cache-Control欄位,隨後再次請求相同的路徑就沒有再輸出log資訊(和我們上面說的網路攔截器不會在請求讀取快取的時候呼叫相吻合)。
這一種方式只要知道最基本的http協議的知識就能很快理解。

第二種

第二種方式如下(但其實是有問題的,接下來討論):

// 設定 單個請求的 快取時間
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

在請求的首部加上Cache-Control欄位,最後再去看快取目錄下面也生成了快取。就我那時候知道的http知識來理解我很困惑,不是Cache-Control欄位只作用在響應報文中嗎?為什麼在請求報文的首部新增快取控制也能實現響應報文的快取。後來再去看了看http權威指南,請求報文中也能有Cache-Control欄位是看到了,但是一些含義還是看的迷迷糊糊,於是決定還是自己來做實驗好了。
以下測試針對請求報文的首部Cache-Control欄位:

Cache-Control:no-cache,如何測試呢,需要藉助第一種方式在本地生成一次快取,隨後傳送帶有Cache-Control:no-cache的請求,你會發現網路攔截器在輸出響應體,再看快取中的Date那一行資訊發現一直在變,說明向,伺服器請求了資料,重新寫了快取。由此可得請求報文中 Cache-Control:no-cache的意思是會被快取的,只不過每次在向客戶端(瀏覽器)提供響應資料時,快取都要向伺服器評估快取響應的有效性。
Cache-Control:max-stale是隻在請求報文中有效的,首先網路攔截器新增的響應報文的首部是Cache-Control: max-age=60, public,也就是快取在60s內有效,隨後設定請求的Cache-Control:max-stale=120.照著htpp協議的意思應該是120s之內都從快取取資料,但是實際卻輸出了響應報文的body,也就是說進行了網路請求,再看請求報文的首部資訊,其中帶著If-Modified-since資訊,說明是快取過期了,但是我明明很快就再次請求了,再把max-stale=120時間改大一點,改成1200,再測試,沒有列印log資訊,說明符合我理解的http的語義,響應報文設定的快取有效時間是60s,請求報文max-stale設定的是1200s有效,過了60s依舊從快取中取。但是這個時間有問題!!!暫時先不管這個,把http的語義搞清楚再說。
上面的情況是在響應報文有快取策略的情況下,接下來看看沒有在響應報文中說明快取策略的情況,先把網路攔截器給響應報文新增Cache-Control欄位的程式碼去掉,測試結果生成了快取(只要完成了第一步設定了cache(chche())就會預設快取),在過期時間內也從快取中讀取。
Cache-Control:max-age這個欄位意思我開始不太明白,後來結合響應首部的Cache-Control:max-age聯合使用的時候突然明白了,假設只在請求首部新增Cache-Control:max-age,你會發現建立了快取,但是立馬就去請求了伺服器(我猜這個時候失效時間為0,因為沒有設定響應首部的Cache-Control:max-age),單獨設定響應首部的Cache-Control:max-age那是有用的,就是我們的第一種方式。如果請求和響應的首部都設定了,就取小的那一個。
是不會返回快取時間超過這個時間的文件,和響應首部的Cache-Control:max-age聯合使用的時候,響應首部的Cache-Control:max-age,失效時間取小的那一個
結合上述,簡單總結起來可以說Cache-Control:max-age規定的是失效時間的下限制(如果請求首部沒有寫預設是很大,如果響應首部沒有寫那就是0,最終的max-age取兩者的小值),Cache-Control:max-stale規定的是上限(只在請求首部有效)。
看到這裡大家應該也知道了,像上面

// 設定 單個請求的 快取時間
@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

這樣是會建立快取,但是請求不會走快取,需要配合攔截器設定響應報文中的Cache-Control。
攔截器程式碼如下:

Interceptor cacheInterceptor = new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                Response response = chain.proceed(request);
                String cacheControl = request.cacheControl().toString();
                if (TextUtils.isEmpty(cacheControl)) {
                    return response;
                }
                return response.newBuilder()
                        .header("Cache-Control", cacheControl )
                        .removeHeader("Pragma")
                        .build();
            }
        };

第三種

離線讀取本地快取,線上獲取最新資料

private Interceptor cacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();

                if (!AppUtil.isNetworkReachable(sContext)) {
                    request = request.newBuilder()
                            //強制使用快取
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                Response response = chain.proceed(request);

                if (AppUtil.isNetworkReachable(sContext)) {
                    //有網的時候讀介面上的@Headers裡的配置,你可以在這裡進行統一的設定
                    String cacheControl = request.cacheControl().toString();
                    Logger.i("has network ,cacheControl=" + cacheControl);
                    return response.newBuilder()
                            .header("Cache-Control", cacheControl)
                            .removeHeader("Pragma")
                            .build();
                } else {
                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                    Logger.i("network error ,maxStale="+maxStale);
                    return response.newBuilder()
                            .header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)
                            .removeHeader("Pragma")
                            .build();
                }

            }
        };
    }

大家看到上面的程式碼可能會有疑問,有疑問的地方如下:

 else {
                    int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
                    Logger.i("network error ,maxStale="+maxStale);
                    return response.newBuilder()
                            .header("Cache-Control", "public, only-if-cached, max-stale="+maxStale)
                            .removeHeader("Pragma")
                            .build();
                }

這裡的程式碼其實都是沒有用的,都可以去掉,only-if-cached和max-stale都只在請求報文中有效,真正做到,沒有網路的時候很長時間快取有效是在這幾句程式碼:

if (!AppUtil.isNetworkReachable(sContext)) {
                    request = request.newBuilder()
                            //強制使用快取
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

所以上面的程式碼可以改為如下:

private Interceptor cacheInterceptor() {
        return new Interceptor() {
            @Override
            public Response intercept(Chain chain) throws IOException {
                Request request = chain.request();

                if (!AppUtil.isNetworkReachable(sContext)) {
                    request = request.newBuilder()
                            //強制使用快取
                            .cacheControl(CacheControl.FORCE_CACHE)
                            .build();
                }

                Response response = chain.proceed(request);

                if (AppUtil.isNetworkReachable(sContext)) {
                    //有網的時候讀介面上的@Headers裡的配置,你可以在這裡進行統一的設定
                    String cacheControl = request.cacheControl().toString();
                    Logger.i("has network ,cacheControl=" + cacheControl);
                    return response.newBuilder()
                            .header("Cache-Control", cacheControl)
                            .removeHeader("Pragma")
                            .build();
                } 
                return response;
            }
        };
    }