你不知道的Retrofit緩存庫RxCache

推薦:看到如此多的 MVP+Dagger2+Retrofit+Rxjava 項目, 輕鬆拿 star, 心動了嗎? 看到身邊的朋友都已早早在項目中使用這些技術, 而你還不會, 失落嗎?
MVPArms 是一個 MVP+Dagger2+Retrofit+Rxjava 可配置化快速集成框架(目前 Dagger 應用最複雜可配置化極強的集成框架), 自帶上萬字 文檔 以及 一鍵生成 MVPDagger2 文件等功能, 成熟穩定且已有上千個商業項目接入, 累計 5k+ star(全球第一 MVP 框架), 如今你只用專一於邏輯, 其餘都交給 MVPArms, 快來構建本身的 MVP+Dagger2+Retrofit+Rxjava 項目吧!

原文地址: http://www.jianshu.com/p/b58ef6b0624bjava

前言

Retrofit無疑是當下最火的網絡請求庫,與同門師兄Okhttp配合使用,簡直是每一個項目的標配,由於Okhttp自帶緩存,因此不少人並不關心其餘緩存庫,可是使用過Okhttp緩存的小夥伴,確定知道Okhttp的緩存必須配合Header使用,比較麻煩,也不夠靈活,因此如今爲你們推薦一款專門爲Retrifit打造的緩存庫RxCachegit

項目地址: RxCache Demo地址: RxCacheSamplegithub

簡介

RxCache使用註解來爲Retrofit配置緩存信息,內部使用動態代理和Dagger來實現,這個庫的資料相對較少,官方教程又是全英文的,這無疑給開發者增長了使用難度,其實我英文也很差,可是源碼是通用的啊,因此我爲你們從源碼的角度來說解此庫,此庫源碼的難點其實都在Dagger注入上,我先爲你們講解用法,後面會再寫篇文章講解源碼,在學習Dagger的朋友除了建議看看個人MVPArms外,還能夠看看這個RxCache的源碼,能學到不少東西,先給張RxCache的架構圖,讓你們嚐嚐鮮,請期待我後面的源碼分析數組

使用

1.定義接口,和Retrofit相似,接口中每一個方法和Retrofit接口中的方法一一對應,每一個方法的參數中必須傳入對應Retrofit接口方法的返回值(返回值必須爲Observable,不然報錯),另外幾個參數DynamicKey,DynamicKeyGroupEvictProvider不是必須的,可是若是要傳入,每一個都只能傳入一個對象,不然報錯,這幾個參數的意義是初學者最困惑的,後面會分析緩存

/**
 * 此爲RxCache官方Demo
 */
public interface CacheProviders {

    @LifeCache(duration = 2, timeUnit = TimeUnit.MINUTES)
    Observable<Reply<List<Repo>>> getRepos(Observable<List<Repo>> oRepos, DynamicKey userName, EvictDynamicKey evictDynamicKey);

    @LifeCache(duration = 2, timeUnit = TimeUnit.MINUTES)
    Observable<Reply<List<User>>> getUsers(Observable<List<User>> oUsers, DynamicKey idLastUserQueried, EvictProvider evictProvider);

    Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
}

複製代碼

2.將接口實例化,和Retrofit構建方式相似,將接口經過using方法傳入,返回一個接口的動態代理對象,調用此對象的方法傳入對應參數就能夠實現緩存了,經過註解和傳入不一樣的參數能夠實現一些自定義的配置, so easy~bash

CacheProviders cacheProviders = new RxCache.Builder()
                .persistence(cacheDir, new GsonSpeaker())
                .using(CacheProviders.class);
複製代碼

詳解

其實RxCache的使用比較簡單,上面的兩步就能夠輕鬆的實現緩存,此庫的的特點主要集中在對緩存的自定義配置,因此我來主要講講那些參數和註解是怎麼回事?服務器

參數

Observable網絡

Observable的意義爲須要將你想緩存的Retrofit接口做爲參數傳入(返回值必須爲Observable),RxCache會在沒有緩存,或者緩存已通過期,或者EvictProvidertrue時,經過這個Retrofit接口從新請求最新的數據,而且將服務器返回的結果包裝成Reply返回,返回以前會向內存緩存和磁盤緩存中各保存一份架構

值得一提的是,若是須要知道返回的結果是來自哪裏(本地,內存仍是網絡),是否加密,則可使用Observable<Reply<List<Repo>>>做爲方法的返回值,這樣RxCache則會使用Reply包裝結果,若是沒這個需求則直接在範型中聲明結果的數據類型Observable<List<Repo>>框架

例外

若是構建RxCache的時候將useExpiredDataIfLoaderNotAvailable設置成true,會在數據爲空或者發生錯誤時,忽視EvictProvidertrue或者緩存過時的狀況,繼續使用緩存(前提是以前請求過有緩存)

