Retrofit和OkHttp使用网络缓存数据

OkHttp缓存优化你的应用

Okhttp缓存原理

我们先从HTTP协议开始入手,关于缓存的HTTP请求/返回头由以下几个,我列了张表格一一解释

请求头/返回头 含义
Cache-Control 这个字段用于指定所有缓存机制在整个请求/响应链中
必须服从的指令。
Pragma 与Cache-Control一样,是兼容HTTP1.0的头部
Expires 资源过期时间
Last-Modified 资源最后修改的时间
If-Modified-Since 在请求头中指定一个日期,若资源最后更新时间超过该日期,
则服务器接受请求,相反的头为If-Unmodified-Since
ETag 识别内容版本的唯一字符串,与资源关联的记号

与缓存最相关的Cache-Control有多条指令,并且在请求或返回头中的效果不一样

在请求头中Cache-Control的指令

指令 参数 说明
no-cache 缓存必须向服务器确认是否过期候才能使用,
即不接受过期缓存,并非不缓存
no-store 真正意义上的不缓存
max-age=[秒] 必须 响应的最大age值
max-stale=[秒] 可忽略 可接受的最大过期时间
min-fresh=[秒] 必须 询问再过[秒]时间后资源是否过期,若过期则不返回
only-if-cached 只获取缓存的资源而不联网获取

在返回头中Cache-Control的指令

指令 参数 说明
public 可向任意方提供响应的缓存
private 向特定用户提供响应缓存
no-cache 可省略 不缓存
no-store 不缓存
max-age=[秒] 必须 响应的最大age值
max-stale=[秒] 可忽略 可接受的最大过期时间
min-fresh=[秒] 必须 询问再过[秒]时间后资源是否过期,若过期则不返回
only-if-cached 只获取缓存的资源而不联网获取

假设Okhttp完全遵守HTTP协议(实际上应该也是),利用Cache-Control我们可以缓存某些必要的资源.
1.有网络的时候:短时间内频繁的请求,后面的请求使用缓存中的资源.
2.无网络的时候:获取之前缓存的数据进行暂时的页面显示,当网络更新时对当前activity的数据进行刷新,刷新界面,避免界面空白的场景.

编写OKHTTP网络拦截器

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) {
            //有网络,检查10秒内的缓存
            req = chain.request()
                    .newBuilder()
                    .cacheControl(new CacheControl
                            .Builder()
                            .maxAge(10, TimeUnit.SECONDS)
                            .build())
                    .build();
        } else {
            //无网络,检查30天内的缓存,即使是过期的缓存
            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的区别
            .addInterceptor(new CacheInterceptor())
            .addNetworkInterceptor(new CacheNetworkInterceptor())
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(10, TimeUnit.SECONDS)
            .build();
    //最后通过使用该HTTP Client进行网络请求, 就实现上述利用缓存优化应用的需求

在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主要的作用是判断当前网络是否有效,如果有效,则创建一个请求,
该请求能获取一个10秒内未过期的缓存,否则强制获取一个缓存(过期了30天也允许).
而CacheNetworkInterceptor 主要是在缓存没命中的情况下,请求网络后,修改返回头,加上Cache-Control,告知OKHTTP对该请求进行一个60秒的缓存.

因此,当频繁请求的时候,OKHTTP使用10秒之内的缓存而不重复请求网络.
当没网络的时候,请求会获取30天内的缓存,避免界面白屏.


OKHTTP关于Cache的源码分析

分析源码之前先看下Cache的策略


Cache.png
Response getResponseWithInterceptorChain() throws IOException {
    // Okhttp获取Response的入口
    // 采用责任链模式,一层层按顺序转交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());
    }
    // 发起实际请求的拦截器
    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的实现
CacheInterceptor代码比较长,我们分段来解释


 @Override public Response intercept(Chain chain) throws IOException {
 
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;
    // 实际上是类似map,将返回内容的URL的MD5的值当key,返回内容当response
    // 然后从cache文件里面查询是否存在该缓存

    long now = System.currentTimeMillis();
    //根据当前的时间,以及缓存策略,来获取response
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    // 根据策略得到cacheReposne 与 NetworkRequest
    // 之后的代码就是根据这两个东西设置返回头

    // 不进行网络请求,且缓存以及过期了,返回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();
    }

    // 不进行网络请求,此时缓存命中,直接返回缓存,后面的拦截器也不会调用了
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
    
    // 否则需要请求网络,继续调用责任链后面的拦截器,请求网络并获取response
    Response networkResponse = null;
    try {
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // 请求异常,关闭缓存避免泄漏
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }
    
    // 请求了网络的同时,缓存其实也找到的情况
    // (比如 需要向服务器确认缓存是否可用的情况)
    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);
      }
      
      // 当请求的协议是https的时候,如果cache没有hansake就丢弃缓存
      if (request.isHttps() && cacheResponse.handshake() == null) {
        return new CacheStrategy(request, null);
      }
      
      /// -- 省略一些代码
      
      // 根据缓存的缓存时间,缓存可接受最大过期时间等等HTTP协议上的规范
      // 来判断缓存是否可用,
            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这三种属性存在时
      //需要向服务器确认缓存的有效性
      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);

      // 构造一个请求询问服务器资源是否过期
      Request conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build();
      return new CacheStrategy(conditionalRequest, cacheResponse);
    

借用一张图来说明http的整个工作流程

image

流程也很清晰明了了,简单的说及时通过Request创建RealCall对象,
经过层层interceptor之后最终产生一个response.
不过值得注意的是,当CacheInterceptor命中缓存之后, 后面的拦截器将不再执行.
这也是addInterceptor 与 addNetworkInterceptor之间的区别


最后附上当网络可用的时候,自动重新请求的一个基于MVP模式的实现方案

NetStatusMonitor是一个单例,用于监听整个应用程序的网络状态
ActivityManager也是一个单例,用来管理应用程序的活动栈,原理Application注册关于活动的生命周期监听.

基于MVP模式,给presenter的抽象基类定义一个refresh的方法
当断网时间超过XX秒的时候,调用在栈顶的activity的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刷新数据
                        this.presenter.refresh()
                    }
                }
            }
        }

        override fun onNetStateChange(oldState: Int, newState: Int) {
            if(newState == NetStatusMonitor.MOBILE){
                showToast("正在使用移动网络")
            }
        }
    })




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()
            }
        })

    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容