测试已测试 | ✓ |
语言语言 | SwiftSwift |
许可证 | MIT |
发布最后发布 | 2016年10月 |
SPM支持 SPM | ✗ |
由 Victor Albertos 和 Roberto Frontado 维护。
该库的目标很简单:像 SDWebImage 缓存图像一样,轻而易举地缓存您的数据模型。.
每个 Swift 应用都是客户端应用,这意味着为仅缓存数据创建和维护数据库是没有意义的。
此外,虽然您可能有一些惊人的数据库用于持久化数据,但这并不能解决真正的挑战:以灵活和简单的方式配置您的缓存需求。
受 Moya API 启发,RxCache 是一个用于 Swift 的响应式缓存库,它依赖于 RxSwift 将您的缓存需求转化为 枚举
。
每个 枚举
值都作为 RxCache 的提供者,它们都通过可观察者进行管理;这是库及其客户端之间的基本契约。
当提供一个包含由昂贵任务提供的数据的可观察者(可能是一个 HTTP 连接)时,RxCache 会确定是否需要订阅它或从先前缓存的存储中获取数据。这个决定是基于提供者的配置做出的。
因此,当您提供可观察者时,您将获得缓存的可观察者,并且下次您可以从与底层任务相关的时间成本中检索它。
安装
RxCache 的使用
pod 'RxCache', '~> 0.1.4'
将要用于 RxCache 的数据模型需要遵守 GlossCacheable 或 OMCacheable 协议。
使用 Gloss
import Gloss
import RxCache
struct Person: Glossy, GlossCacheable {
let name: String
init?(json: JSON) {
guard let name: String = "name" <~~ json else { return nil }
self.name = name
}
func toJSON() -> JSON? {
return jsonify([
"name" ~~> self.name]
)
}
}
使用 ObjectMapper
import ObjectMapper
import RxCache
class Person: Mappable, OMCacheable {
var name: String?
required init?(JSON: [String : AnyObject]) {
mapping(Map(mappingType: .FromJSON, JSONDictionary: JSON))
}
func mapping(map: Map) {
name <- map["name"]
}
required init?(_ map: Map) { }
}
如果您想添加可以将 JSON 转换为和从 JSON 转换的其他映射库,请提出请求或创建 PR :-)
当您的模型符合 GlossCacheable
OMCacheable
或 protocol
后,现在可以创建一个枚举,该枚举符合 Provider 协议,包含所需的所有值以创建缓存提供者。
enum CacheProviders : Provider {
case GetMocks()
case GetMocksWith5MinutesLifeTime()
case GetMocksEvictProvider(evict : Bool)
case GetMocksPaginate(page : Int)
case GetMocksPaginateEvictingPerPage(page : Int, evictByPage: Bool)
case GetMocksPaginateWithFiltersEvictingPerFilter(filter: String, page : String, evictByPage: Bool)
var lifeCache: LifeCache? {
switch self {
case GetMocksWith5MinutesLifeTime:
return LifeCache(duration: 5, timeUnit: LifeCache.TimeUnit.Minutes)
default:
return nil
}
}
var dynamicKey: DynamicKey? {
switch self {
case let GetMocksPaginate(page):
return DynamicKey(dynamicKey: String(page))
case let GetMocksPaginateEvictingPerPage(page, _):
return DynamicKey(dynamicKey: String(page))
default:
return nil
}
}
var dynamicKeyGroup: DynamicKeyGroup? {
switch self {
case let GetMocksPaginateWithFiltersEvictingPerFilter(filter, page, _):
return DynamicKeyGroup(dynamicKey: filter, group: page)
default:
return nil
}
}
var evict: EvictProvider? {
switch self {
case let GetMocksEvictProvider(evict):
return EvictProvider(evict: evict)
case let GetMocksPaginateEvictingPerPage(_, evictByPage):
return EvictDynamicKey(evict: evictByPage)
case let GetMocksPaginateWithFiltersEvictingPerFilter(_, _, evictByPage):
return EvictDynamicKey(evict: evictByPage)
default:
return nil
}
}
}
RxCache 提供了一套类,以指示 Provider 如何处理缓存数据。
enum
现在您可以通过调用 RxCache.Providers.cache()
静态方法来调用您的枚举提供者。
将数据提供者在一系列仓库类中组织是一个非常好的做法,其中 RxCache 和 Moya 协同工作。实际上,RxCache 是与 Moya 非常匹配的工具,用于创建指向端点的自动管理缓存数据的仓库。
这将是 MockRepository
class MockRepository {
private let providers: RxCache
init() {
providers = RxCache.Providers
}
func getMocks(update: Bool) -> Observable<[Mock]> {
let provider : Provider = CacheProviders.GetMocksEvictProvider(evict: update)
return providers.cache(getExpensiveMocks(), provider: provider)
}
func getMocksPaginate(page: Int, update: Bool) -> Observable<[Mock]> {
let provider : Provider = CacheProviders.GetMocksPaginateEvictingPerPage(page: page, evictByPage: update)
return providers.cache(getExpensiveMocks(), provider: provider)
}
func getMocksWithFiltersPaginate(filter: String, page: Int, update: Bool) -> Observable<[Mock]> {
let provider : Provider = CacheProviders.GetMocksPaginateWithFiltersEvictingPerFilter(filter: filter, page: page, evictByPage: update)
return providers.cache(getExpensiveMocks(), provider: provider)
}
//In a real use case, here is when you build your observable with the expensive operation.
//Or if you are making http calls you can use Moya-RxSwift extensions to get it out of the box.
private func getExpensiveMocks() -> Observable<[Mock]> {
return Observable.just([Mock]())
}
}
以下用例展示了常见场景,这将有助于了解如何使用 DynamicKey
和 DynamicKeyGroup
类以及移除范围。
enum CacheProviders : Provider {
// Mock List
case GetMocks() // Mock List without evicting
case GetMocksEvictProvider(evictProvider : EvictProvider) // Mock List evicting
// Mock List Filtering
case GetMocksFiltered(filter: String) // Mock List filtering without evicting
case GetMocksFilteredEvict(filter: String, evictProvider : EvictProvider) // Mock List filtering evicting
// Mock List Paginated with filters
case GetMocksFilteredPaginate(filterAndPage: String) // Mock List paginated with filters without evicting
case GetMocksFilteredPaginateEvict(filter: String, page: Int, evictProvider : EvictProvider) // Mock List paginated with filters evicting
var lifeCache: LifeCache? {
return nil
}
var dynamicKey: DynamicKey? {
switch self {
case let GetMocksFiltered(filter):
return DynamicKey(dynamicKey: filter)
case let GetMocksFilteredEvict(filter, _):
return DynamicKey(dynamicKey: filter)
case let GetMocksFilteredPaginate(filterAndPage):
return DynamicKey(dynamicKey: filterAndPage)
default:
return nil
}
}
var dynamicKeyGroup: DynamicKeyGroup? {
switch self {
case let GetMocksFilteredPaginateEvict(filter, page, _):
return DynamicKeyGroup(dynamicKey: filter, group: String(page))
default:
return nil
}
}
var evict: EvictProvider? {
switch self {
case let GetMocksFilteredEvict(_, evictProvider):
return evictProvider
case let GetMocksFilteredPaginateEvict(_, _, evictProvider):
return evictProvider
default:
return nil
}
}
}
//Hit observable evicting all mocks
let provider : Provider = CacheProviders.GetMocksEvictProvider(evictProvider: EvictProvider(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//This lines return an error observable: "EvictDynamicKey was provided but not was provided any DynamicKey"
let provider : Provider = CacheProviders.GetMocksEvictProvider(evictProvider: EvictDynamicKey(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting all mocks using EvictProvider
let provider : Provider = CacheProviders.GetMocksFilteredEvict(filter: "actives", evictProvider: EvictProvider(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting mocks of one filter using EvictDynamicKey
let provider : Provider = CacheProviders.GetMocksFilteredEvict(filter: "actives", evictProvider: EvictDynamicKey(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//This lines return an error observable: "EvictDynamicKeyGroup was provided but not was provided any Group"
let provider : Provider = CacheProviders.GetMocksFilteredEvict(filter: "actives", evictProvider: EvictDynamicKeyGroup(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting all mocks using EvictProvider
let provider : Provider = CacheProviders.GetMocksFilteredPaginateEvict(filter: "actives", page: 1, evictProvider: EvictProvider(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting all mocks pages of one filter using EvictDynamicKey
let provider : Provider = CacheProviders.GetMocksFilteredPaginateEvict(filter: "actives", page: 1, evictProvider: EvictDynamicKey(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
//Hit observable evicting one page mocks of one filter using EvictDynamicKeyGroup
let provider : Provider = CacheProviders.GetMocksFilteredPaginateEvict(filter: "actives", page: 1, evictProvider: EvictDynamicKeyGroup(evict: true))
providers.cache(getExpensiveMocks(), provider: provider)
正如您可能已经注意到的,使用 DynamicKey
或 DynamicKeyGroup
与 Evict
类一起使用的主要目的是在移除对象时进行多个范围操作。
枚举提供者示例声明了一种类型,其关联值接受 EvictProvider
,以便在运行时具体化更具体的 EvictProvider
类型。
但这是我为了演示目的做的,你总是应该将驱逐类缩小到你真正需要的类型 -事实上,你不应该将Evict
类作为关联值公开,而是要求一个Bool
并在你的enum
提供者中隐藏实现细节。
对于最后的例子,带有筛选器的分页列表,在生产代码中我会将作用域缩小到EvictDynamicKey
,因为这样可以让我针对筛选后的模拟数据进行分页,并按其筛选条件逐个驱逐,例如由下拉刷新触发。
此可操作性API提供了一个使用提供者执行写操作的简单方法。尽管也可以使用经典API来完成写操作,但它的实现更复杂,更容易出错。实际上,Actions类是一个包装经典API的包装器,它处理驱逐作用域和列表。
一些操作示例
let provider = RxProvidersMock.GetMocksEvictCache(evict: false)
Actions<Mock>.with(provider)
.addFirst(Mock())
.addLast(Mock())
// Add a new mock at 5th position
.add({ (position, count) -> Bool in position == 5 }, candidate: Mock())
.evictFirst()
//Evict first element if the cache has already 300 records
.evictFirst { (count) -> Bool in count > 300 }
.evictLast()
//Evict last element if the cache has already 300 records
.evictLast { (count) -> Bool in count > 300 }
//Evict all inactive elements
.evictIterable { (position, count, mock) -> Bool in mock.active == false }
.evictAll()
//Update the mock with id 5
.update({ mock -> Bool in mock.id == 5 },
replace: { mock in
mock.active = true
return mock
})
//Update all inactive mocks
.update({ mock -> Bool in mock.active == false },
replace: { mock in
mock.active = true
return mock
})
.toObservable()
.subscribe()
前面的每个操作都将在组合的观察者收到订阅后执行。这样,底层提供者缓存将轻松修改其元素。
默认情况下,RxCache将限制设置为300兆字节,但你可以通过调用RxCache.Providers.maxMBPersistenceCache
属性来更改此值。
RxCache.Providers.maxMBPersistenceCache = 100
这个限制确保在动态键值会动态变化的提供者(例如基于GPS位置的筛选器或由后端解决方案提供的动态筛选器)的情况下,磁盘不会无限增长。
当达到此限制时,RxCache将无法在磁盘上持续新数据。这就是为什么RxCache有一个自动过程,在分配给持久层阈值内存即将达到时,将删除任何记录,即使记录的寿命尚未完成。
但通过从Provider
协议中实现expirable()
方法并返回false作为值,使用此提供者持久化的对象将被排除在此过程之外。
enum CacheProviders : Provider {
case GetNotExpirableMocks()
public func expirable() -> Bool {
switch self {
case GetNotExpirableMocks():
return false
default:
return true
}
}
}
默认情况下,如果缓存的已过期且由observable
加载数据返回null,RxCache将返回一个observable
错误,从而阻止为此提供服务已被标记为驱逐的数据。
你可以修改此行为,通过将useExpiredDataIfLoaderNotAvailable
的值设置为true,允许RxCache在加载器返回nil值时提供被驱逐的数据。
RxCache.Providers.useExpiredDataIfLoaderNotAvailable = true
RxCache从一个三层之一提供数据
策略非常简单
Victor Albertos
RxCache 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。