Carlos 0.9.1

Carlos 0.9.1

测试已测试
Lang语言 SwiftSwift
许可 MIT
发布最后发布2016年12月
SwiftSwift版本3.0
SPM支持SPM

Vittorio MonacoCesar Vargas CasasecaIvan Lisovyi维护。



Carlos 0.9.1

  • Vittorio Monaco

Carlos

Build Status

一个简单但灵活的缓存,使用Swift编写,用于iOS 8+WatchOS 2应用。

本读me内容

什么是Carlos?

Carlos是一组类和函数,用于在您的应用程序中实现自定义、灵活且强大的缓存层。

使用函数式编程词汇,Carlos构成了一个叠加缓存系统。您可以在这里或在这个视频中找到对其如何实现的最佳解释,感谢@bkase提供幻灯片。

默认情况下,Carlos附带了内存缓存、磁盘缓存、简单的网络检索器和NSUserDefaults缓存(磁盘缓存受HanekeSwift的启发)。

使用Carlos,您可以

  • 创建级别和检索器以满足您的需求
  • 组合级别
  • 取消挂起的请求
  • 转换每个级别将获取的键,或每个级别将输出的值(这意味着您可以根据以后如何使用它来实现每个级别的独立实现)。Carlos已经提供了一些常见的值转换器
  • 对缓存级别应用后处理步骤,例如清理输出或调整图像大小
  • 后处理步骤值转换也可以根据获取值时使用的键进行条件应用
  • 响应应用中的内存压力事件
  • 当较低级别的一个在键上获取值时自动填充上级,因此下次第一级将已经将其缓存在其中
  • 根据布尔条件启用或禁用所组成缓存的具体级别
  • 轻松聚合请求,以便您无需关心是否必须由昂贵的缓存级别执行5个具有相同键的请求,而这5个请求中的任何一个尚未完成。Carlos可以为您处理这些
  • 批量获取请求进行处理,只会在所有请求都完成后通知您
  • 为复杂场景设置多个通道,在这些场景中,根据某些键或条件,应使用不同的缓存
  • 限制缓存应处理的并发请求数量
  • 调度您缓存的全部操作到特定的GCD队列
  • 拥有类型安全的复杂缓存,如果不满足类型要求,则代码甚至无法编译

安装

子模块

