OKHttp3(支持Retrofit)的網(wǎng)絡數(shù)據(jù)緩存Interceptor攔截器的實現(xiàn)
前言:前段時間在開發(fā)APP的時候,經(jīng)常出現(xiàn)由于用戶設備環(huán)境的原因,拿不到從網(wǎng)絡端獲取的數(shù)據(jù),所以在APP端展現(xiàn)的結果總是一個空白的框,這種情況對于用戶體驗來講是極其糟糕的,所以,苦思冥想決定對OKHTTP下手(因為我在項目中使用的網(wǎng)絡請求框架就是OKHTTP),則 寫了這么一個網(wǎng)絡數(shù)據(jù)緩存攔截器。
OK,那么我們決定開始寫了,我先說一下思路:
思路篇
既然要寫的是網(wǎng)絡數(shù)據(jù)緩存攔截器,主要是利用了OKHTTP強大的攔截器功能,那么我們應該對哪些數(shù)據(jù)進行緩存呢,或者在哪些情況下啟用數(shù)據(jù)進行緩存機制呢?
第一 :支持POST請求,因為官方已經(jīng)提供了一個緩存攔截器,但是有一個缺點,就是只能對GET請求的數(shù)據(jù)進行緩存,對POST則不支持。
第二 :網(wǎng)絡正常的時候,則是去網(wǎng)絡端取數(shù)據(jù),如果網(wǎng)絡異常,比如TimeOutException UnKnowHostException 諸如此類的問題,那么我們就需要去緩存取出數(shù)據(jù)返回。
第三 :如果從緩存中取出的數(shù)據(jù)是空的,那么我們還是需要讓這次請求走剩下的正常的流程。
第四 :調用者必須對緩存機制完全掌控,可以根據(jù)自己的業(yè)務需求選擇性的對數(shù)據(jù)決定是否進行緩存。
第五 :使用必須簡單,這是最最最最重要的一點。
好,我們上面羅列了五點是我們的大概思路,現(xiàn)在來說一下代碼部分:
代碼篇
緩存框架 :我這里使用的緩存框架是DiskLruCache https://github.com/JakeWharton/DiskLruCache 這個緩存框架可以存儲到本地,也經(jīng)過谷歌認可,這也是選擇這個框架的主要原因。我這里也對緩存框架進行封裝了一個CacheManager類:
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.xiaolei.OkhttpCacheInterceptor.Log.Log;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
* Created by xiaolei on 2017/5/17.
*/
public class CacheManager
{
public static final String TAG = "CacheManager";
//max cache size 10mb
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 10;
private static final int DISK_CACHE_INDEX = 0;
private static final String CACHE_DIR = "responses";
private DiskLruCache mDiskLruCache;
private volatile static CacheManager mCacheManager;
public static CacheManager getInstance(Context context)
{
if (mCacheManager == null)
{
synchronized (CacheManager.class)
{
if (mCacheManager == null)
{
mCacheManager = new CacheManager(context);
}
}
}
return mCacheManager;
}
private CacheManager(Context context)
{
File diskCacheDir = getDiskCacheDir(context, CACHE_DIR);
if (!diskCacheDir.exists())
{
boolean b = diskCacheDir.mkdirs();
Log.d(TAG, "!diskCacheDir.exists() --- diskCacheDir.mkdirs()=" + b);
}
if (diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE)
{
try
{
mDiskLruCache = DiskLruCache.open(diskCacheDir,
getAppVersion(context), 1/*一個key對應多少個文件*/, DISK_CACHE_SIZE);
Log.d(TAG, "mDiskLruCache created");
} catch (IOException e)
{
e.printStackTrace();
}
}
}
/**
* 同步設置緩存
*/
public void putCache(String key, String value)
{
if (mDiskLruCache == null) return;
OutputStream os = null;
try
{
DiskLruCache.Editor editor = mDiskLruCache.edit(encryptMD5(key));
os = editor.newOutputStream(DISK_CACHE_INDEX);
os.write(value.getBytes());
os.flush();
editor.commit();
mDiskLruCache.flush();
} catch (IOException e)
{
e.printStackTrace();
} finally
{
if (os != null)
{
try
{
os.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
}
/**
* 異步設置緩存
*/
public void setCache(final String key, final String value)
{
new Thread()
{
@Override
public void run()
{
putCache(key, value);
}
}.start();
}
/**
* 同步獲取緩存
*/
public String getCache(String key)
{
if (mDiskLruCache == null)
{
return null;
}
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try
{
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(encryptMD5(key));
if (snapshot != null)
{
fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
bos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) != -1)
{
bos.write(buf, 0, len);
}
byte[] data = bos.toByteArray();
return new String(data);
}
} catch (IOException e)
{
e.printStackTrace();
} finally
{
if (fis != null)
{
try
{
fis.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
if (bos != null)
{
try
{
bos.close();
} catch (IOException e)
{
e.printStackTrace();
}
}
}
return null;
}
/**
* 異步獲取緩存
*/
public void getCache(final String key, final CacheCallback callback)
{
new Thread()
{
@Override
public void run()
{
String cache = getCache(key);
callback.onGetCache(cache);
}
}.start();
}
/**
* 移除緩存
*/
public boolean removeCache(String key)
{
if (mDiskLruCache != null)
{
try
{
return mDiskLruCache.remove(encryptMD5(key));
} catch (IOException e)
{
e.printStackTrace();
}
}
return false;
}
/**
* 獲取緩存目錄
*/
private File getDiskCacheDir(Context context, String uniqueName)
{
String cachePath = context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
/**
* 對字符串進行MD5編碼
*/
public static String encryptMD5(String string)
{
try
{
byte[] hash = MessageDigest.getInstance("MD5").digest(
string.getBytes("UTF-8"));
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash)
{
if ((b & 0xFF) < 0x10)
{
hex.append("0");
}
hex.append(Integer.toHexString(b & 0xFF));
}
return hex.toString();
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e)
{
e.printStackTrace();
}
return string;
}
/**
* 獲取APP版本號
*/
private int getAppVersion(Context context)
{
PackageManager pm = context.getPackageManager();
try
{
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
return pi == null ? 0 : pi.versionCode;
} catch (PackageManager.NameNotFoundException e)
{
e.printStackTrace();
}
return 0;
}
}
緩存CacheInterceptor攔截器:利用OkHttp的Interceptor攔截器機制,智能判斷緩存場景,以及網(wǎng)絡情況,對不同的場景進行處理。
import android.content.Context;
import com.xiaolei.OkhttpCacheInterceptor.Catch.CacheManager;
import com.xiaolei.OkhttpCacheInterceptor.Log.Log;
import java.io.IOException;
import okhttp3.FormBody;
import okhttp3.Interceptor;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/**
* 字符串的緩存類
* Created by xiaolei on 2017/12/9.
*/
public class CacheInterceptor implements Interceptor
{
private Context context;
public void setContext(Context context)
{
this.context = context;
}
public CacheInterceptor(Context context)
{
this.context = context;
}
@Override
public Response intercept(Chain chain) throws IOException
{
Request request = chain.request();
String cacheHead = request.header("cache");
String cache_control = request.header("Cache-Control");
if ("true".equals(cacheHead) || // 意思是要緩存
(cache_control != null && !cache_control.isEmpty())) // 這里還支持WEB端協(xié)議的緩存頭
{
long oldnow = System.currentTimeMillis();
String url = request.url().url().toString();
String responStr = null;
String reqBodyStr = getPostParams(request);
try
{
Response response = chain.proceed(request);
if (response.isSuccessful()) // 只有在網(wǎng)絡請求返回成功之后,才進行緩存處理,否則,404存進緩存,豈不笑話
{
ResponseBody responseBody = response.body();
if (responseBody != null)
{
responStr = responseBody.string();
if (responStr == null)
{
responStr = "";
}
CacheManager.getInstance(context).setCache(CacheManager.encryptMD5(url + reqBodyStr), responStr);//存緩存,以鏈接+參數(shù)進行MD5編碼為KEY存
Log.i("HttpRetrofit", "--> Push Cache:" + url + " :Success");
}
return getOnlineResponse(response, responStr);
} else
{
return chain.proceed(request);
}
} catch (Exception e)
{
Response response = getCacheResponse(request, oldnow); // 發(fā)生異常了,我這里就開始去緩存,但是有可能沒有緩存,那么久需要丟給下一輪處理了
if (response == null)
{
return chain.proceed(request);//丟給下一輪處理
} else
{
return response;
}
}
} else
{
return chain.proceed(request);
}
}
private Response getCacheResponse(Request request, long oldNow)
{
Log.i("HttpRetrofit", "--> Try to Get Cache --------");
String url = request.url().url().toString();
String params = getPostParams(request);
String cacheStr = CacheManager.getInstance(context).getCache(CacheManager.encryptMD5(url + params));//取緩存,以鏈接+參數(shù)進行MD5編碼為KEY取
if (cacheStr == null)
{
Log.i("HttpRetrofit", "<-- Get Cache Failure ---------");
return null;
}
Response response = new Response.Builder()
.code(200)
.body(ResponseBody.create(null, cacheStr))
.request(request)
.message("OK")
.protocol(Protocol.HTTP_1_0)
.build();
long useTime = System.currentTimeMillis() - oldNow;
Log.i("HttpRetrofit", "<-- Get Cache: " + response.code() + " " + response.message() + " " + url + " (" + useTime + "ms)");
Log.i("HttpRetrofit", cacheStr + "");
return response;
}
private Response getOnlineResponse(Response response, String body)
{
ResponseBody responseBody = response.body();
return new Response.Builder()
.code(response.code())
.body(ResponseBody.create(responseBody == null ? null : responseBody.contentType(), body))
.request(response.request())
.message(response.message())
.protocol(response.protocol())
.build();
}
/**
* 獲取在Post方式下。向服務器發(fā)送的參數(shù)
*
* @param request
* @return
*/
private String getPostParams(Request request)
{
String reqBodyStr = "";
String method = request.method();
if ("POST".equals(method)) // 如果是Post,則盡可能解析每個參數(shù)
{
StringBuilder sb = new StringBuilder();
if (request.body() instanceof FormBody)
{
FormBody body = (FormBody) request.body();
if (body != null)
{
for (int i = 0; i < body.size(); i++)
{
sb.append(body.encodedName(i)).append("=").append(body.encodedValue(i)).append(",");
}
sb.delete(sb.length() - 1, sb.length());
}
reqBodyStr = sb.toString();
sb.delete(0, sb.length());
}
}
return reqBodyStr;
}
}
以上是主體思路,以及主要實現(xiàn)代碼,現(xiàn)在來說一下使用方式
使用方式:
gradle使用:
compile 'com.xiaolei:OkhttpCacheInterceptor:1.0.0'
由于是剛剛提交到Jcenter,可能會出現(xiàn)拉不下來的情況(暫時還未過審核),著急的讀者可以再在你的Project:build.gradle里的repositories里新增我maven的鏈接:
allprojects {
repositories {
maven{url 'https://dl.bintray.com/kavipyouxiang/maven'}
}
}
我們新建一個項目,項目截圖是這樣的:

項目截圖
demo很簡單,一個主頁面,一個Bean,一個Retrofit,一個網(wǎng)絡請求接口
注意,因為是網(wǎng)絡,緩存,有關,所以,毫無疑問我們要在manifest里面添加網(wǎng)絡請求權限,文件讀寫權限:
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
使用的時候,你只需要為你的OKHttpClient添加一個Interceptor:
client = new OkHttpClient.Builder()
.addInterceptor(new CacheInterceptor(context))//添加緩存攔截器,添加緩存的支持
.retryOnConnectionFailure(true)//失敗重連
.connectTimeout(30, TimeUnit.SECONDS)//網(wǎng)絡請求超時時間單位為秒
.build();
如果你想哪個接口的數(shù)據(jù)緩存,那么久為你的網(wǎng)絡接口,添加一個請求頭CacheHeaders.java這個類里包含了所有的情況,一般情況下只需要CacheHeaders.NORMAL就可以了
public interface Net
{
@Headers(CacheHeaders.NORMAL) // 這里是關鍵
@FormUrlEncoded
@POST("geocoding")
public Call<DataBean> getIndex(@Field("a") String a);
}
業(yè)務代碼:
Net net = retrofitBase.getRetrofit().create(Net.class);
Call<DataBean> call = net.getIndex("蘇州市");
call.enqueue(new Callback<DataBean>()
{
@Override
public void onResponse(Call<DataBean> call, Response<DataBean> response)
{
DataBean data = response.body();
Date date = new Date();
textview.setText(date.getMinutes() + " " + date.getSeconds() + ":\n" + data + "");
}
@Override
public void onFailure(Call<DataBean> call, Throwable t)
{
textview.setText("請求失敗!");
}
});
我們這里對網(wǎng)絡請求,成功了,則在界面上輸出文字,加上當前時間,網(wǎng)絡失敗,則輸出一個請求失敗。
大概代碼就是這樣子的,詳細代碼,文章末尾將貼出demo地址
看效果:演示圖

這里演示了,從網(wǎng)絡正常,到網(wǎng)絡不正常,再恢復到正常的情況。
結尾
以上篇章就是整個從思路,到代碼,再到效果圖的流程,這里貼一下DEMO的地址,喜歡的可以點個Start
Demo地址:https://github.com/xiaolei123/OkhttpCacheInterceptor
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Retrofit之OKHttpCall源碼分析
- Okhttp、Retrofit進度獲取的方法(一行代碼搞定)
- Android 封裝Okhttp+Retrofit+RxJava,外加攔截器實例
- okhttp3.4.1+retrofit2.1.0實現(xiàn)離線緩存的示例
- RxJava+Retrofit+OkHttp實現(xiàn)多文件下載之斷點續(xù)傳
- RxJava+Retrofit+OkHttp實現(xiàn)文件上傳
- 深入淺出RxJava+Retrofit+OkHttp網(wǎng)絡請求
- 淺談RxJava+Retrofit+OkHttp 封裝使用
- Android中Retrofit+OkHttp進行HTTP網(wǎng)絡編程的使用指南
- Retrofit和OkHttp如何實現(xiàn)Android網(wǎng)絡緩存
相關文章
使用Spring-Retry解決Spring Boot應用程序中的重試問題
重試的使用場景比較多,比如調用遠程服務時,由于網(wǎng)絡或者服務端響應慢導致調用超時,此時可以多重試幾次。用定時任務也可以實現(xiàn)重試的效果,但比較麻煩,用Spring Retry的話一個注解搞定所有,感興趣的可以了解一下2023-04-04

