RxCache 1.0.2

RxCache 1.0.2

测试已测试
语言语言 SwiftSwift
许可证 MIT
发布最后发布2016年10月
SPM支持 SPM

Victor AlbertosRoberto Frontado 维护。



RxCache 1.0.2

Swift 语言的原始 RxCache Java 版本 实现

该库的目标很简单:SDWebImage 缓存图像一样,轻而易举地缓存您的数据模型。.

每个 Swift 应用都是客户端应用,这意味着为仅缓存数据创建和维护数据库是没有意义的。

此外,虽然您可能有一些惊人的数据库用于持久化数据,但这并不能解决真正的挑战:以灵活和简单的方式配置您的缓存需求。

Moya API 启发,RxCache 是一个用于 Swift 的响应式缓存库,它依赖于 RxSwift 将您的缓存需求转化为 枚举

每个 枚举 值都作为 RxCache 的提供者,它们都通过可观察者进行管理;这是库及其客户端之间的基本契约。

当提供一个包含由昂贵任务提供的数据的可观察者(可能是一个 HTTP 连接)时,RxCache 会确定是否需要订阅它或从先前缓存的存储中获取数据。这个决定是基于提供者的配置做出的。

因此,当您提供可观察者时,您将获得缓存的可观察者,并且下次您可以从与底层任务相关的时间成本中检索它。

安装

RxCache 可通过 CocoaPods 获得。要安装它,只需将以下行添加到您的 Podfile 中

RxCache 的使用

pod 'RxCache', '~> 0.1.4'

RxCache 的使用

准备您的数据模型

将要用于 RxCache 的数据模型需要遵守 GlossCacheableOMCacheable 协议。

使用 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 OMCacheableprotocol 后,现在可以创建一个枚举,该枚举符合 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 如何处理缓存数据。

  • LifeCache 设置数据被移除前的持续时间。如果未提供 LifeCache,则数据不会被移除,除非显式使用 EvictProviderEvictDynamicKeyEvictDynamicKeyGroup 进行移除。
  • EvictProvider 允许显式移除与 Provider 关联的所有数据。
  • EvictDynamicKey 允许显式移除特定 DynamicKey 的数据。
  • EvictDynamicKeyGroup 允许显式移除特定 DynamicKeyGroup 的数据。
  • DynamicKey 是为那些需要处理多个记录的 Provider 提供的,它们需要提供多个键,例如具有分页、排序或过滤要求的端点。要移除特定键关联的数据,请使用 EvictDynamicKey。
  • DynamicKeyGroup 是为那些需要处理分组的多个记录的 Provider 提供的,它们需要提供组织在组中的多个键,例如具有过滤和分页要求的端点。要移除特定组键关联的数据,请使用 EvictDynamicKeyGroup。

使用 CacheProviders 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]())
    }
}

用例

  • 使用经典 API RxCache 进行读写操作。
  • 使用仅限写入操作的可行动 API RxCache。

经典 API RxCache

以下用例展示了常见场景,这将有助于了解如何使用 DynamicKeyDynamicKeyGroup 类以及移除范围。

枚举提供者示例

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)

正如您可能已经注意到的,使用 DynamicKeyDynamicKeyGroupEvict 类一起使用的主要目的是在移除对象时进行多个范围操作。

枚举提供者示例声明了一种类型,其关联值接受 EvictProvider,以便在运行时具体化更具体的 EvictProvider 类型。

但这是我为了演示目的做的,你总是应该将驱逐类缩小到你真正需要的类型 -事实上,你不应该将Evict类作为关联值公开,而是要求一个Bool并在你的enum提供者中隐藏实现细节。

对于最后的例子,带有筛选器的分页列表,在生产代码中我会将作用域缩小到EvictDynamicKey,因为这样可以让我针对筛选后的模拟数据进行分页,并按其筛选条件逐个驱逐,例如由下拉刷新触发。

可操作的API RxCache

此可操作性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从一个三层之一提供数据

  • 内存层 -> 由NSCache提供。
  • 持久层 -> RxCache内部使用NSCoding来序列化和反序列化符合RxObject协议的JSON结果。
  • 加载器层(客户端库提供的观察者)

策略非常简单

  • 如果请求的数据在内存中,并且它没有过期,则从内存中获取它。
  • 否则,如果请求的数据在持久层中,并且它没有过期,则从持久层中获取它。
  • 否则,从加载器层获取它。

作者

Victor Albertos

RxCache Android 版本

  • RxCache:Android和Java的反应式缓存库。

许可证

RxCache 可在 MIT 许可下使用。有关更多信息,请参阅 LICENSE 文件。