Retrofit 風格的 RxCache 及其多種快取替換演算法

田園風光.jpg
ofollow,noindex">RxCache 是一個支援 Java 和 Android 的 Local Cache 。
之前的文章 《給 Java 和 Android 構建一個簡單的響應式Local Cache》 、 《RxCache 整合 Android 的持久層框架 greenDAO、Room》 曾詳細介紹過它。
目前,對框架增加一些 Annotation 以及 Cache 替換演算法。
一. 基於 Annotation 完成快取操作
類似 Retrofit 風格的方式,支援通過標註 Annotation 來完成快取的操作。
例如先定義一個介面,用於定義快取的各種操作。
public interface Provider { @CacheKey("user") @CacheMethod(methodType = MethodType.GET) <T> Record<T> getData(@CacheClass Class<T> clazz); @CacheKey("user") @CacheMethod(methodType = MethodType.SAVE) @CacheLifecycle(duration = 2000) void putData(@CacheValue User user); @CacheKey("user") @CacheMethod(methodType = MethodType.REMOVE) void removeUser(); @CacheKey("test") @CacheMethod(methodType = MethodType.GET, observableType = ObservableType.MAYBE) <T> Maybe<Record<T>> getMaybe(@CacheClass Class<T> clazz); }
通過 CacheProvider 建立該介面,然後可以完成各種快取操作。
public class TestCacheProvider { public static void main(String[] args) { RxCache.config(new RxCache.Builder()); RxCache rxCache = RxCache.getRxCache(); CacheProvider cacheProvider = new CacheProvider.Builder().rxCache(rxCache).build(); Provider provider = cacheProvider.create(Provider.class); User u = new User(); u.name = "tony"; u.password = "123456"; provider.putData(u); // 將u存入快取中 Record<User> record = provider.getData(User.class); // 從快取中獲取key="user"的資料 if (record!=null) { System.out.println(record.getData().name); } provider.removeUser(); // 從快取中刪除key="user"的資料 record = provider.getData(User.class); if (record==null) { System.out.println("record is null"); } User u2 = new User(); u2.name = "tony2"; u2.password = "000000"; rxCache.save("test",u2); Maybe<Record<User>> maybe = provider.getMaybe(User.class); // 從快取中獲取key="test"的資料,返回的型別為Maybe maybe.subscribe(new Consumer<Record<User>>() { @Override public void accept(Record<User> userRecord) throws Exception { User user = userRecord.getData(); if (user!=null) { System.out.println(user.name); System.out.println(user.password); } } }); } }
CacheProvider 核心是 create(),它通過動態代理來建立Provider。
public <T> T create(Class<T> clazz) { CacheProxy cacheProxy = new CacheProxy(rxCache); try { return (T) Proxy.newProxyInstance(CacheProvider.class.getClassLoader(), new Class[]{clazz}, cacheProxy); } catch (Exception e) { e.printStackTrace(); } return null; }
其中,CacheProxy 實現了 InvocationHandler 介面,是建立代理類的呼叫處理器。
package com.safframework.rxcache.proxy; import com.safframework.rxcache.RxCache; import com.safframework.rxcache.proxy.annotation.*; import java.lang.annotation.Annotation; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; /** * Created by tony on 2018/10/30. */ public class CacheProxy implements InvocationHandler { RxCache rxCache; public CacheProxy(RxCache rxCache) { this.rxCache = rxCache; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { CacheMethod cacheMethod = method.getAnnotation(CacheMethod.class); CacheKey cacheKey = method.getAnnotation(CacheKey.class); CacheLifecycle cacheLifecycle = method.getAnnotation(CacheLifecycle.class); Annotation[][] allParamsAnnotations = method.getParameterAnnotations(); Class cacheClazz = null; Object cacheValue = null; if (allParamsAnnotations != null) { for (int i = 0; i < allParamsAnnotations.length; i++) { Annotation[] paramAnnotations = allParamsAnnotations[i]; if (paramAnnotations != null) { for (Annotation annotation : paramAnnotations) { if (annotation instanceof CacheClass) { cacheClazz = (Class) args[i]; } if (annotation instanceof CacheValue) { cacheValue = args[i]; } } } } } if (cacheMethod!=null) { MethodType methodType = cacheMethod.methodType(); long duration = -1; if (cacheLifecycle != null) { duration = cacheLifecycle.duration(); } if (methodType == MethodType.GET) { ObservableType observableType = cacheMethod.observableType(); if (observableType==ObservableType.NOUSE) { returnrxCache.get(cacheKey.value(),cacheClazz); } else if (observableType == ObservableType.OBSERVABLE){ returnrxCache.load2Observable(cacheKey.value(),cacheClazz); } else if (observableType==ObservableType.FLOWABLE) { returnrxCache.load2Flowable(cacheKey.value(),cacheClazz); } else if (observableType==ObservableType.SINGLE) { returnrxCache.load2Single(cacheKey.value(),cacheClazz); } else if (observableType==ObservableType.MAYBE) { returnrxCache.load2Maybe(cacheKey.value(),cacheClazz); } } else if (methodType == MethodType.SAVE) { rxCache.save(cacheKey.value(),cacheValue,duration); } else if (methodType == MethodType.REMOVE) { rxCache.remove(cacheKey.value()); } } return null; } }
CacheProxy 的 invoke() 方法先獲取 Method 所使用的 Annotation,包括CacheMethod、CacheKey、CacheLifecycle。
其中,CacheMethod 是最核心的 Annotation,它取決於 rxCache 使用哪個方法。CacheMethod 支援的方法型別包括:獲取、儲存、刪除快取。當 CacheMethod 的 methodType 是 GET 型別,則可能會返回 RxJava 的各種 Observable 型別,或者還是返回所儲存的物件型別。
CacheKey 是任何方法都需要使用的 Annotation。CacheLifecycle 只有儲存快取時才會使用。
二. 支援多種快取替換演算法
RxCache 包含了兩級快取: Memory 和 Persistence 。
Memory 的預設實現 FIFOMemoryImpl、LRUMemoryImpl、LFUMemoryImpl 分別使用 FIFO、LRU、LFU 演算法來快取資料。
2.1 FIFO
通過使用 LinkedList 存放快取的 keys,ConcurrentHashMap 存放快取的資料,就可以實現 FIFO。
2.2 LRU
LRU是Least Recently Used的縮寫,即最近最少使用,常用於頁面置換演算法,是為虛擬頁式儲存管理服務的。
使用 ConcurrentHashMap 和 ConcurrentLinkedQueue 實現該演算法。如果某個資料已經存放在快取中,則從 queue 中刪除並新增到 queue 的第一個位置。如果快取已滿,則從 queue 中刪除最後面的資料。並把新的資料新增到快取。
public class LRUCache<K,V> { private Map<K,V> cache = null; private AbstractQueue<K> queue = null; private int size = 0; public LRUCache() { this(Constant.DEFAULT_CACHE_SIZE); } public LRUCache(int size) { this.size = size; cache = new ConcurrentHashMap<K,V>(size); queue = new ConcurrentLinkedQueue<K>(); } public boolean containsKey(K key) { return cache.containsKey(key); } public V get(K key) { //Recently accessed, hence move it to the tail queue.remove(key); queue.add(key); return cache.get(key); } public V getSilent(K key) { return cache.get(key); } public void put(K key, V value) { //ConcurrentHashMap doesn't allow null key or values if(key == null || value == null) throw new RxCacheException("key is null or value is null"); if(cache.containsKey(key)) { queue.remove(key); } if(queue.size() >= size) { K lruKey = queue.poll(); if(lruKey != null) { cache.remove(lruKey); } } queue.add(key); cache.put(key,value); } /** * 獲取最近最少使用的值 * @return */ public V getLeastRecentlyUsed() { K remove = queue.remove(); queue.add(remove); return cache.get(remove); } public void remove(K key) { cache.remove(key); queue.remove(key); } public void clear() { cache.clear(); queue.clear(); } ...... }
2.3 LFU
LFU是Least Frequently Used的縮寫,即最近最不常用使用。
看上去跟 LRU 類似,其實它們並不相同。LRU 是淘汰最長時間未被使用的資料,而 LFU 是淘汰一定時期內被訪問次數最少的資料。
LFU 會記錄資料在一定時間內的使用次數。稍顯複雜感興趣的可以閱讀 RxCache 中相關的原始碼。
三. 總結
RxCache 大體已經完成,初步可以使用。
RxCache github 地址: https://github.com/fengzhizi715/RxCache
Android 版本的 RxCache github 地址: https://github.com/fengzhizi715/RxCache4a
對於 Android ,除了支援常見的持久層框架之外,還支援 RxCache 轉換成 LiveData。如果想要跟 Retrofit 結合,可以通過 RxCache 的 transform 策略。
對於Java 後端,RxCache 只是一個本地快取,不適合存放大型的資料。但是其內建的 Memory 層包含了多種快取替換演算法,不用內建的 Memory 還可以使用 Guava Cache、Caffeine 。