Retrofit和OkHttp使用网络缓存数据

作者: Weechan_ | 来源:发表于2018-11-10 11:03 被阅读20次

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

    相关文章

      网友评论

      本文标题:Retrofit和OkHttp使用网络缓存数据

      本文链接:https://www.haomeiwen.com/subject/yikzxqtx.html