一日一學(xué)_okhttp(本地緩存)

在學(xué)習(xí)okhttp緩存策略之前,我先思考了web前端瀏覽器緩存的策略。
瀏覽器緩存(客戶端緩存),它分為強緩存和協(xié)商緩存

  • 強緩存
    瀏覽器在加載資源時,先根據(jù)資源http header判斷它是否命中強 緩存,強緩存如果命中,瀏覽器直接從自己的緩存中讀取資源,不會發(fā)請求 到服務(wù)器。
  • 協(xié)商緩存
    當(dāng)強緩存沒有命中緩存時,瀏覽器一定會發(fā)送一個請求到服務(wù)器,通過服務(wù)器端依據(jù)資源的另外的http header驗證這個資源是否命中協(xié)商緩存,如果協(xié)商緩存命中,服務(wù)器會將這個請求返回,但是不會返回這個資源的數(shù)據(jù),而是告訴客戶端可以直接從緩存中加載這個資源,于是瀏覽器就又會從自己的緩存中去加載這個資源;

強緩存

強緩存是利用Expires或者Cache-Control這兩個http response header實現(xiàn)的,它們都用來表示資源在客戶端緩存的有效期。

  • Expires
    Expires是http1.0提出的一個表示資源過期時間的header,它描述的是一個絕對時間,由服務(wù)器返回,用GMT格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT,它的緩存原理是:
    1 .瀏覽器第一次跟服務(wù)器請求一個資源,服務(wù)器在返回這個資源的同時,在respone的header加上Expires的header
  1. 瀏覽器在接收到這個資源后,會把這個資源連同所有response header一起緩存下來
  2. 瀏覽器再次請求這個資源時,先從緩存中尋找,找到這個資源后,拿出它的Expires跟當(dāng)前的請求時間比較,如果請求時間在Expires指定的時間之前,就能命中緩存,否則就不行。
  3. 如果緩存沒有命中,瀏覽器直接從服務(wù)器加載資源時,Expires Header在重新加載的時候會被更新。

Expires是較老的強緩存管理header,由于它是服務(wù)器返回的一個絕對時間,在服務(wù)器時間與客戶端時間相差較大時,緩存管理容易出現(xiàn)問題,比如隨意修改下客戶端時間,就能影響緩存命中的結(jié)果。所以在http1.1的時候,提出了一個新的header,就是Cache-Control,這是一個相對時間,在配置緩存的時候,以秒為單位,用數(shù)值表示,如:Cache-Control:max-age=315360000,它的緩存原理與Expires相似。

  • Cache-Control與Expires不同之處
    Cache-Control描述的是一個相對時間,在進行緩存命中的時候,都是利用客戶端時間進行判斷,所以相比較Expires,Cache-Control的緩存管理更有效,安全一些。這兩個header可以只啟用一個,也可以同時啟用,當(dāng)response header中,Expires和Cache-Control同時存在時,Cache-Control優(yōu)先級高于Expires:

協(xié)商緩存

協(xié)商緩存跟強緩存不一樣,強緩存不發(fā)請求到服務(wù)器,所以有時候資源更新了都在本地,但是協(xié)商緩存會發(fā)請求到服務(wù)器,所以資源是否更新,服務(wù)器肯定知道。大部分web服務(wù)器都默認開啟協(xié)商緩存,而且是同時啟用(Last-Modified,If-Modified-Since) 和 (ETag、If-None-Match)。

  • Last-Modified,If-Modified-Since
  1. 瀏覽器第一次跟服務(wù)器請求資源時,服務(wù)器在返回這個資源的同時,在respone的header加上Last-Modified的header,這個header表示這個資源在服務(wù)器上的最后修改時間。
  2. 瀏覽器再次跟服務(wù)器請求這個資源時,在request的header上加上If-Modified-Since的header,這個header的值就是上一次請求時返回的Last-Modified的值。
  3. 服務(wù)器再次收到資源請求時,根據(jù)瀏覽器傳過來If-Modified-Since與服務(wù)器上的最后修改時間判斷,如果沒有變化則返回304 Not Modified(不會返回資源內(nèi)容,header也不會改變);如果有變化,就正常返回資源內(nèi)容。
  4. 瀏覽器收到304的響應(yīng)后,就會從緩存中加載資源。(沒有命中,瀏覽器直接從服務(wù)器加載資源,Header在重新加載更新)

(Last-Modified,If-Modified-Since)根據(jù)服務(wù)器時間返回的header,一般來說,在沒有調(diào)整服務(wù)器時間和篡改客戶端緩存的情況下,這兩個header配合是非常可靠的,但是有時候也會服務(wù)器上資源其實有變化,但是最后修改時間卻沒有變化的情況,而這種問題又很不容易被定位出來,而當(dāng)這種情況出現(xiàn)的時候,就會影響協(xié)商緩存的可靠性。所以就有了另外一對header來管理協(xié)商緩存,這對header就是(ETag、If-None-Match)。

  • ETag、If-None-Match
  1. 覽器第一次跟服務(wù)器請求資源,服務(wù)器在返回這個資源的同時,在respone的header加上ETag的header,這個header是服務(wù)器根據(jù)當(dāng)前請求的資源生成的一個唯一標識,這個唯一標識是一個字符串,只要資源有變化這個串就不同,跟最后修改時間沒有關(guān)系.
  2. 瀏覽器再次跟服務(wù)器請求這個資源時,在request的header上加上If-None-Match的header,這個header的值就是上一次請求時返回的ETag的值.
  3. 服務(wù)器再次收到資源請求時,根據(jù)瀏覽器傳過來If-None-Match和然后再根據(jù)資源生成一個新的ETag,如果這兩個值相同就說明資源沒有變化,否則就是有變化;如果沒有變化則返回304 Not Modified,如果有變化,就正常返回資源內(nèi)容。與Last-Modified不一樣的是,當(dāng)服務(wù)器返回304 Not Modified的響應(yīng)時,由于ETag重新生成過,response header中還會把這個ETag返回,即使這個ETag跟之前的沒有變化.
  4. 瀏覽器收到304的響應(yīng)后,就會從緩存中加載資源。

