推薦:看到如此多的 MVP+Dagger2+Retrofit+Rxjava 專案, 輕鬆拿 star, 心動了嗎? 看到身邊的朋友都已早早在專案中使用這些技術, 而你還不會, 失落嗎? MVPArms 是一個 MVP+Dagger2+Retrofit+Rxjava 可配置化快速整合框架(目前 Dagger 應用最複雜可配置化極強的整合框架), 自帶上萬字 文件 以及 一鍵生成 MVP 和 Dagger2 檔案等功能, 成熟穩定且已有上千個商業專案接入, 累計 5k+ star(全球第一 MVP 框架), 現在你只用專注於邏輯, 其他都交給 MVPArms, 快來構建自己的 MVP+Dagger2+Retrofit+Rxjava 專案吧! |
---|
原文地址: http://www.jianshu.com/p/b58ef6b0624b
前言
Retrofit
無疑是當下最火的網路請求庫,與同門師兄Okhttp
配合使用,簡直是每個專案的標配,因為Okhttp
自帶快取,所以很多人並不關心其他快取庫,但是使用過Okhttp
快取的小夥伴,肯定知道Okhttp
的快取必須配合Header使用,比較麻煩,也不夠靈活,所以現在為大家推薦一款專門為Retrifit
打造的快取庫RxCache
專案地址: RxCache Demo地址: RxCacheSample
簡介
RxCache
使用註解來為Retrofit
配置快取資訊,內部使用動態代理和Dagger
來實現,這個庫的資料相對較少,官方教程又是全英文的,這無疑給開發者增加了使用難度,其實我英文也不好,但是原始碼是通用的啊,所以我為大家從原始碼的角度來講解此庫,此庫原始碼的難點其實都在Dagger
注入上,我先為大家講解用法,後面會再寫篇文章講解原始碼,在學習Dagger
的朋友除了建議看看我的MVPArms
外,還可以看看這個RxCache
的原始碼,能學到很多東西,先給張RxCache的架構圖,讓大家嚐嚐鮮,請期待我後面的原始碼分析
使用
1.定義介面,和Retrofit
類似,介面中每個方法和Retrofit介面中的方法一一對應,每個方法的引數中必須傳入對應Retrofit介面方法的返回值(返回值必須為Observable,否則報錯),另外幾個引數DynamicKey,DynamicKeyGroup和EvictProvider不是必須的,但是如果要傳入,每個都只能傳入一個物件,否則報錯,這幾個引數的意義是初學者最困惑的,後面會分析
/**
* 此為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~
CacheProviders cacheProviders = new RxCache.Builder()
.persistence(cacheDir, new GsonSpeaker())
.using(CacheProviders.class);
複製程式碼
詳解
其實RxCache
的使用比較簡單,上面的兩步就可以輕鬆的實現快取,此庫的的特色主要集中在對快取的自定義配置,所以我來主要講講那些引數和註解是怎麼回事?
引數
Observable
此Observable的意義為需要將你想快取的Retrofit
介面作為引數傳入(返回值必須為Observable),RxCache
會在沒有快取,或者快取已經過期,或者EvictProvider為true時,通過這個Retrofit
介面重新請求最新的資料,並且將伺服器返回的結果包裝成Reply返回,返回之前會向記憶體快取和磁碟快取中各儲存一份
值得一提的是,如果需要知道返回的結果是來自哪裡(本地,記憶體還是網路),是否加密,則可以使用Observable<Reply<List<Repo>>>
作為方法的返回值,這樣RxCache
則會使用Reply包裝結果,如果沒這個需求則直接在範型中宣告結果的資料型別Observable<List<Repo>>
例外
如果構建RxCache
的時候將useExpiredDataIfLoaderNotAvailable設定成true,會在資料為空或者發生錯誤時,忽視EvictProvider為true或者快取過期的情況,繼續使用快取(前提是之前請求過有快取)
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時,在建構函式中第一個引數傳頁數,第二個引數傳使用者識別符號就可以了
理論上DynamicKey和DynamicKeyGroup根據不同的需求只用傳入其中一個即可,但是也可以兩個引數都傳,以上面的需求為例,兩個引數都傳的話,它會先取DynamicKey的Key(頁數)然後再取DynamicKeyGroup的第二個Key(使用者識別符號),加上介面名組成識別符號,來獲取和儲存資料,這樣就會忽略DynamicKeyGroup的第一個Key(頁數)
EvictProvider & EvictDynamicKey & EvictDynamicKeyGroup
這三個物件內部都儲存有一個boolean型別的欄位,其意思為是否驅逐(使用或刪除)快取,RxCache
在取到未過期的快取時,會根據這個boolean欄位,考慮是否使用這個快取,如果為true,就會重新通過Retrofit
獲取新的資料,如果為false就會使用這個快取
這三個物件有什麼關係呢?
這三個物件是相互繼承關係,繼承關係為EvictProvider < EvictDynamicKey < EvictDynamicKeyGroup,這三個物件你只能傳其中的一個,多傳一個都會報錯,按理說你不管傳那個物件都一樣,因為裡面都儲存有一個boolean欄位,根據這個欄位判斷是否使用快取
不同在哪呢?
如果有未過期的快取,並且裡面的boolean為false時,你傳這三個中的哪一個都是一樣的,但是在boolean為true時,這時就有區別了,RxCache
會在Retrofit
請求到新資料後,在boolean為true時刪除對應的快取
刪除規則是什麼呢?
還是以請求一個介面,該介面的資料會根據不同的分頁返回不同的資料,並且同一個分頁還要根據不同使用者顯示不同的資料為例
三個都不傳,RxCache
會自己new EvictProvider(false);
,這樣預設為false就不會刪除任何快取
EvictDynamicKeyGroup 只會刪除對應分頁下,對應使用者的快取
EvictDynamicKey 會刪除那個分頁下的所有快取,比如你請求的是第一頁下user1的資料,它不僅會刪除user1的資料還會刪除當前分頁下其他user2,user3...的資料
EvictProvider 會刪除當前介面下的所有快取,比如你請求的是第一頁的資料,它不僅會刪除第一頁的資料,還會把這個介面下其他分頁的資料全刪除
所以你可以根據自己的邏輯選擇傳那個物件,如果請求的這個介面沒有分頁功能,這時你不想使用快取,按理說你應該傳EvictProvider,並且在構造時傳入true,但是你如果傳EvictDynamicKey和EvictDynamicKeyGroup達到的效果也是一樣
註解
@LifeCache
@LifeCache顧名思義,則是用來定義快取的生命週期,當Retrofit
獲取到最新的資料時,會將資料及資料的配置資訊封裝成Record,在本地和記憶體中各儲存一份,Record中則儲存了**@LifeCache**的值(毫秒)和當前資料請求成功的時間(毫秒)timeAtWhichWasPersisted
以後每次取快取時,都會判斷timeAtWhichWasPersisted+@LifeCache的值是否小於當前時間(毫秒),小於則過期,則會立即清理當前快取,並使用Retrofit重新請求最新的資料,如果EvictProvider為true不管快取是否過期都不會使用快取
@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);
複製程式碼
值得注意的是: 構建RxCache
時persistence方法傳入的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值最大的**@Migration的version**值儲存到本地
@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
的@Migration中evictClasses宣告的class進行資料遷移(即清理含有這個class的快取資料)
@Actionable
這個註解在官方介紹中說明了會使用註解處理器給使用了這個註解的Interface,自動生成一個相同類名以Actionable結尾的類檔案,使用這個類的APi方便更好的執行寫操作,沒使用過,不做過多介紹
總結
到這裡RxCache
的介紹就告一段落了,相信看完這篇文章後,基本使用肯定是沒問題的
但是在使用中發現了一個問題,如果使用BaseResponse< T >,包裹資料的時候會出現錯誤,如issue#41和issue#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 Request給Rxcache
公眾號
掃碼關注我的公眾號 JessYan,一起學習進步,如果框架有更新,我也會在公眾號上第一時間通知大家
Hello 我叫 JessYan,如果您喜歡我的文章,可以在以下平臺關注我
- 個人主頁: jessyan.me
- GitHub: github.com/JessYanCodi…
- 掘金: juejin.im/user/57a9db…
- 簡書: www.jianshu.com/u/1d0c0bc63…
- 微博: weibo.com/u/178626251…
-- The end