OKHttp使用Interceptor的緩存問題
前言
網上關于OKHttp的文章特別多,用法大家都知道,這里就只說說關于使用OKHttp中的攔截器來做緩存的問題。之前從來沒有使用過攔截器做緩存,所以一開始也是從網上一頓搜,大部分文章都是差不多的,然后就把網上的代碼拷貝過來了,不出所料,果然和文中的效果不一樣,下面就說說這兩天踩過的坑。
概念
對于攔截器的詳細論述我也在網上搜索看了好多,這里給出我的理解: 攔截器 :簡單來說就是在使用OKHttp訪問網絡的時候,可以通過自定義攔截器將發送的Request攔截下來,然后就可以做一些操作,比如添加一些請求頭參數等。
那么我們怎么通過配置請求頭或者響應頭來達到緩存呢?
答案是OKHttp已經幫我們完成了,我們只需要按照它的要求去配置好請求頭或者響應頭就可以了,那么怎么配置呢?這里說一下,不管是web中瀏覽器的緩存還是Android中使用OKHttp來做緩存,都是通過響應頭(即 Response 的 header )來完成的。拿Android使用OKHttp來說,客戶端是拿到服務器的響應以后,OKHttp是通過獲取響應中的參數來配置對應的緩存。那么請求頭又有什么用呢?這就要分兩種情況了。
一:可以和服務器協商
這種情況的時候,我們可以要求服務端來按照我們的要求來配置對應緩存的響應頭參數,然后我們只負責解析就可以達到緩存的目的。這種情況,我們不需要為了緩存再寫攔截器來做配置。
二:不能和服務器協商
很多時候,服務器根本不是我們自己維護的,所以這時就需要我們自己來完成緩存的響應頭設置了,那我們怎么拿到服務器返回的響應然后添加自己響應頭呢?就是通過攔截器,使用攔截器不僅可以攔截發出的 Request 請求,同時還可以配置收到的 Response 響應,配置好之后返回就可以了
使用
先說一個依賴的問題,一般現在我們的網絡庫都是Rxjava+ Retrofit + OKHttp,由于Retrofit 中內置了OKHttp,所以我們在依賴的時候,只依賴Retrofit就可以了,不要再去依賴OKHttp,因為我在網上看到有人因為兩個的版本問題出現了各種奇怪的問題,這樣浪費精力就得不償失了。下面是我的依賴
//Rxjava,Rxandroid
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.0.1'
//RxJava2 Adapter
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
//retrofit
compile 'com.squareup.retrofit2:retrofit:2.1.0'
//Gson converter
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
接下來開始上代碼,然后在說說我遇到的坑和注意事項。
既然要做緩存,就先說緩存策略:
分為有網絡和無網絡的情況,在有網絡的時候,先讀取緩存中的內容,緩存時間到之后訪問網絡拿數據,在沒有網絡的時候讀取緩存數據。
下面貼出判斷網絡代碼
/**
* 判斷是否有網絡
*
* @return 返回值
*/
public static boolean isNetworkConnected() {
Context context = RxApplication.getContext();
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}</code></pre>
別忘了加權限
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
定義一個攔截器
public class MyCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
return null;
}
只要實現 Interceptor 接口,實現一個方法就可以。可以看到接口返回的是一個 Response 對象,我們就可以把配置好的響應頭直接返回就可以了,怎么配置呢?看下面。
然后是在有網絡的時候需要在攔截器中添加的設置
@Override
public Response intercept(Chain chain) throws IOException {
//攔截Request對象
Request request = chain.request();
//判斷有無網絡連接
boolean connected = isNetworkConnected();
if (connected) {
//有網絡,緩存時間短,緩存90s
String cacheControl = request.cacheControl().toString();
//這里返回的就是我們獲取到的響應頭,添加緩存配置返回
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control","public, max-age=90")
.build();
}
這里就有個疑惑了:看上面的代碼,在這個攔截器中我們既可以獲得 Request 請求對象,同時返回的竟然是服務器的 Response 響應頭對象,那么攔截器在什么時機攔截的請求呢?又在什么時機攔截的響應呢?這個我沒有看源碼,只能通過效果反推一下,通過在有網的時候打印Log可以發現,當訪問網絡的時候,Log打印有時兩次有時多次,我推測,一個攔截器在客戶端發送到服務端的線路上會攔截一次,在服務端發送回客戶端的線路上又會攔截一次,這樣也就做到了,既可以攔截 Request 請求,也做到了攔截服務器返回的 Response 響應。當然我只是怎么推測,有知道的朋友請留言告訴我一些(感謝)。總之它的作用就是這樣的。
然后是在沒有網絡的時候添加的設置
if (!connected) {
//沒有網絡時設置強制讀取緩存
int maxTime = 3600;
return response.newBuilder()
//這里的設置的是我們的沒有網絡的緩存時間,想設置多少就是多少。
.header("Cache-Control", "public, only-if-cached, max-age=" + maxTime)
.removeHeader("Pragma")
.build();
}
最后貼出攔截器的完整代碼
public class MyCacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
//攔截Request對象
Request request = chain.request();
//判斷有無網絡連接
boolean connected = isNetworkConnected();
if (!connected) {
//如果沒有網絡,從緩存獲取數據
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Log.e("zhanghe", "no network");
}
Response response = chain.proceed(request);
if (connected) {
//有網絡,緩存時間短
Log.e("zhanghe", "有網絡");
String cacheControl = request.cacheControl().toString();
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control","public, max-age=90")
.build();
} else {
//沒有網絡
Log.e("zhanghe", "沒有網絡的緩存設置");
int maxTime = 3600;
return response.newBuilder()
//這里的設置的是我們的沒有網絡的緩存時間,想設置多少就是多少。
.header("Cache-Control", "public, max-age=" + maxTime)
.removeHeader("Pragma")
.build();
}
}
}
然后還要創建一個緩存路徑再將這個攔截器添加到Client中
File file = new File(RxApplication.getContext().getCacheDir(), "rxCache");
//緩存大小10M
int cacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(file, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache) //設置緩存
.addNetworkInterceptor(cacheInterceptor)//添加攔截器
.connectTimeout(5, TimeUnit.SECONDS) //連接超時
.writeTimeout(5, TimeUnit.SECONDS) //寫入超時
.readTimeout(5, TimeUnit.SECONDS) //讀取超時
.build();
然后就可以構造Retrofit對象了
Retrofit mRetrofit = new Retrofit.Builder()
.client(client)
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
之后的操作我就不說了,這不是文章的重點。
坑一
代碼寫完了,網上的例子都是這么寫的,然后我就去運行了,但是,我怎么知道OKHttp什么時候在訪問網絡,什么時候在訪問緩存呢?所以我想到了使用Fiddler來看,然后就有了代碼中那些Log輸出,然后開始調試,解釋結果是每次Fiddler都會顯示從網絡上下載數據了,而且按照常理,從服務器獲取到的響應頭會有添加的 Cache-Control 屬性,但是沒有。
先告訴大家吧,在調試的時候千萬千萬千萬不要用Fiddler,否則你會被坑慘的,最后我自己寫了一個接口,第一次訪問完之后,馬上把接口中的數據換了這樣來測試是不是讀取的緩存,結果證明Fiddler也還是會顯示出一條http請求 (可能是我不會用Fiddler吧,不過還是建議大家自己寫個接口來測比較方便)。
對于響應頭的 Cache-Control 屬性,Fiddler確實是顯示不出來,但是通過代碼獲取到的響應頭確實添加了,獲取響應頭代碼如下:
new Thread(new Runnable() {
@Override
public void run() {
try {
Call<MyInfo> call = RetrofitManager.getInstance()
.createApi(MovieApi.class)
.getInfo();
Response<MyInfo> execute = call.execute();
//獲取響應頭
Headers headers = execute.raw().headers();
//獲取響應嗎
int code = execute.code();
//獲取對應的字段
String s = headers.get("Cache-Control");
Log.e("zhang", s);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
最終結論:Fiddler很坑,調試的時候不要用它。
坑二
我在調試中,在有網的情況下都可以可以的,即第一次從網絡上獲取之后,然后在訪問就是走的緩存,但是在將網絡斷了之后,總是不能讀取緩存數據.Google,百度了好久,網上差不多都是類似上面的代碼,好多還是就直接轉載的,不知道有沒有測試過就發表出來了,好坑(嗚嗚嗚。。。)
然后我就找到lygttpod的GitHub郵箱,郵件問他,發現自己上面代碼少調用一個方法,就是在添加攔截器方法的位置,代碼再貼一遍
File file = new File(RxApplication.getContext().getCacheDir(), "rxCache");
//緩存大小10M
int cacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(file, cacheSize);
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache) //設置緩存
.addInterceptor(cacheInterceptor) //☆☆☆
.addNetworkInterceptor(cacheInterceptor)//添加攔截器
.connectTimeout(5, TimeUnit.SECONDS) //連接超時
.writeTimeout(5, TimeUnit.SECONDS) //寫入超時
.readTimeout(5, TimeUnit.SECONDS) //讀取超時
.build();
這里添加攔截器有兩個方法 addInterceptor 和 addNetworkInterceptor ,分別表示添加作為應用攔截器和網絡攔截器,想看著兩種攔截器的區別點這里,其實我也不太明白兩者的區別,但是在后邊說緩存坑的時候會說到兩者的區別。
添加了這行代碼之后果然好用了,但是為什么呢?如果只添加一個會怎么樣呢?
至于為什么要添加兩個,我沒有去看源碼所以無法解答,如果有朋友知道,請留言告訴我以上,但是我測試了分別添加一個的結果:
一:只添加網絡攔截器
在有網絡的時候,緩存邏輯是正常的,第一次訪問網絡,緩存到本地,之后會先去讀緩存,設置的緩存時間到了以后,會去訪問網絡;但是斷網的時候,OKHttp就不會去讀緩存了,為什么呢?debug之后,發現是這行代碼出現了異常: Response response = chain.proceed(request); 拋出的異常為: java.net.SocketException: sendto failed: ETIMEDOUT (Connection timed out) 但是異常不會導致程序正常邏輯出現錯誤,之后導致斷網狀態不會讀取緩存
二:只添加應用攔截器
在有網絡的時候,OKHttp每次都會去訪問網絡,不會去讀緩存;但是在斷網的時候,代碼正常運行,因為上面代碼配置了緩存邏輯,所以OKHttp會去讀緩存。
看了上面的測試結果,就明白了為什么要將那一個攔截器對象add兩次,因為只有add兩次才能夠滿足我們的緩存邏輯:在有網絡的時候,先去讀緩存,緩存時間到了,再去訪問網絡獲取數據;在沒有網絡的時候,去讀緩存中的數據。
但是add兩次也有一個弊端:就是因為同一個攔截器add了兩次,在有網的情況下,是有緩存就讀緩存的,讀緩存的時候理論上是不應該再走攔截器的,但因為add了兩次,所以每次都會再走一遍攔截器,測試的時候每次讀緩存都會打印這句話 Log.e("zhanghe", "有網絡");
最后
最后我感覺既然是兩種情況加的攔截器,即有網情況和無網情況,所以我就建立了兩個對應的類,經過測試也避免了上面說的弊端。代碼如下:
/**
*
* 在有網絡的情況下,先去讀緩存,設置的緩存時間到了,在去網絡獲取
*/
public class NetInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
boolean connected = NetUtil.isNetworkConnected();
if(connected){
//如果有網絡,緩存90s
Log.e("zhanghe","print");
Response response = chain.proceed(request);
int maxTime = 90;
return response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=" + maxTime)
.build();
}
//如果沒有網絡,不做處理,直接返回
return chain.proceed(request);
}
}
/**
* author: zh on 2017/4/13.
* 在沒有網絡的情況下,取讀緩存數據
*/
public class NoNetInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
boolean connected = NetUtil.isNetworkConnected();
//如果沒有網絡,則啟用 FORCE_CACHE
if (!connected) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build();
Log.e("zhanghe", "無網絡設置_common");
Response response = chain.proceed(request);
return response.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=3600")
.removeHeader("Pragma")
.build();
}
//有網絡的時候,這個攔截器不做處理,直接返回
return chain.proceed(request);
}
}
添加到 OkHttpClient 的時候就可以分別添加
OkHttpClient client = new OkHttpClient.Builder()
.cache(cache)
.addInterceptor(new NoNetInterceptor()) //將無網絡攔截器當做應用攔截器添加
.addNetworkInterceptor(new NetInterceptor()) //將有網絡攔截器當做網絡攔截器添加
//.addInterceptor(cacheInterceptor)
//.addNetworkInterceptor(cacheInterceptor)
.connectTimeout(DUFAULT_TIME_OUT, TimeUnit.SECONDS) //連接超時
.writeTimeout(DUFAULT_TIME_OUT, TimeUnit.SECONDS) //寫入超時
.readTimeout(DUFAULT_TIME_OUT, TimeUnit.SECONDS) //讀取超時
.build();
上面就是我對OKHttp的攔截器做緩存的理解,哪里有不對的地方請留言,或者github上郵箱發給我
來自:http://www.jianshu.com/p/cf59500990c7