上面為http簡單緩存知識。接下來我們來查看okhttp緩存策略(和瀏覽器原理差不多,要不總結(jié)這么多白瞎了)。

okHttp源碼分析

OkHttp中緩存策略與瀏覽器處理大同小異。
我們只看CacheStrategy的getCandidate()方法

private CacheStrategy getCandidate() {
  //如果緩存沒有命中,就不需要加緩存Header了
  if (cacheResponse == null) {
    //沒有緩存的網(wǎng)絡(luò)請求,直接訪問
    return new CacheStrategy(request, null);
  }

  // 如果緩存的TLS握手信息丟失,返回進行直接連接
  if (request.isHttps() && cacheResponse.handshake() == null) {
    //直接訪問
    return new CacheStrategy(request, null);
  }

  //檢測response的狀態(tài)碼,Expired時間,是否有no-cache標簽
  if (!isCacheable(cacheResponse, request)) {
    //直接訪問
    return new CacheStrategy(request, null);
  }

  CacheControl requestCaching = request.cacheControl();

  //有ETag/Since標簽
  if (requestCaching.noCache() || hasConditions(request)) {
    //直接連接,把緩存判斷交給服務(wù)器
    return new CacheStrategy(request, null);
  }
  //根據(jù)RFC協(xié)議計算
  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) {
    //大部分情況下設(shè)置是0
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
  }

  long maxStaleMillis = 0;
  //ParseHeader中的緩存控制信息
  CacheControl responseCaching = cacheResponse.cacheControl();
  if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
    //設(shè)置最大過期時間,一般設(shè)置為0
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
  }

  //緩存在過期時間內(nèi),可以使用
  if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
    //返回上次的緩存
    Response.Builder builder = cacheResponse.newBuilder();
    return new CacheStrategy(null, builder.build());
  }

  //緩存失效, 如果有etag等信息
  //發(fā)送請求,交給服務(wù)器處理
  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);
  }
  //網(wǎng)絡(luò)請求
  Request conditionalRequest = conditionalRequestBuilder.build();
  return hasConditions(conditionalRequest) ? new CacheStrategy(conditionalRequest,
      cacheResponse) : new CacheStrategy(conditionalRequest, null);
}

okhttp源碼可以看出緩存完全由服務(wù)器Header決定的,自己沒有必要進行控制。用Interceptor中手工添加緩存代碼控制,在實時換取更換數(shù)據(jù)的時候,會出現(xiàn)使用緩存數(shù)據(jù)的風(fēng)險(自己項目出現(xiàn)過bug)。

嘿嘿嘿,前面都是鋪墊和我遇到的坑,接下來才是本文正文:

使用Rxjava進行緩存

先對RxCache 對象進行數(shù)據(jù)的初始化

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);//運行內(nèi)存的8分之1
        private int memoryMaxSize;
        private int appVersion;
        private long diskMaxSize;
        private File diskDir;
        private IDiskConverter diskConverter;

        public Builder(){
        }

        /**
         * 不設(shè)置,默認為運行內(nèi)存的8分之1
         */
        public Builder memorySize(int maxSize) {
            this.memoryMaxSize = maxSize;
            return this;
        }

        /**
         * 不設(shè)置,默認為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;
        }

        /**
         * 不設(shè)置, 默為認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這個轉(zhuǎn)換類,為了以后擴展你可以存到本地或者數(shù)據(jù)庫。

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


加載網(wǎng)絡(luò)網(wǎng)址事例:

  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轉(zhuǎn)換成另一種類型的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);
                  }
              });
          }
      };
  }
}

運行結(jié)果了,本人懶自己運行去吧。

  //通過key進行數(shù)據(jù)的內(nèi)存緩存和本地緩存,CacheProviders 是控制網(wǎng)絡(luò)和本地的終于類
  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這個如何進行網(wǎng)絡(luò),緩存切換的(還存在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;
                    }
                });

    }

   //加載緩存數(shù)據(jù)
    <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);
                    }
                });
    }
        //加載網(wǎng)絡(luò)數(shù)據(jù)
     <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) {
                        //保存網(wǎng)絡(luò)數(shù)據(jù)
                        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-當(dāng)發(fā)生錯誤的時候,讓Observable發(fā)射一個預(yù)先定義好的數(shù)據(jù)并正常地終止


onErrorReturn

舉上面的例子,當(dāng)loadRemote沒有網(wǎng)絡(luò)就會報錯,立馬會執(zhí)行onErrorReturn返回一個null。

concat()操作符持有多個Observable對象,并將它們按順序串聯(lián)成隊列。
firstOrDefault() 阻塞直到Observable發(fā)射了一個數(shù)據(jù)或者終止,返回第一項數(shù)據(jù),或者返回默認值.又是一些概念,讓老夫擼一串代碼,就明白了。


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

運行結(jié)果

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 存儲到內(nèi)存中,下次加載頁面更快加載,提高用戶體驗.

class LruMemoryCache {
    //lruCache算法是最近最少使用算法(LinkedHashMap封裝)。
    //我會新寫一篇解釋lruCache的實現(xiàn)
    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);
            }
        };
    }
   //現(xiàn)在明白,當(dāng)獲取數(shù)據(jù)的時候,數(shù)據(jù)會排到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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容