如果您不使用CocoaPods,仍然可以将Carlos作为子模块添加,将Carlos.xcodeproj拖放到您的项目中,并在您的目标中嵌入Carlos.framework

  • Carlos.xcodeproj拖放到您的项目中
  • 选择您的应用程序目标
  • Embedded binaries部分点击+按钮
  • 添加Carlos.framework(对于WatchOS 2应用程序,请使用CarlosWatch.framework

手动

您可以直接将所需文件拖放到您的项目中,但请注意,这样您将无法自动获得最新的所有Carlos功能(例如,包括新操作的新文件)。

这些文件包含在Carlos文件夹中。

如果您想将Carlos集成到WatchOS 2应用程序中,请不要包含文件MemoryWarning.swift

Playground

我们提供了一个小型的Xcode playground与项目一起发布,这样您就可以快速看到Carlos的工作方式,并与您自定义的层、层组合以及请求池化、限制等多种配置进行实验。

要使用我们的Playground,请按照以下步骤操作

  • 打开Xcode项目Carlos.xcodeproj
  • 选择Carlos框架目标,以及一个64位平台(例如,iPhone 6
  • 使用⌘+B构建目标
  • 点击Playground文件Carlos.playground
  • 编写您的代码

要求

  • iOS 8.0+
  • WatchOS 2+
  • Xcode 8.0+

用法

要运行示例项目,请克隆存储库。

用法示例

let cache = MemoryCacheLevel<String, NSData>().compose(DiskCacheLevel())

此行将生成一个缓存,它以 String 作为键,返回 NSData 值。在此缓存上为给定键设置值时,将在两个级别上设置它。在缓存上获取给定键的值时,将首先尝试从内存级别获取它,如果找不到一个,将询问磁盘级别。如果两个级别都没有值,则请求将失败。如果磁盘级别可以获取一个值,这将同时也在内存级别上设置,以便下次获取更快。

Carlos 包含一个 CacheProvider 类,以便标准缓存可以轻松访问。

  • 使用 CacheProvider.dataCache() 创建一个以 URL 作为键并返回 NSData 值的缓存
  • 使用 CacheProvider.imageCache() 创建一个以 URL 作为键并返回 UIImage 值的缓存
  • 使用 CacheProvider.JSONCache() 创建一个以 URL 作为键并返回 AnyObject 值的缓存(应根据您的应用程序安全地将它们转换成数组或字典)

上述方法始终创建新的实例(因此,如果您两次调用 CacheProvider.imageCache(),则不会返回相同的实例,即使因为使用了相同的磁盘文件夹,磁盘级别将有效共享,但这是一种副作用,不应依赖于它),您应该在您的应用程序层中负责保留结果。如果您始终想要相同的实例,您可以使用以下访问器

  • CacheProvider.sharedDataCache 以检索共享的数据缓存实例
  • CacheProvider.sharedImageCache 以检索共享的图像缓存实例
  • CacheProvider.sharedJSONCache 以检索共享的 JSON 缓存实例

创建请求

要从缓存获取值,请使用 get 方法。

cache.get("key")
  .onSuccess { value in
      print("I found \(value)!")
  }
  .onFailure { error in
      print("An error occurred :( \(error)")
  }

您还可以将请求存储在某个位置,然后向它附加多个 onSuccessonFailure 监听器。

let request = cache.get("key")

request.onSuccess { value in
    print("I found \(value)!")
}

[... somewhere else]


request.onSuccess { value in
    print("I can read \(value), too!")
}

请求也可以使用 cancel() 方法取消,并且您可以通过在给定的请求上调用 onCancel 来通知此事件。

let request = cache.get(key).onCancel {
    print(""Looks like somebody canceled this request!")
}

[... somewhere else]
request.cancel()

当缓存请求成功时,会调用所有监听器。 并且 即使在请求已经完成工作之后添加监听器,您仍然会收到回调。 在任何给定时间,请求仅处于 正在执行成功失败已取消 中的一个状态,并且 不能多次失败、成功或取消

如果您只是对请求完成时的任何情况都感兴趣,无论是成功、失败还是取消,您可以使用 onCompletion

request.onCompletion { result in
   switch result {
   case .success(let value):
     print("Request succeeded with value \(value)")
   case .error(let error):
     print("Request failed with code \(error)")
   case .cancelled:
     print("This request has been canceled")
   }

   print("Nevertheless the request completed")
}

尽管如此,此缓存并不很有用。它永远不会 主动 获取值,只是将其存储起来以供以后使用。让我们尝试使其更吸引人

let cache = MemoryCacheLevel()
              .compose(DiskCacheLevel())
              .compose(NetworkFetcher())

这将创建一个缓存级别,它以 URL 作为键并存储 NSData 值(类型从 NetworkFetcher 对键和值的强制要求 URLNSData 推断而出,而 MemoryCacheLevelDiskCacheLevel 则更加灵活,如后续所述)。

键转换

键转换旨在使将缓存级别连接到您正在构建的任何缓存成为可能。

让我们看看它是如何工作的

// Define your custom ErrorType values
enum URLTransformationError: Error {
    case invalidURLString
}

let transformedCache = NetworkFetcher().transformKeys(OneWayTransformationBox(transform: { Future(value: URL(string: $0), error: URLTransformationError.invalidURLString) }))

上面的行表示,所有进入 NetworkFetcher 级别的键都必须首先转换成 URL 值。现在我们可以将此缓存连接到一个之前定义的以 String 作为键的缓存级别。

let cache = MemoryCacheLevel<String, NSData>().compose(transformedCache)

如果这看起来并不太安全(人们总可以通过作为键传递字符串垃圾,而它不会神奇地翻译成 URL,从而导致 NetworkFetcher 运行出错并以静默失败),我们仍然可以使用特定于域的结构作为键,假设它包含 StringURL

struct Image {
  let identifier: String
  let URL: Foundation.URL
}

let imageToString = OneWayTransformationBox(transform: { (image: Image) -> Future<String> in
    Future(image.identifier)
})

let imageToURL = OneWayTransformationBox(transform: { (image: Image) -> Future<URL> in
    Future(image.URL)
})

let memoryLevel = MemoryCacheLevel<String, NSData>().transformKeys(imageToString)
let diskLevel = DiskCacheLevel<String, NSData>().transformKeys(imageToString)
let networkLevel = NetworkFetcher().transformKeys(imageToURL)

let cache = memoryLevel.compose(diskLevel).compose(networkLevel)

现在我们可以安全地进行如下请求

let image = Image(identifier: "550e8400-e29b-41d4-a716-446655440000", URL: URL(string: "http://goo.gl/KcGz8T")!)

cache.get(image).onSuccess { value in
  print("Found \(value)!")
}

Carlos 0.5 起您还可以将条件应用于用于键转换的 OneWayTransformers。只需在转换器上调用 conditioned 函数并传递您的条件。条件也可以是异步的,并必须返回 Future<Bool>,有机会在转换失败时返回一个特定的错误。

let transformer = OneWayTransformationBox<String, URL>(transform: { key in
  Future(value: URL(string: key), error: MyError.stringIsNotURL)
}).conditioned { key in
  Future(key.rangeOfString("http") != nil)
}

let cache = CacheProvider.imageCache().transformKeys(transformer)

但不止这些。

如果我们磁盘缓存仅存储 Data,但我们想方便地将内存缓存中的实例存储为 UIImage 呢?

值转换

值转换器让您有一个缓存,例如存储 Data 并将其转换为存储 UIImage 值的缓存。让我们看看如何实现它

let dataTransformer = TwoWayTransformationBox(transform: { (image: UIImage) -> Future<Data> in
    Future(UIImagePNGRepresentation(image))
}, inverseTransform: { (data: Data) -> Future<UIImage> in
    Future(UIImage(data: data)!)
})

let memoryLevel = MemoryCacheLevel<String, UIImage>().transformKeys(imageToString).transformValues(dataTransformer)

内存级别现在可以替换我们之前的一个,区别在于它将内部存储 UIImage 值!

请注意,与键转换一样,如果您的转换闭包失败(无论是正向转换还是逆向转换),缓存级别将跳过,就像检索失败一样。对于 set 调用也适用相同考虑。

Carlos 内置一些值转换器,例如:

  • JSONTransformerNSData 实例序列化为 JSON
  • ImageTransformerNSData 实例序列化为 UIImage 值(在 Mac OS X 框架中不可用)
  • StringTransformerNSData 实例序列化为有给定编码的 String
  • 一些 Cocoa 类的扩展(如 DateFormatterNumberFormatterMKDistanceFormatter),以便您可以根据需要使用自定义实例。

Carlos 0.4 开始,您可以只需使用 OneWayTransformer(与必要的 TwoWayTransformer 相比,用于常规 CacheLevel 实例)将来自 Fetcher 实例的值进行转换。这是因为 Fetcher 协议不要求 set)。这意味着您可以轻松地将从互联网获取的 JSON 的 HttpClient 作为 Fetcher 连接,并转换其输出到模型对象(例如 a struct)成为复杂的缓存管道,而无需创建仅为满足 TwoWayTransformer 协议要求的虚拟反向转换。

Carlos 0.5 开始,所有转换器都原生支持异步计算,因此在您的自定义转换器中,昂贵的转换不会阻塞其他操作。实际上,内置的 ImageTransformer 在后台队列上处理图像转换。

Carlos 0.5 开始,您还可以将条件应用于用于值转换的 TwoWayTransformers。只需在转换器上调用 conditioned 函数并传递您的条件(一个用于正向转换,一个用于逆向转换)。条件也可以是异步的,并必须返回 Future<Bool>,有机会在转换失败时返回一个特定的错误。

let transformer = JSONTransformer().conditioned({ input in
  Future(myCondition)
}, inverseCondition: { input in
  Future(myCondition)
})

let cache = CacheProvider.dataCache().transformValues(transformer)

输出后处理

在某些情况下,您的缓存级别可能返回正确的值,但格式不理想。例如,您想要对 Cache 返回的整个输出进行清理,而不管确切的层是什么。

对于这些情况,从Carlos 0.4中引入的postProcess函数可能有所帮助。该函数作为CacheLevel协议的协议扩展提供。

postProcess函数接收一个CacheLevel和一个参数为OneWayTransformerTypeIn == TypeOut的函数,输出一个封装了后处理步骤的装饰性BasicCache

// Let's create a simple "to uppercase" transformer
let transformer = OneWayTransformationBox<NSString, String>(transform: { Future($0.uppercased() as String) })

// Our memory cache
let memoryCache = MemoryCacheLevel<String, NSString>()

// Our decorated cache
let transformedCache = memoryCache.postProcess(transformer)

// Lowercase value set on the memory layer
memoryCache.set("test String", forKey: "key")

// We get the lowercase value from the undecorated memory layer
memoryCache.get("key").onSuccess { value in
  let x = value
}

// We get the uppercase value from the decorated cache, though
transformedCache.get("key").onSuccess { value in
  let x = value
}

Carlos 0.5版本起,您还可以将条件应用于用于后处理转换的OneWayTransformers。只需在转换器上调用conditioned函数并传递您的条什。此条件也可以是异步的,必须返回一个Future,有机会返回特定错误以处理转换失败。请注意,条件实际上将将缓存的输出作为输入,而不是用于获取此值的键!如果您要根据键应用条件,请使用conditionedPostProcess,但请注意,这还不支持使用OneWayTransformer实例。

let processer = OneWayTransformationBox<NSData, NSData>(transform: { value in
      Future(value: String(data: value as Data, encoding: .utf8)?.uppercased().data(using: .utf8) as NSData?, error: FetchError.conditionNotSatisfied)
    }).conditioned { value in
      Future(value.length < 1000)
    }

let cache = CacheProvider.dataCache().postProcess(processer)

条件输出后处理

在简单输出后处理的情况下扩展,您还可以根据获取值时使用的键应用条件转换。

对于这些情况,从Carlos 0.6引入的conditionedPostProcess函数可能有所帮助。该函数作为CacheLevel协议的协议扩展提供。

conditionedPostProcess函数接收一个CacheLevel和一个符合ConditionedOneWayTransformer的装饰转换器作为参数,并输出一个封装了条件后处理步骤的装饰性CacheLevel

// Our memory cache
let memoryCache = MemoryCacheLevel<String, NSString>()

// Our decorated cache
let transformedCache = memoryCache.conditionedPostProcess(ConditionedOneWayTransformationBox(conditionalTransformClosure: { (key, value) in
    if key == "some sentinel value" {
        return Future(value.uppercased())
    } else {
        return Future(value)
    }
})

// Lowercase value set on the memory layer
memoryCache.set("test String", forKey: "some sentinel value")

// We get the lowercase value from the undecorated memory layer
memoryCache.get("some sentinel value").onSuccess { value in
  let x = value
}

// We get the uppercase value from the decorated cache, though
transformedCache.get("some sentinel value").onSuccess { value in
  let x = value
}

条件值转换

在简单值转换的情况下扩展,您还可以根据获取或设置值时使用的键应用条件转换。

对于这些情况,从Carlos 0.6引入的conditionedValueTransformation函数可能有所帮助。该函数作为CacheLevel协议的协议扩展提供。

conditionedValueTransformation函数接收一个CacheLevel和一个符合ConditionedTwoWayTransformer的装饰转换器作为参数,并输出一个具有修改后的OutputType(与正常值转换情况中的转换器TypeOut相等)的装饰性CacheLevel,其中封装了条件值转换步骤。

// Our memory cache
let memoryCache = MemoryCacheLevel<String, NSString>()

// Our decorated cache
let transformedCache = memoryCache.conditionedValueTransformation(ConditionedTwoWayTransformationBox(conditionalTransformClosure: { (key, value) in
    if key == "some sentinel value" {
        return Future(1)
    } else {
        return Future(0)
    }
}, conditionalInverseTransformClosure: { (key, value) in
    if key > 0 {
        return Future("Positive")
    } else {
        return Future("Null or negative")
    }
})

// Value set on the memory layer
memoryCache.set("test String", forKey: "some sentinel value")

// We get the same value from the undecorated memory layer
memoryCache.get("some sentinel value").onSuccess { value in
  let x = value
}

// We get 1 from the decorated cache, though
transformedCache.get("some sentinel value").onSuccess { value in
  let x = value
}

// We set "Positive" on the decorated cache
transformedCache.set(5, forKey: "test")

组合转换器

Carlos 0.4起,可以组合多个OneWayTransformer对象。这样,可以创建多个转换模块来构建一个小型库,然后根据应用方便组合它们。

可以使用与正常CacheLevel相同的方谜来组合转换器:使用compose协议扩展。

let firstTransformer = ImageTransformer() // NSData -> UIImage
let secondTransformer = ImageTransformer().invert() // Trivial UIImage -> NSData

let identityTransformer = firstTransformer.compose(secondTransformer)

相同的做法可以应用于TwoWayTransformer对象(顺便说一就要是,它已经是OneWayTransformer)。

Carlos将默认提供许多转换模块。

请求池

当您有一个正常运行的缓存,但某些级别(比如网络请求器或数据库请求器)成本较高时,您可能希望以某种方式将请求池化,使得对于同一个键的多个请求在其中一个请求完成之前合并,以便当一个请求完成时,所有其他请求也自动完成,无需实际多次执行昂贵的操作。

这种功能由Carlos提供。

let cache = (memoryLevel.compose(diskLevel).compose(networkLevel)).pooled()

请注意,键必须遵循Hashable协议,才能使pooled函数正常工作。

extension Image: Hashable {
  var hashValue: Int {
    return identifier.hashValue
  }
}

extension Image: Equatable {}

func ==(lhs: Image, rhs: Image) -> Bool {
  return lhs.identifier == rhs.identifier && lhs.URL == rhs.URL
}

现在我们可以为同一Image值执行多个获取操作,并确保仅启动一个网络请求。

批量获取请求

Carlos 0.7起,您可以通过batchGetSome将键列表传递给您的CacheLevel。这返回一个Future,当指定的所有键的请求都完成后将成功,但不一定是成功。尽管如此,您只需在成功回调和错误回调中获取成功值。

Carlos 0.9起,您可以通过allBatch将您的CacheLevel转换为接受键列表的形式。在这样一个CacheLevel上调用get将返回一个只有在所有指定的键的请求都成功时才会成功的Future,而在任何一个指定的键的请求失败时立即失败。如果您取消由这个CacheLevel返回的Future,所有挂起的请求也将被取消。

使用示例

let cache = MemoryCacheLevel<String, Int>()

for iter in 0..<99 {
  cache.set(iter, forKey: "key_\(iter)")
}

let keysToBatch = (0..<100).map { "key_\($0)" }

cache.batchGetSome().get(keysToBatch)
  .onSuccess { values in
    print("Got \(values.count) values in total")
  }.onFailure {
    print("Failed because \($0)")
  }

在这种情况下,当只有一个请求时,allBatch().get会失败,因为只有99个键已设置,最后一个请求将使整个批量失败并产生valueNotInCache错误。而batchGetSome().get将成功,并打印“总共获取99个值”。

因为allBatch返回一个新的CacheLevel实例,所以它可以像其他缓存一样组合或转换。

let cache = MemoryCacheLevel<String, Int>()
              .allBatch()
              .capRequests(3)

在这种情况下,cache是一种接受字符串键序列的缓存,并返回一个包含整数的列表的Future,但限制了3个并发请求(下一段中将有更多关于限制并发请求的信息)。

限制并发请求

如果您想限制缓存级别可以接受的并发请求的数量,而不是依赖于键(否则请参阅请求池化部分),您可能需要查看capRequests函数。

下面是在实际中的应用示例。

let myCache = MyFirstLevel().compose(MySecondLevel())

let cappedCache = myCache.capRequests(3)

cappedCache现在将仅接受最多3个并发的get操作。如果到达第4个请求,它将被排队,只有在执行中的一个请求完成后才会执行。当资源只能由有限数量的消费者同时访问时,并且建立到资源的另一个连接可能很昂贵或降低已执行请求的性能时,这可能有用。

条件缓存

有时我们可能有一些只在某些条件下才会查询的级别。比如,我们有一个DatabaseLevel,只有当用户在应用中启用某个设置,且实际上开始将数据存储在数据库中时,才会被触发。我们可能希望避免在设置首先启用的情况下访问数据库。

let conditionedCache = cache.conditioned { key in
  Future(appSettingIsEnabled)
}

闭包会得到缓存请求的键,并必须返回一个表示请求是否可以继续或跳过级别的Future<Bool>对象,还有可能通过特定的Error失败以将错误信息传递给调用者。

在运行时,如果变量 appSettingIsEnabled 的值为 false,则会跳过该层级(或如果这是唯一的或最后的层级,则操作将失败)。如果为 true,则会执行 get 请求。

分发缓存

Carlos 0.5 版本发布以来,可以通过 dispatch 协议扩展将给定 CacheLevel 的所有操作调度到特定的 GCD 队列中。

let queue = DispatchQueue(label: "com.vendor.customQueue", attributes: .concurrent)

let cache = CacheProvider.imageCache().dispatch(queue)

结果 CacheLevel 将在指定的队列上调度 getsetonMemoryWarningclear 操作。

多个缓存通道

如果您有一个复杂的场景,根据键或某些外部条件,要么使用一个缓存,要么使用另一个缓存,那么 switchLevels 函数可能很有用。

使用方法

let lane1 = MemoryCacheLevel<URL, NSData>() // The two lanes have to be equivalent (same key type, same value type).
let lane2 = CacheProvider.dataCache() // Keep in mind that you can always use key transformation or value transformations if two lanes don't match by default

let switched = switchLevels(lane1, lane2) { key in
  if key.scheme == "http" {
    return .cacheA
  } else {
    return .cacheB // The example is just meant to show how to return different lanes
  }
}

现在,根据键 URL 的方案,将使用第一条通道或第二条通道。

监听内存警告

如果我们在缓存层级中存储大对象,我们可能希望收到内存警告事件的通知。这就是 listenToMemoryWarningsunsubscribeToMemoryWarnings 函数起作用的地方。

let token = cache.listenToMemoryWarnings()

稍后

unsubscribeToMemoryWarnings(token)

在第一个调用中,当出现内存警告时,缓存层级及其所有组成层级将收到对 onMemoryWarning 的调用。

在第二个调用中,该行为将停止。

请注意,此功能尚不支持 CarlosWatch.framework 框架的 WatchOS 2

归一化

如果您需要将多个 Carlos 组合调用的结果存储在属性中,将属性的类型设置为 BasicCache 可能会很烦琐,因为有些调用返回不同的类型(例如,PoolCacheRequestCapperCache)。在这种情况下,您可以在将缓存层级分配给属性之前对其 normalize 并将其转换为 BasicCache 值。

import Carlos
import PiedPiper

class CacheManager {
  let cache: BasicCache<URL, NSData>

  init(injectedCache: BasicCache<URL, NSData>) {
    self.cache = injectedCache
  }
}

[...]

let manager = CacheManager(injectedCache: CacheProvider.dataCache().pooled().capRequests(3)) // This won't compile

let manager = CacheManager(injectedCache: CacheProvider.dataCache().pooled().capRequests(3).normalize()) // This will

作为提示,始终在您需要将多个组合调用的结果分配给属性时使用 normalize。如果值已经是 BasicCache,则调用将不执行任何操作,因此在那种情况下不会损失性能。

创建自定义层级

创建自定义层级非常简单,并受到鼓励(毕竟,如果只需要内存、磁盘和网络功能,已经有多个缓存库可用!)

让我们看看如何做到这一点。

class MyLevel: CacheLevel {
  typealias KeyType = Int
  typealias OutputType = Float

  func get(_ key: KeyType) -> Future<OutputType> {
    let request = Promise<OutputType>()

    // Perform the fetch and either succeed or fail
    [...]

    request.succeed(1.0)

    return request.future
  }

  func set(_ value: OutputType, forKey key: KeyType) -> Future<()> {  
    let promise = Promise<OutputType>()

    // Store the value (db, memory, file, etc) and call this on completion:
    promise.succeed()

    return promise.future
  }

  func clear() {
    // Clear the stored values
  }

  func onMemoryWarning() {
    // A memory warning event came. React appropriately
  }
}

上面的类符合 CacheLevel 协议。首先,我们需要声明我们接受的键类型和返回的输出类型。在这个示例中,我们有 Int 键和 Float 输出值。

需要实现的方法有 4 个:getsetclearonMemoryWarning

get 需要返回一个 Future,我们可以在方法体开始时创建一个 Promise,并通过调用 future 返回其关联的 Future。然后我们根据获取的结果通过调用其上的 succeedfail 通知监听器。这些调用可以是(并且在大多数情况下将是)异步的。

set 需要返回一个表示操作结果的 Future。它还必须存储给定键的给定值。

clear 表示删除缓存层级的意图。

onMemoryWarning 会在之前调用过 listenToMemoryWarning 方法的情况下,通知内存压力事件。

这个样例缓存现在可以被连接到其他缓存的列表中,如果需要的话,可以像前面段落中看到的那样转换其键或值。

你可以通过几种方式来创建一个 Promise

  • 如上文代码片段所示,即通过实例化一个并调用 succeedfail,根据操作的结果来决定;
  • 直接返回初始化的结果,传递一个值,一个错误或者两者(如果值是可选的)

记住在你的 return 语句中调用 future 在你的 Promise 上!

//#1
let result: Promise<String>()

[...]

result.succeed("success")

// or

result.fail(MyError.invalidData)

return result.future
//#2
return Future("success")

//#3
return Future(MyError.invalidData)

//#4
return Future<String>(value: optionalString, error: MyError.invalidData)

创建自定义抓取器

Carlos 0.4 版本开始,引入了 Fetcher 协议,使库的用户更容易创建自定义抓取器,这些抓取器可以用作缓存中的只读级别。一个“伪装成 Fetcher”的例子在 Carlos 中一直存在,即 NetworkFetcher:你只能用它来从网络读取,不能写入(setclearonMemoryWarning无操作)。

这就是现在实现你自己的自定义抓取器有多简单了

class CustomFetcher: Fetcher {
  typealias KeyType = String
  typealias OutputType = String

  func get(_ key: KeyType) -> Future<OutputType> {
    return Future("Found an hardcoded value :)")
  }
}

当然,你仍然需要声明你的 CacheLevel 处理的 KeyTypeOutputType,但你只需要实现 get。减少你的空白代码!

内置级别

默认情况下,Carlos 提供了 3 个缓存级别

  • MemoryCacheLevel
  • DiskCacheLevel
  • NetworkFetcher
  • 0.5 版本开始,增加了 UserDefaultsCacheLevel

MemoryCacheLevel 是一个易失性缓存,在内部使用 NSCache 实例存储其值。可以通过初始化器指定容量,并且支持在内存压力下清除(如果级别已订阅了内存警告通知)。它接受符合 StringConvertible 协议的任何类型的键,可以存储符合 ExpensiveObject 协议的任何类型的值。在默认情况下,DataNSDataStringNSStringUIImageURL 已经符合后者协议,而 StringNSStringURL 符合 StringConvertible 协议。这个缓存级别是线程安全的。

DiskCacheLevel 是一个持久性缓存,将其值异步存储在磁盘上。可以通过初始化器指定容量,以确保磁盘大小不会太大。它接受符合 StringConvertible 协议的任何类型的键,可以存储符合 NSCoding 协议的任何类型的值。这个缓存级别是线程安全的,并且目前是唯一在调用 set 时可能会失败的 CacheLevel,会返回 DiskCacheLevelError.diskArchiveWriteFailed 错误。

NetworkFetcher 是一个缓存级别,它异步通过网络获取值。它接受 URL 键并返回 NSData 值。这个缓存级别是线程安全的。

NSUserDefaultsCacheLevel 是一个持久性缓存,它将值存储在具有特定名称的 UserDefaults 持久域中。它接受符合 StringConvertible 协议的任何类型的键,可以存储符合 NSCoding 协议的任何类型的值。它使用内部软缓存来避免频繁击中持久存储,并且可以在不影响保存在 standardUserDefaults 或其他持久域中的其他值的情况下进行清除。这个缓存级别是线程安全的。

日志记录

在决定如何处理Carlos中的登录问题时,我们选择了最灵活的方法,这种方法不需要我们编写完整的日志框架代码,即能够插入自己的日志库。如果你想使Carlos的输出仅当超过给定级别时才打印,如果你想完全在发布构建中静默它,或者你想将其路由到文件,或者任何其他情况:只需将你的日志处理闭包分配给Carlos.Logger.output

Carlos.Logger.output = { message, level in
   myLibrary.log(message) //Plug here your logging library
}

我们在生产中使用了XCGLogger,并能够轻松地在调试构建中进行日志记录。

测试

Carlos经过彻底测试,以确保它设计提供的功能在重构时安全,尽可能无错误。

我们使用QuickNimble而不是XCTest,以便拥有良好的BDD测试布局。

到目前为止,为Carlos编写了大约1000项测试(请参阅Tests文件夹),整体而言,测试代码库的大小是生产代码库的两倍。

未来开发

Carlos正在开发中,您可以在这里查看所有开放的问题。它们被分配到里程碑,以便您可以了解特定功能何时将发货。

如果您想为此仓库做出贡献,请

  • 创建一个问题并说明您的问题和解决方案
  • 在您的本地机器上克隆此仓库
  • 创建一个分支,包含问题编号和功能名称的简短摘要
  • 实现您的解决方案
  • 编写测试(未经测试的功能将不会合并)
  • 当所有测试都编写且结果为绿色时,创建一个带有所选方法的简短描述的拉取请求

使用Carlos的应用

正在使用Carlos?请通过拉取请求联系我们,我们将很高兴提及您的应用!

作者

Carlos由WeltN24内部制作。

贡献者

维托里奥·莫内科,[email protected],在Github上的@vittoriom,在Twitter上的@Vittorio_Monaco

艾萨德·哈吉德雷达奇克,@esad

许可证

Carlos可在MIT许可证下获得。有关更多信息,请参阅LICENSE文件。

致谢

Carlos内部使用

类强化的灵感来自Haneke。源代码已大幅修改,但采用原始文件对Carlos的发展已被证明非常有价值。