OKHttp3(支持Retrofit)的网络数据缓存Interceptor拦截器的实现
前言:前段时间在开发app的时候,经常出现由于用户设备环境的原因,拿不到从网络端获取的数据,所以在app端展现的结果总是一个空白的框,这种情况对于用户体验来讲是极其糟糕的,所以,苦思冥想决定对okhttp下手(因为我在项目中使用的网络请求框架就是okhttp),则 写了这么一个网络数据缓存拦截器。
ok,那么我们决定开始写了,我先说一下思路:
思路篇
既然要写的是网络数据缓存拦截器,主要是利用了okhttp强大的拦截器功能,那么我们应该对哪些数据进行缓存呢,或者在哪些情况下启用数据进行缓存机制呢?
第一 :支持post请求,因为官方已经提供了一个缓存拦截器,但是有一个缺点,就是只能对get请求的数据进行缓存,对post则不支持。
第二 :网络正常的时候,则是去网络端取数据,如果网络异常,比如timeoutexception unknowhostexception 诸如此类的问题,那么我们就需要去缓存取出数据返回。
第三 :如果从缓存中取出的数据是空的,那么我们还是需要让这次请求走剩下的正常的流程。
第四 :调用者必须对缓存机制完全掌控,可以根据自己的业务需求选择性的对数据决定是否进行缓存。
第五 :使用必须简单,这是最最最最重要的一点。
好,我们上面罗列了五点是我们的大概思路,现在来说一下代码部分:
代码篇
缓存框架 :我这里使用的缓存框架是disklrucache https://github.com/jakewharton/disklrucache 这个缓存框架可以存储到本地,也经过谷歌认可,这也是选择这个框架的主要原因。我这里也对缓存框架进行封装了一个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拦截器机制,智能判断缓存场景,以及网络情况,对不同的场景进行处理。
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端协议的缓存头 { 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()) // 只有在网络请求返回成功之后,才进行缓存处理,否则,404存进缓存,岂不笑话 { responsebody responsebody = response.body(); if (responsebody != null) { responstr = responsebody.string(); if (responstr == null) { responstr = ""; } cachemanager.getinstance(context).setcache(cachemanager.encryptmd5(url + reqbodystr), responstr);//存缓存,以链接+参数进行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); // 发生异常了,我这里就开始去缓存,但是有可能没有缓存,那么久需要丢给下一轮处理了 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));//取缓存,以链接+参数进行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方式下。向服务器发送的参数 * * @param request * @return */ private string getpostparams(request request) { string reqbodystr = ""; string method = request.method(); if ("post".equals(method)) // 如果是post,则尽可能解析每个参数 { 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; } }
以上是主体思路,以及主要实现代码,现在来说一下使用方式
使用方式:
gradle使用:
compile 'com.xiaolei:okhttpcacheinterceptor:1.0.0'
由于是刚刚提交到jcenter,可能会出现拉不下来的情况(暂时还未过审核),着急的读者可以再在你的project:build.gradle里的repositories里新增我maven的链接:
allprojects { repositories { maven{url 'https://dl.bintray.com/kavipyouxiang/maven'} } }
我们新建一个项目,项目截图是这样的:
项目截图
demo很简单,一个主页面,一个bean,一个retrofit,一个网络请求接口
注意,因为是网络,缓存,有关,所以,毫无疑问我们要在manifest里面添加网络请求权限,文件读写权限:
<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)//网络请求超时时间单位为秒 .build();
如果你想哪个接口的数据缓存,那么久为你的网络接口,添加一个请求头cacheheaders.java这个类里包含了所有的情况,一般情况下只需要cacheheaders.normal就可以了
public interface net { @headers(cacheheaders.normal) // 这里是关键 @formurlencoded @post("geocoding") public call<databean> getindex(@field("a") string a); }
业务代码:
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("请求失败!"); } });
我们这里对网络请求,成功了,则在界面上输出文字,加上当前时间,网络失败,则输出一个请求失败。
大概代码就是这样子的,详细代码,文章末尾将贴出demo地址
看效果:演示图
这里演示了,从网络正常,到网络不正常,再恢复到正常的情况。
结尾
以上篇章就是整个从思路,到代码,再到效果图的流程,这里贴一下demo的地址,喜欢的可以点个start
demo地址:https://github.com/xiaolei123/okhttpcacheinterceptor
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: jsp ${param.id}用法