DynamicKey & DynamicKeyGroup

有不少開發者最困惑的就是這兩個參數的意義,兩個一塊兒傳以及不傳會有影響嗎?說到這裏就要提下,RxCache是怎麼存儲緩存的,RxCache並非經過使用URL充當標識符來儲存和獲取緩存的

那是什麼呢?

沒錯RxCache就是經過這兩個對象加上上面CacheProviders接口中聲明的方法名,組合起來一個標識符,經過這個標識符來存儲和獲取緩存

標識符規則爲:

方法名 + $d$d$d$" + dynamicKey.dynamicKey + "$g$g$g$" + DynamicKeyGroup.group dynamicKey或DynamicKeyGroup爲空時則返回空字符串,即什麼都不傳的標識符爲: "方法名$d$d$d$$g$g$g$" 複製代碼

什麼意思呢?

好比RxCache,的內存緩存使用的是Map,它就用這個標識符做爲Key,put和get數據(本地緩存則是將這個標識符做爲文件名,使用流寫入或讀取這個文件,來儲存或獲取緩存),若是儲存和獲取的標識符不一致那就取不到想取的緩存

和咱們有什麼關係呢?

舉個例子,咱們一個接口具備分頁功能,咱們使用RxCache給他設置了3分鐘的緩存,若是這兩個對象都不傳入參數中,它會默認使用這個接口的方法名去存儲和獲取緩存,意思是咱們以前使用這個接口獲取到了第一頁的數據,三分鐘之內屢次調用這個接口,請求其餘分頁的數據,它返回的緩存仍是第一頁的數據,直到緩存過時,因此咱們如今想具有分頁功能,必須傳入DynamicKey,DynamicKey內部存儲有一個key,咱們在構建的時候傳入頁數,RxCache將會根據不一樣的頁數分別保存一份緩存,它內部作的事就是將方法名+DynamicKey變成一個String類型的標識符去獲取和存儲緩存

DynamicKey和DynamicKeyGroup有什麼關係呢

DynamicKey存儲有一個Key,DynamicKey的應用場景: 請求同一個接口,須要參照一個變量的不一樣返回不一樣的數據,好比分頁,構造時傳入頁數就能夠了

DynamicKeyGroup存儲有兩個key,DynamicKeyGroup是在DynamicKey基礎上的增強版,應用場景:請求同一個接口不只須要分頁,每頁又須要根據不一樣的登陸人返回不一樣的數據,這時候構造DynamicKeyGroup時,在構造函數中第一個參數傳頁數,第二個參數傳用戶標識符就能夠了

理論上DynamicKeyDynamicKeyGroup根據不一樣的需求只用傳入其中一個便可,可是也能夠兩個參數都傳,以上面的需求爲例,兩個參數都傳的話,它會先取DynamicKeyKey(頁數)而後再取DynamicKeyGroup的第二個Key(用戶標識符),加上接口名組成標識符,來獲取和存儲數據,這樣就會忽略DynamicKeyGroup的第一個Key(頁數)

EvictProvider & EvictDynamicKey & EvictDynamicKeyGroup

這三個對象內部都保存有一個boolean類型的字段,其意思爲是否驅逐(使用或刪除)緩存,RxCache在取到未過時的緩存時,會根據這個boolean字段,考慮是否使用這個緩存,若是爲true,就會從新經過Retrofit獲取新的數據,若是爲false就會使用這個緩存

這三個對象有什麼關係呢?

這三個對象是相互繼承關係,繼承關係爲EvictProvider < EvictDynamicKey < EvictDynamicKeyGroup,這三個對象你只能傳其中的一個,多傳一個都會報錯,按理說你無論傳那個對象都同樣,由於裏面都保存有一個boolean字段,根據這個字段判斷是否使用緩存

不一樣在哪呢?

若是有未過時的緩存,而且裏面的booleanfalse時,你傳這三個中的哪個都是同樣的,可是在booleantrue時,這時就有區別了,RxCache會在Retrofit請求到新數據後,在booleantrue時刪除對應的緩存

刪除規則是什麼呢?

仍是以請求一個接口,該接口的數據會根據不一樣的分頁返回不一樣的數據,而且同一個分頁還要根據不一樣用戶顯示不一樣的數據爲例

三個都不傳,RxCache會本身new EvictProvider(false);,這樣默認爲false就不會刪除任何緩存

EvictDynamicKeyGroup 只會刪除對應分頁下,對應用戶的緩存

EvictDynamicKey 會刪除那個分頁下的全部緩存,好比你請求的是第一頁下user1的數據,它不只會刪除user1的數據還會刪除當前分頁下其餘user2,user3...的數據

EvictProvider 會刪除當前接口下的全部緩存,好比你請求的是第一頁的數據,它不只會刪除第一頁的數據,還會把這個接口下其餘分頁的數據全刪除

