一日一学_okhttp(本地缓存)

作者: WuXiao_ | 来源:发表于2017-01-12 20:30 被阅读519次

    在学习okhttp缓存策略之前,我先思考了web前端浏览器缓存的策略。
    浏览器缓存(客户端缓存),它分为强缓存和协商缓存

    • 强缓存
      浏览器在加载资源时,先根据资源http header判断它是否命中强 缓存,强缓存如果命中,浏览器直接从自己的缓存中读取资源,不会发请求 到服务器。
    • 协商缓存
      当强缓存没有命中缓存时,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外的http header验证这个资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就又会从自己的缓存中去加载这个资源;

    强缓存

    强缓存是利用Expires或者Cache-Control这两个http response header实现的,它们都用来表示资源在客户端缓存的有效期。

    • Expires
      Expires是http1.0提出的一个表示资源过期时间的header,它描述的是一个绝对时间,由服务器返回,用GMT格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,它的缓存原理是:
      1 .浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在respone的header加上Expires的header
    1. 浏览器在接收到这个资源后,会把这个资源连同所有response header一起缓存下来
    2. 浏览器再次请求这个资源时,先从缓存中寻找,找到这个资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires指定的时间之前,就能命中缓存,否则就不行。
    3. 如果缓存没有命中,浏览器直接从服务器加载资源时,Expires Header在重新加载的时候会被更新。

    Expires是较老的强缓存管理header,由于它是服务器返回的一个绝对时间,在服务器时间与客户端时间相差较大时,缓存管理容易出现问题,比如随意修改下客户端时间,就能影响缓存命中的结果。所以在http1.1的时候,提出了一个新的header,就是Cache-Control,这是一个相对时间,在配置缓存的时候,以秒为单位,用数值表示,如:Cache-Control:max-age=315360000,它的缓存原理与Expires相似。

    • Cache-Control与Expires不同之处
      Cache-Control描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较Expires,Cache-Control的缓存管理更有效,安全一些。这两个header可以只启用一个,也可以同时启用,当response header中,Expires和Cache-Control同时存在时,Cache-Control优先级高于Expires:

    协商缓存

    协商缓存跟强缓存不一样,强缓存不发请求到服务器,所以有时候资源更新了都在本地,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。大部分web服务器都默认开启协商缓存,而且是同时启用(Last-Modified,If-Modified-Since) 和 (ETag、If-None-Match)。

    • Last-Modified,If-Modified-Since
    1. 浏览器第一次跟服务器请求资源时,服务器在返回这个资源的同时,在respone的header加上Last-Modified的header,这个header表示这个资源在服务器上的最后修改时间。
    2. 浏览器再次跟服务器请求这个资源时,在request的header上加上If-Modified-Since的header,这个header的值就是上一次请求时返回的Last-Modified的值。
    3. 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since与服务器上的最后修改时间判断,如果没有变化则返回304 Not Modified(不会返回资源内容,header也不会改变);如果有变化,就正常返回资源内容。
    4. 浏览器收到304的响应后,就会从缓存中加载资源。(没有命中,浏览器直接从服务器加载资源,Header在重新加载更新)

    (Last-Modified,If-Modified-Since)根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合是非常可靠的,但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况,而这种问题又很不容易被定位出来,而当这种情况出现的时候,就会影响协商缓存的可靠性。所以就有了另外一对header来管理协商缓存,这对header就是(ETag、If-None-Match)。

    • ETag、If-None-Match
    1. 览器第一次跟服务器请求资源,服务器在返回这个资源的同时,在respone的header加上ETag的header,这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系.
    2. 浏览器再次跟服务器请求这个资源时,在request的header上加上If-None-Match的header,这个header的值就是上一次请求时返回的ETag的值.
    3. 服务器再次收到资源请求时,根据浏览器传过来If-None-Match和然后再根据资源生成一个新的ETag,如果这两个值相同就说明资源没有变化,否则就是有变化;如果没有变化则返回304 Not Modified,如果有变化,就正常返回资源内容。与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化.
    4. 浏览器收到304的响应后,就会从缓存中加载资源。

    上面为http简单缓存知识。接下来我们来查看okhttp缓存策略(和浏览器原理差不多,要不总结这么多白瞎了)。

    okHttp源码分析

    OkHttp中缓存策略与浏览器处理大同小异。
    我们只看CacheStrategy的getCandidate()方法

    private CacheStrategy getCandidate() {
      //如果缓存没有命中,就不需要加缓存Header了
      if (cacheResponse == null) {
        //没有缓存的网络请求,直接访问
        return new CacheStrategy(request, null);
      }
    
      // 如果缓存的TLS握手信息丢失,返回进行直接连接
      if (request.isHttps() && cacheResponse.handshake() == null) {
        //直接访问
        return new CacheStrategy(request, null);
      }
    
      //检测response的状态码,Expired时间,是否有no-cache标签
      if (!isCacheable(cacheResponse, request)) {
        //直接访问
        return new CacheStrategy(request, null);
      }
    
      CacheControl requestCaching = request.cacheControl();
    
      //有ETag/Since标签
      if (requestCaching.noCache() || hasConditions(request)) {
        //直接连接,把缓存判断交给服务器
        return new CacheStrategy(request, null);
      }
      //根据RFC协议计算
      long ageMillis = cacheResponseAge();
      //max-age
      long freshMillis = computeFreshnessLifetime();
    
      if (requestCaching.maxAgeSeconds() != -1) {
        //max-age
        freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
      }
    
      long minFreshMillis = 0;
      if (requestCaching.minFreshSeconds() != -1) {
        //大部分情况下设置是0
        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
      }
    
      long maxStaleMillis = 0;
      //ParseHeader中的缓存控制信息
      CacheControl responseCaching = cacheResponse.cacheControl();
      if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        //设置最大过期时间,一般设置为0
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
      }
    
      //缓存在过期时间内,可以使用
      if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        //返回上次的缓存
        Response.Builder builder = cacheResponse.newBuilder();
        return new CacheStrategy(null, builder.build());
      }
    
      //缓存失效, 如果有etag等信息
      //发送请求,交给服务器处理
      Request.Builder conditionalRequestBuilder = request.newBuilder();
    
      if (etag != null) {
        conditionalRequestBuilder.header("If-None-Match", etag);
      } else if (lastModified != null) {
        conditionalRequestBuilder.header("If-Modified-Since", lastModifiedString);
      } else if (servedDate != null) {
        conditionalRequestBuilder.header("If-Modified-Since", servedDateString);
      }
      //网络请求
      Request conditionalRequest = conditionalRequestBuilder.build();
      return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest,
          cacheResponse) : new CacheStrategy(conditionalRequest, null);
    }
    

    okhttp源码可以看出缓存完全由服务器Header决定的,自己没有必要进行控制。用Interceptor中手工添加缓存代码控制,在实时换取更换数据的时候,会出现使用缓存数据的风险(自己项目出现过bug)。

    嘿嘿嘿,前面都是铺垫和我遇到的坑,接下来才是本文正文:

    使用Rxjava进行缓存

    先对RxCache 对象进行数据的初始化

    public final class RxCache {
     //缓存是基于LruCache,DiskLruCache上进行的,这一步是初始化这俩个
    //主角,后面会进行讲解。
     private RxCache(int memoryMaxSize, int appVersion, long diskMaxSize, File diskDir, IDiskConverter diskConverter) {
            cacheCore = new CacheCore(new LruMemoryCache(memoryMaxSize), new LruDiskCache(diskConverter,diskDir,appVersion,diskMaxSize));
        }
    
     public static final class Builder {
           private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
            private static final int DEFAULT_MEMORY_CACHE_SIZE=(int) (Runtime.getRuntime().maxMemory()/8);//运行内存的8分之1
            private int memoryMaxSize;
            private int appVersion;
            private long diskMaxSize;
            private File diskDir;
            private IDiskConverter diskConverter;
    
            public Builder(){
            }
    
            /**
             * 不设置,默认为运行内存的8分之1
             */
            public Builder memorySize(int maxSize) {
                this.memoryMaxSize = maxSize;
                return this;
            }
    
            /**
             * 不设置,默认为1
             */
            public Builder appVersion(int appVersion) {
                this.appVersion = appVersion;
                return this;
            }
    
            public Builder diskDir(File directory) {
                this.diskDir = directory;
                return this;
            }
    
    
            public Builder diskConverter(IDiskConverter converter) {
                this.diskConverter = converter;
                return this;
            }
    
            /**
             * 不设置, 默为认50MB
             */
            public Builder diskSize(long maxSize) {
                this.diskMaxSize = maxSize;
                return this;
            }
            public RxCache build() {
                if(this.diskDir==null){
                    throw new NullPointerException("DiskDir can not be null");
                }
                if (!this.diskDir.exists()) {
                   this.diskDir.mkdirs();
                }
                if(this.diskConverter==null){
                    this.diskConverter=new DiskConverter();
                }
                if(memoryMaxSize<=0){
                    memoryMaxSize= DEFAULT_MEMORY_CACHE_SIZE;
                }
                if(diskMaxSize<=0){
                    diskMaxSize=MAX_DISK_CACHE_SIZE;
    
                }
                appVersion= Math.max(1,this.appVersion);
                //初始化
                return  new RxCache(memoryMaxSize,appVersion,diskMaxSize,diskDir,diskConverter);
            }
    
           
        }
    }
    
    

    也可以自己进行配置

      //前面我讲过Builder模式,这里就可以体会到了
      public static RxCache getRxCache(Context context) {
    
            RxCache rxCache = new RxCache.Builder()
                    .diskDir(new File(context.getCacheDir().getPath() + File.separator + "data"))
                    .diskConverter(new DiskConverter())
                    .memorySize(2*1024*1024)
                    .build();
           return rxCache;
        }
    
    

    DiskConverter这个转换类,为了以后扩展你可以存到本地或者数据库。

    public class DiskConverter implements IDiskConverter {
    
        @Override
        public Object load(InputStream source) {
            Object value = null;
            ObjectInputStream oin = null;
            try {
                oin = new ObjectInputStream(source);
                value = oin.readObject();
            } catch (IOException | ClassNotFoundException e) {
            } finally {
                close(oin);
            }
            return value;
        }
    
        @Override
        public boolean writer(OutputStream sink, Object data) {
            ObjectOutputStream oos = null;
            try {
                oos = new ObjectOutputStream(sink);
                oos.writeObject(data);
                oos.flush();
                return true;
            } catch (IOException e) {
                return false;
            } finally {
                close(oos);
            }
        }
    
        public  void close(Closeable close) {
            if (close != null) {
                try {
                    closeThrowException(close);
                } catch (IOException ignored) {
                }
            }
        }
    
        public void closeThrowException(Closeable close) throws IOException {
            if (close != null) {
                close.close();
            }
        }
    }
    
    
    

    加载网络网址事例:

      RxNetwork.getInstance()
                    .createApi(Api.class, false)
                    .getAd(1, 2)
                     //主要这一部分,缓存的核心一步
                    .compose(rxCache.<AdBean>transformer("cache", CacheProviders.cache))
                    .subscribeOn(Schedulers.io())
                    .observeOn(AndroidSchedulers.mainThread())
                    .subscribe(new RxSubscriber<AdBean>() {
                        @Override
                        public void onSuccess(AdBean adBeanResult) {
                            Log.i("MainActivity", adBeanResult.getData().getAdList().get(0).getPicUrl());
                        }
    
                        @Override
                        public void onFailed(Throwable e) {
    
                        }
                    });
    
    

    讲之前我们重新认识Rxjava的Transformer这个老朋友。

    Transformer代码是Func1<Observable<T>, Observable<R>>,换言之就是:可以通过它将一种类型的Observable转换成另一种类型的Observable。呸,这是什么鬼,每篇博客都这么写,demo还不给,逗我玩。
    我给大家留下demo,自己琢磨去吧

    //运行....
    main(){
    
    Observable.just("1","2").compose(RxDemoTransformer.<String>transformerTest()).subscribe(new Subscriber<Integer>() {
                @Override
                public void onCompleted() {
    
                }
    
                @Override
                public void onError(Throwable e) {
    
                }
    
                @Override
                public void onNext(Integer integer) {
                    Log.i("MainActivity",integer.intValue()+"");
                }
            });
    
    }
    -------------------------------
    public class RxDemoTransformer {
      public  static <String>Observable.Transformer<String,Integer> transformerTest(){
          return new Observable.Transformer<String,Integer>(){
    
              @Override
              public Observable<Integer> call(Observable<String> stringObservable) {
                  return stringObservable.map(new Func1<String, Integer>() {
                      @Override
                      public Integer call(String string) {
                          return Integer.decode((java.lang.String) string);
                      }
                  });
              }
          };
      }
    }
    
    

    运行结果了,本人懒自己运行去吧。

      //通过key进行数据的内存缓存和本地缓存,CacheProviders 是控制网络和本地的终于类
      public <T> Observable.Transformer<T, Result<T>> transformer(final 
                          String key, final CacheProviders providers) {
            return new Observable.Transformer<T, Result<T>>() {
                @Override
                public Observable<Result<T>> call(Observable<T> tObservable) {
                    return providers.execute(RxCache.this,key,tObservable);
                }
            };
        }
    
    

    来看看CacheProviders这个如何进行网络,缓存切换的(还存在bug,大家慢慢找,呵呵。。。)

    public final class CacheProviders {
       
        public static final CacheProviders cache=new CacheProviders();
    
        public <T> Observable<Result<T>> execute(RxCache rxCache, String key, Observable<T> source) {
            Observable<Result<T>> cache = loadCache(rxCache,key);
            Observable<Result<T>> remote = loadRemote(rxCache,key, source, CacheType.MemoryAndDisk)
                    .onErrorReturn(new Func1<Throwable, Result<T>>() {
                        @Override
                        public Result<T> call(Throwable throwable) {
                            return null;
                        }
                    });
            return Observable.concat(remote, cache)
                    .firstOrDefault(null, new Func1<Result<T>, Boolean>() {
                        @Override
                        public Boolean call(Result<T> tResultData) {
                            return tResultData != null && tResultData.data != null;
                        }
                    });
    
        }
    
       //加载缓存数据
        <T> Observable<Result<T>> loadCache(final RxCache rxCache, final String key) {
            return rxCache
                    .<T>load(key)
                    .map(new Func1<T, Result<T>>() {
                        @Override
                        public Result<T> call(T o) {
    
                            return new Result<>(ResultFrom.Cache, key,  o);
                        }
                    });
        }
            //加载网络数据
         <T> Observable<Result<T>> loadRemote(final RxCache rxCache, final String key, Observable<T> source, final CacheType target) {
            return source
                    .map(new Func1<T, Result<T>>() {
                        @Override
                        public Result<T> call(T t) {
                            //保存网络数据
                            rxCache.save(key, t,target).subscribeOn(Schedulers.io())
                                    .subscribe(new Action1<Boolean>() {
                                        @Override
                                        public void call(Boolean status) {
    
                                        }
                                    });
                            return new Result<>(ResultFrom.Remote, key, t);
                        }
                    });
        }
    
    
    }
    
    

    OnErrorReturn是什么鬼。
    OnErrorReturn-当发生错误的时候,让Observable发射一个预先定义好的数据并正常地终止


    onErrorReturn

    举上面的例子,当loadRemote没有网络就会报错,立马会执行onErrorReturn返回一个null。

    concat()操作符持有多个Observable对象,并将它们按顺序串联成队列。
    firstOrDefault() 阻塞直到Observable发射了一个数据或者终止,返回第一项数据,或者返回默认值.又是一些概念,让老夫撸一串代码,就明白了。

    
            Observable<String>  oba =Observable.just("1");
            Observable<String>  obb =Observable.just("2");
    
            Observable.concat(oba, obb).firstOrDefault(null, new Func1<String, Boolean>() {
                @Override
                public Boolean call(String s) {
                    if (s.equals("2")){
                        return true;
                    }
                    return false;
                }
            }).subscribe(new Subscriber<String>() {
                @Override
                public void onCompleted() {
    
                }
    
                @Override
                public void onError(Throwable e) {
    
                }
    
                @Override
                public void onNext(String s) {
                    Log.i("MainActivity","MainActivity --->"+s);
                }
            });
    

    运行结果

    MainActivity: MainActivity --->2
    

    通过demo是不是我上面的缓存代码明白是什么策略了。

    //缓存核心,LruMemoryCache 对lruCache封装,进行保存与读取
    //LruDiskCache DiskLruCache进行了封装,进行保存与读取
    class CacheCore {
    
        private LruMemoryCache memory;
        private LruDiskCache disk;
    
        CacheCore(LruMemoryCache memory, LruDiskCache disk) {
            this.memory = memory;
            this.disk = disk;
        }
    
    
        /**
         * 读取
         */
        <T> T load(String key) {
            if (memory != null) {
                T result = memory.load(key);
                if (result != null) {
                    return result;
                }
            }
    
            if (disk != null) {
                T result = disk.load(key);
                if (result != null) {
                    return result;
                }
            }
    
            return null;
        }
    
        /**
         * 保存
         */
        <T> boolean save(String key, T value, CacheType target) {
            if (value == null) { //如果要保存的值为空,则删除
                return memory.remove(key) && disk.remove(key);
            }
    
            if (target.supportMemory() && memory != null) {
                memory.save(key, value);
            }
            if (target.supportDisk() && disk != null) {
                return disk.save(key, value);
            }
    
            return false;
        }
    
    }
    
    

    LruMemoryCache 存储到内存中,下次加载页面更快加载,提高用户体验.

    class LruMemoryCache {
        //lruCache算法是最近最少使用算法(LinkedHashMap封装)。
        //我会新写一篇解释lruCache的实现
        private LruCache<String, Serializable> mCache;
        private final HashSet<String> mKeySet;
    
        public LruMemoryCache(final int cacheSize) {
            mKeySet = new HashSet<>();
            mCache = new LruCache<String, Serializable>(cacheSize) {
                @Override
                protected int sizeOf(String key, Serializable value) {
                   return  calcSize(value);
                }
            };
        }
       //现在明白,当获取数据的时候,数据会排到LinkedHashMap队尾就可以
        public <T> T load(String key) {
             return (T) mCache.get(key);
        }
        
        public <T> boolean save(String key, T value) {
            if (null != value) {
                mCache.put(key, (Serializable) value);
                mKeySet.add(key);
            }
            return true;
    }
     ..........
    }
    

    LruDiskCache存储到本地核心类(DiskLruCache 我会新写一篇进行讲解)

    class LruDiskCache {
        private IDiskConverter mDiskConverter;
        private DiskLruCache mDiskLruCache;
    
    
         LruDiskCache(IDiskConverter diskConverter, File diskDir, int appVersion, long diskMaxSize) {
            this.mDiskConverter = diskConverter;
            try {
                mDiskLruCache = DiskLruCache.open(diskDir, appVersion, 1, diskMaxSize);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
         <T> T load(String key) {
            if (mDiskLruCache == null) {
                return null;
            }
            try {
                DiskLruCache.Editor edit = mDiskLruCache.edit(key);
                if (edit == null) {
                    return null;
                }
                InputStream source = edit.newInputStream(0);
                T value ;
                if (source != null) {
                    value = (T) mDiskConverter.load(source);
                    close(source);
                    edit.commit();
                    return value;
                }
                edit.abort();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
    
         <T> boolean save(String key, T value) {
            if (mDiskLruCache == null) {
                return false;
            }
            //如果要保存的值为空,则删除
            if (value == null) {
                return remove(key);
            }
            try {
                DiskLruCache.Editor edit = mDiskLruCache.edit(key);
                if (edit == null) {
                    return false;
                }
                OutputStream sink = edit.newOutputStream(0);
                if (sink != null) {
                    mDiskConverter.writer(sink, value);
                    close(sink);
                    edit.commit();
                    return true;
                }
                edit.abort();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return false;
        }
        ....................
    }
    

    项目地址:https://github.com/quiet-wuxiao/RxHttp

    相关文章

      网友评论

      本文标题:一日一学_okhttp(本地缓存)

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