因此你能夠根據本身的邏輯選擇傳那個對象,若是請求的這個接口沒有分頁功能,這時你不想使用緩存,按理說你應該傳EvictProvider,而且在構造時傳入true,可是你若是傳EvictDynamicKeyEvictDynamicKeyGroup達到的效果也是同樣

註解

@LifeCache

@LifeCache顧名思義,則是用來定義緩存的生命週期,當Retrofit獲取到最新的數據時,會將數據及數據的配置信息封裝成Record,在本地和內存中各保存一份,Record中則保存了**@LifeCache**的值(毫秒)和當前數據請求成功的時間(毫秒)timeAtWhichWasPersisted

之後每次取緩存時,都會判斷timeAtWhichWasPersisted+@LifeCache的值是否小於當前時間(毫秒),小於則過時,則會當即清理當前緩存,並使用Retrofit從新請求最新的數據,若是EvictProvidertrue無論緩存是否過時都不會使用緩存

@EncryptKey & @Encrypt

這兩個註解的做用都是用來給緩存加密,區別在於做用域不同

@EncryptKey是做用在接口上

@EncryptKey("123")
public interface CacheProviders {

}
複製代碼

而**@Encrypt**是做用在方法上

@EncryptKey("123")
public interface CacheProviders {

	@Encrypt
	Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
}

}
複製代碼

若是須要給某個請求接口的緩存作加密的操做,則在對應的方法上加上**@Encrypt**,在存儲和獲取緩存時,RxCache就會使用**@EncryptKey的值做爲Key給緩存數據進行加解密,所以每一個Providers中的全部的方法都只能使用相同的Key**進行加解密

值得注意的是,RxCache只會給本地緩存進行加密操做,並不會給內存緩存進行加密,給本地數據加密使用的是Java自帶的CipherInputStream,解密使用的是CipherOutputStream

@Expirable

還記得咱們在構建RxCache時,有一個setMaxMBPersistenceCache方法,這個能夠設置,本地緩存的最大容量,單位爲MB,若是沒設置則默認爲100MB

這個最大容量和@Expirable又有什麼關係呢?

固然有!還記得我以前說過在每次Retrofit從新獲取最新數據時,返回數據前會將最新數據在內存緩存和本地緩存中各存一份

存儲完畢後,會檢查如今的本地緩存大小,若是如今本地緩存中存儲的全部緩存大小加起來大於或者等於setMaxMBPersistenceCache中設置的大小(默認爲100MB)的百分之95,RxCache就會作一些操做,將總的緩存大小控制在百分之70如下

作的什麼操做?

很簡單,RxCache會遍歷,構建RxCache時傳入的cacheDirectory中的全部緩存數據,一個個刪除直到總大小小於百分70,遍歷的順序不能保證,因此搞很差對你特別重要的緩存就被刪除了,這時**@Expirable就派上用場了,在方法上使用它而且給它設置爲false**(若是沒使用這個註解,則默認爲true),就能夠保證這個接口的緩存數據,在每次須要清理時都倖免於難

@Expirable(false)
	Observable<Reply<User>> getCurrentUser(Observable<User> oUser, EvictProvider evictProvider);
複製代碼

值得注意的是: 構建RxCachepersistence方法傳入的cacheDirectory,是用來存放RxCache本地緩存的文件夾,這個文件夾裏最好不要有除RxCache以外的任何數據,這樣會在每次須要遍歷清理緩存時,節省沒必要要的開銷,由於RxCache並沒檢查文件名,不論是不是本身的緩存,他都會去遍歷獲取

@SchemeMigration & @Migration

這兩個註解是用來數據遷移的,用法:

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

複製代碼

什麼叫數據遷移呢?

簡單的說就是在最新的版本中某個接口返回值類型內部發生了改變,從而獲取數據的方式發生了改變,可是存儲在本地的數據,是未改變的版本,這樣在反序列化時就可能發生錯誤,爲了規避這個風險,做者就加入了數據遷移的功能

有什麼應用場景呢?

可能上面的話,不是很好理解,舉個很是簡單的例子:

public class Mock{
	private int id;
}

複製代碼

Mock裏面有一個字段id,如今是一個整型int,能知足咱們如今的需求,可是隨着產品的迭代,發現int不夠用了

public class Mock{
	private long id;
}

複製代碼

爲了知足如今的需求,咱們使用long代替int,因爲緩存中的Mock仍是以前未改變的版本,而且未過時,在使用本地緩存時會將數據反序列化,將int變爲long,就會出現問題

數據遷移是怎麼解決上面的問題呢?

其實很是簡單,就是使用註解聲明,以前有緩存而且內部修改過的class,RxCache會把含有這些class的緩存所有清除掉

RxCache是怎麼操做的呢?

值得一提的是,在每次建立接口的動態代理時,也就是在每次調用RxCache.using(CacheProviders.class)時,會執行兩個操做,清理含有**@Migration中聲明的evictClasses**的緩存,以及遍歷本地緩存文件夾清理全部已通過期的緩存

每次清理完須要數據遷移的緩存時,會將version值最大的**@Migrationversion**值保存到本地

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 3, evictClasses = {Mock3.class}),
            @Migration(version = 2, evictClasses = {Mock2.class})
    })
interface Providers {}

複製代碼

如上面的聲明方式,它會將3保存到本地,每次調用using(),開始數據遷移時會將上次保存的version值從本地取出來,會在**@SchemeMigration中查找大於這個version值的@Migration**,取出裏面evictClasses,去重後,遍歷全部本地緩存,只要緩存數據中含有你聲明的class,就將這個緩存清除

好比evictClasses中聲明瞭Mock.class,會把以Observable< List< Mock >>,Observable< Map< String,Mock > >,Observable < Mock[] >或者Observable< Mock >做爲返回值的接口緩存所有清理掉,而後再將最大version值記錄到本地

因此每次有須要數據遷移的類時,必須在**@SchemeMigration中添加新的@Migration**,而且註解中version的值必須**+1**,這樣纔會達到數據遷移的效果

@SchemeMigration({
            @Migration(version = 1, evictClasses = {Mock.class}),
            @Migration(version = 3, evictClasses = {Mock3.class}),
            @Migration(version = 2, evictClasses = {Mock2.class}),
            @Migration(version = 4, evictClasses = {Mock2.class})
            
    })
interface Providers {}

複製代碼

如在上面的基礎上,Mock2內部又發生改變,又須要數據遷移,就要新添加個**@Migration**,version = 4(3+1),這時在調用using()時只會將version = 4@MigrationevictClasses聲明的class進行數據遷移(即清理含有這個class的緩存數據)

@Actionable

這個註解在官方介紹中說明了會使用註解處理器給使用了這個註解的Interface,自動生成一個相同類名以Actionable結尾的類文件,使用這個類的APi方便更好的執行寫操做,沒使用過,不作過多介紹

總結

到這裏RxCache的介紹就告一段落了,相信看完這篇文章後,基本使用確定是沒問題的

可是在使用中發現了一個問題,若是使用BaseResponse< T >,包裹數據的時候會出現錯誤,如issue#41issue#73

分析問題

上面說了RxCache會將Retrofit返回的數據封裝到Record對象裏,Record會判斷這個數據是那種類型,會先判斷這個數據是不是Collection(List的父類),數組仍是Map,若是都不是他會默認這個數據就是普通的對象

Record裏有三個字段分別儲存這個數據的,容器類名,容器裏值的類名,和Map的Key類名,意思爲若是數據類型爲List< String >,容器類名爲List,值類名爲String,Key類名爲空,若是數據類型爲Map< String,Integer >,容器類名爲Map,值類名爲Integer,key類名爲String

這三個字段的做用就是,在取本地緩存時可使用Gson根據字段類型恢復真實數據的類型,問題就在這,由於使用的是BaseResponse< T >包裹數據,在上面的判斷裏,他排除了這個數據是List,數組Map後它只會認定這個數據是普通的對象,這時他只會把三個字段裏中值類名保存爲BaseResponse其餘則爲空,範型的類型它並沒經過字段記錄,因此它在取的時候天然不會正確返回T的類型

解決問題

知道問題所在後,咱們如今就來解決問題,解決這個問題如今有兩個方向,一個是內部解決,一個是外部解決,外部解決的方式就能夠經過上面issue#73所提到的方式

所謂內部解決就要改這個框架的內部代碼了,問題就出在Record在數據爲普通對象的時候,他不會使用字段保存範型的類型名,因此在取本地緩存的時候就沒法正確恢復數據類型

解決的思路就是咱們必須對數據爲普通對象的時候作特殊處理,最簡單的方式就是若是數據爲對象時咱們再判斷instanceof BaseResponse,若是爲true咱們就重複作上面的判斷

即判斷BaseResponse中,T的類型是否爲List,組,Map仍是對象?

而後在用對應的字段保存對應的類型名,取本地緩存的時候就能夠用Gson按這些字段恢復正確的數據類型,可是這樣強制的判斷instanceof對於一個框架來講靈活性和擴展性會大打折扣,因此我後面寫源碼分析的時候會認真考慮下這個問題,能夠的話我會Pull RequestRxcache

公衆號

掃碼關注個人公衆號 JessYan,一塊兒學習進步,若是框架有更新,我也會在公衆號上第一時間通知你們


Hello 我叫 JessYan,若是您喜歡個人文章,能夠在如下平臺關注我

-- The end

相關文章
相關標籤/搜索