一个简单但灵活的缓存,使用 Swift 编写,适用于
iOS 13+
和WatchOS 6
应用。
Carlos 1.0.0 已从 PiedPiper 依赖迁移到 Combine,因此最低支持的平台版本与 Combine 的最低支持平台版本相同。 有关更多信息,请参见发行说明页面。
Carlos
是一小组类和函数,用于在您的应用程序中实现自定义、灵活和强大的缓存层。
使用函数式编程词汇,Carlos 构成一个幺半群缓存系统。 您可以在此处或此视频中查看关于如何实现的最佳解释,感谢 @bkase 提供幻灯片。
默认情况下,Carlos
附带一个内存缓存、一个磁盘缓存、一个简单的网络获取器和一个 NSUserDefaults
缓存(磁盘缓存的灵感来自 HanekeSwift)。
使用 Carlos
,您可以
Carlos
已经提供了一些常用的值转换器Carlos
可以为您处理通过 Xcode 将 Carlos
添加到您的项目,或将以下行添加到您的包依赖项中
.package("https://github.com/spring-media/Carlos", from: "1.0.0")
Carlos
可通过 CocoaPods 获得。 要安装它,只需将以下行添加到您的 Podfile 中
pod "Carlos", :git => "https://github.com/spring-media/Carlos"
也支持 Carthage
。
要运行示例项目,请克隆存储库。
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")
.sink(
receiveCompletion: { completion in
if case let .failure(error) = completion {
print("An error occurred :( \(error)")
}
},
receiveValue: { value in
print("I found \(value)!")
}
)
也可以使用 cancel()
方法取消请求,您可以通过在给定请求上调用 onCancel
来收到此事件的通知
let cancellable = cache.get(key)
.handleEvents(receiveCancel: {
print("Looks like somebody canceled this request!")
})
.sink(...)
[... somewhere else]
cancellable.cancel()
但是,此缓存不是很有用。 它永远不会主动获取值,只会存储它们以供以后使用。 让我们尝试让它更有趣
let cache = MemoryCacheLevel()
.compose(DiskCacheLevel())
.compose(NetworkFetcher())
这将创建一个缓存级别,该缓存级别接受 URL
键并存储 NSData
值(该类型是从 NetworkFetcher
的 URL
键和 NSData
值的硬性要求推断出来的,而 MemoryCacheLevel
和 DiskCacheLevel
更加灵活,如下所述)。
键转换旨在使将缓存级别插入到您正在构建的任何缓存中成为可能。
让我们看看它们是如何工作的
// Define your custom ErrorType values
enum URLTransformationError: Error {
case invalidURLString
}
let transformedCache = NetworkFetcher().transformKeys(
OneWayTransformationBox(
transform: {
Future { promise in
let url = URL(string: $0) {
promise(.success(url))
} else {
promise(.failure(URLTransformationError.invalidURLString))
}
}
}
)
)
在上面的代码行中,我们说进入 NetworkFetcher 级别的所有键都必须首先转换为 URL
值。 现在,我们可以将此缓存插入到先前定义的接受 String
键的缓存级别中
let cache = MemoryCacheLevel<String, NSData>().compose(transformedCache)
如果这看起来不是很安全(一个人总是可以将字符串垃圾作为键传递,它不会神奇地转换为 URL
,从而导致 NetworkFetcher
静默失败),我们仍然可以使用特定于域的结构作为键,假设它包含 String
和 URL
值
struct Image {
let identifier: String
let URL: Foundation.URL
}
let imageToString = OneWayTransformationBox(transform: { (image: Image) -> AnyPublisher<String, String> in
Just(image.identifier).eraseToAnyPublisher()
})
let imageToURL = OneWayTransformationBox(transform: { (image: Image) -> AnyPublisher<URL> in
Just(image.URL).eraseToAnyPublisher()
})
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).sink {
print("Found \(value)!")
}
自从 Carlos 0.5
起,您也可以将条件应用于用于键转换的 OneWayTransformers
。 只需在转换器上调用 conditioned
函数并传递您的条件。 该条件也可以是异步的,并且必须返回一个 AnyPublisher<Bool, Error>
,有机会为转换失败返回一个特定的错误。
let transformer = OneWayTransformationBox<String, URL>(transform: { key in
Future { promise in
if let value = URL(string: key) {
promise(.success(value))
} else {
promise(.failure(MyError.stringIsNotURL))
}
}.eraseToAnyPublisher()
}).conditioned { key in
Just(key)
.filter { $0.rangeOfString("http") != nil }
.eraseToAnyPublisher()
}
let cache = CacheProvider.imageCache().transformKeys(transformer)
但这还不是全部。
如果我们的磁盘缓存只存储 Data
,但我们希望我们的内存缓存方便地存储 UIImage
实例呢?
值转换器允许您拥有一个缓存(假设)存储 Data
并将其变异为一个存储 UIImage
值的缓存。 让我们看看如何操作
let dataTransformer = TwoWayTransformationBox(transform: { (image: UIImage) -> AnyPublisher<Data, Error> in
Just(UIImagePNGRepresentation(image)).eraseToAnyPublisher()
}, inverseTransform: { (data: Data) -> AnyPublisher<UIImage, Error> in
Just(UIImage(data: data)!).eraseToAnyPublisher()
})
let memoryLevel = MemoryCacheLevel<String, UIImage>().transformKeys(imageToString).transformValues(dataTransformer)
这个内存级别现在可以替换我们之前的内存级别,不同之处在于它将在内部存储 UIImage
值!
请记住,与键转换一样,如果您的转换闭包失败(正向转换或反向转换),则将跳过缓存级别,就好像获取失败一样。 相同的注意事项适用于 set
调用。
Carlos
附带一些开箱即用的值转换器,例如
JSONTransformer
将 NSData
实例序列化为 JSONImageTransformer
将 NSData
实例序列化为 UIImage
值(在 Mac OS X 框架上不可用)StringTransformer
使用给定的编码将 NSData
实例序列化为 String
值DateFormatter
、NumberFormatter
、MKDistanceFormatter
),以便您可以根据需要使用自定义的实例。从 Carlos 0.4
开始,可以使用 OneWayTransformer
转换来自 Fetcher
实例的值(与用于正常 CacheLevel
实例的必需 TwoWayTransformer
相反。 这是因为 Fetcher
协议不需要 set
)。 这意味着您可以轻松地链接从互联网获取 JSON 并将其输出转换为模型对象(例如 struct
)的 Fetcher
,从而形成一个复杂的缓存管道,而无需创建一个虚拟的反向转换来满足 TwoWayTransformer
协议的要求。
从 Carlos 0.5
开始,所有转换器都原生支持异步计算,因此您可以在自定义转换器中进行昂贵的转换,而不会阻止其他操作。 实际上,开箱即用的 ImageTransformer
在后台队列中处理图像转换。
从 Carlos 0.5
开始,您还可以将条件应用于用于值转换的 TwoWayTransformers
。 只需在转换器上调用 conditioned
函数并传递您的条件(一个用于正向转换,一个用于反向转换)。 该条件也可以是异步的,并且必须返回一个 AnyPublisher<Bool, Error>
,有机会为转换失败返回一个特定的错误。
let transformer = JSONTransformer().conditioned({ input in
Just(myCondition).eraseToAnyPublisher()
}, inverseCondition: { input in
Just(myCondition)eraseToAnyPublisher()
})
let cache = CacheProvider.dataCache().transformValues(transformer)
在某些情况下,您的缓存级别可能会返回正确的值,但格式可能并非最佳。例如,您可能希望清理从缓存整体获取的输出,而无需考虑返回该输出的具体层。
对于这些情况,Carlos 0.4
引入的 postProcess
函数可能会有所帮助。 该函数可以作为 CacheLevel
协议的协议扩展使用。
postProcess
函数接受一个 CacheLevel
和一个 TypeIn == TypeOut
的 OneWayTransformer
作为参数,并输出一个装饰过的 BasicCache
,其中嵌入了后处理步骤。
// Let's create a simple "to uppercase" transformer
let transformer = OneWayTransformationBox<NSString, String>(transform: { Just($0.uppercased() as String).eraseToAnyPublisher() })
// 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").sink { value in
let x = value
}
// We get the uppercase value from the decorated cache, though
transformedCache.get("key").sink { value in
let x = value
}
从 Carlos 0.5
开始,您还可以将条件应用于用于后处理转换的 OneWayTransformers
。 只需在转换器上调用 conditioned
函数并传递您的条件即可。 该条件也可以是异步的,并且必须返回一个 AnyPublisher<Bool, Error>
,这样就有机会为转换失败返回特定的错误。 请记住,该条件实际上会将缓存的输出作为输入,而不是用于获取此值的键! 如果要应用基于键的条件,请改用 conditionedPostProcess
,但请记住,这尚不支持使用 OneWayTransformer
实例。
let processer = OneWayTransformationBox<NSData, NSData>(transform: { value in
Future { promise in
if let value = String(data: value as Data, encoding: .utf8)?.uppercased().data(using: .utf8) as NSData? {
promise(.success(value))
} else {
promise(.failure(FetchError.conditionNotSatisfied))
}
}
}).conditioned { value in
Just(value.length < 1000).eraseToAnyPublisher()
}
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 Just(value.uppercased()).eraseToAnyPublisher()
} else {
return Just(value).eraseToAnyPublisher()
}
})
// 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").sink { value in
let x = value
}
// We get the uppercase value from the decorated cache, though
transformedCache.get("some sentinel value").sink { value in
let x = value
}
扩展了简单的 值转换 的情况,您还可以应用基于用于获取或设置该值的键的条件转换。
对于这些情况,Carlos 0.6
引入的 conditionedValueTransformation
函数可能会有所帮助。 该函数可以作为 CacheLevel
协议的协议扩展使用。
conditionedValueTransformation
函数接受一个 CacheLevel
和一个符合 ConditionedTwoWayTransformer
的条件转换器作为参数,并输出一个装饰过的 CacheLevel
,其中具有修改过的 OutputType
(等于转换器的 TypeOut
,就像在正常值转换情况下一样),并且嵌入了条件值转换步骤。
// 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 Just(1).eraseToAnyPublisher()
} else {
return Just(0).eraseToAnyPublisher()
}
}, conditionalInverseTransformClosure: { (key, value) in
if key > 0 {
return Just("Positive").eraseToAnyPublisher()
} else {
return Just("Null or negative").eraseToAnyPublisher()
}
})
// 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").sink { value in
let x = value
}
// We get 1 from the decorated cache, though
transformedCache.get("some sentinel value").sink { 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
。 这将返回一个 AnyPublisher
,当指定键的所有请求 *完成* 时(不一定是成功)它才会成功。 但是,您只会获得成功回调中的成功值。
从 Carlos 0.9
开始,您可以通过 allBatch
将您的 CacheLevel
转换为接受键列表的缓存级别。 在这样的 CacheLevel
上调用 get
会返回一个 AnyPublisher
,只有当 **所有** 指定键的请求都成功时,它才会成功,并且 **只要** 指定键的请求 **之一** 失败,它就会失败。 如果您取消此 CacheLevel
返回的 AnyPublisher
,则所有挂起的请求也会被取消。
一个用法示例
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(keysToBatch).sink(
receiveCompletion: { completion in
print("Failed because \($0)")
},
receiveValue: { values in
print("Got \(values.count) values in total")
}
)
在这种情况下,allBatch().get
调用将失败,因为只设置了 99 个键,并且最后一个请求将导致整个批处理失败,并显示 valueNotInCache
错误。 相反,batchGetSome().get
将成功,并打印 Got 99 values in total
。
由于 allBatch
返回一个新的 CacheLevel
实例,因此可以像任何其他缓存一样对其进行组合或转换
在这种情况下,cache
是一个接受 String
键序列并返回 Int
值列表的 AnyPublisher
的缓存,但仅限于 3 个并发请求(有关限制并发请求的更多信息,请参见下一段)。
有时,我们可能有应该仅在某些条件下查询的级别。 假设我们有一个 DatabaseLevel
,只有当用户在应用程序中启用给定的设置(实际上开始在数据库中存储数据)时才应触发它。 我们可能希望避免首先在禁用该设置时访问数据库。
let conditionedCache = cache.conditioned { key in
Just(appSettingIsEnabled).eraseToAnyPublisher()
}
该闭包获取缓存被要求获取的键,并且必须返回一个 AnyPublisher<Bool, Error>
对象,指示该请求是否可以继续或应该跳过该级别,并且可以选择使用特定的 Error
失败,以便将错误传达给调用者。
在运行时,如果变量 appSettingIsEnabled
为 false
,则 get
请求将跳过该级别(或者如果这是缓存中唯一或最后一个级别,则会失败)。 如果 true
,则将执行 get
请求。
如果您有一个复杂的场景,根据键或某些其他外部条件,应该使用一个或另一个缓存,那么 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 的方案,将使用第一个通道或第二个通道。
如果我们在缓存级别中存储较大的对象,我们可能希望收到内存警告事件的通知。 这就是 listenToMemoryWarnings
和 unsubscribeToMemoryWarnings
函数派上用场的地方
let token = cache.listenToMemoryWarnings()
稍后
unsubscribeToMemoryWarnings(token)
通过第一次调用,当出现内存警告时,缓存级别及其所有组成级别都将收到对 onMemoryWarning
的调用。
通过第二次调用,该行为将停止。
请记住,WatchOS 2 框架 CarlosWatch.framework
尚不支持此功能。
如果您需要在属性中存储多个 Carlos
组合调用的结果,则将属性的类型设置为 BasicCache
可能会很麻烦,因为某些调用会返回不同的类型(例如 PoolCache
)。 在这种情况下,您可以在将缓存级别分配给该属性之前对其进行 normalize
,它将被转换为 BasicCache
值。
import Carlos
class CacheManager {
let cache: BasicCache<URL, NSData>
init(injectedCache: BasicCache<URL, NSData>) {
self.cache = injectedCache
}
}
[...]
let manager = CacheManager(injectedCache: CacheProvider.dataCache().pooled()) // This won't compile
let manager = CacheManager(injectedCache: CacheProvider.dataCache().pooled().normalize()) // This will
作为提示,如果您需要将多个组合调用的结果分配给属性,请始终使用 normalize
。 如果该值已经是 BasicCache
,则该调用是一个空操作,因此在这种情况下不会有性能损失。
创建自定义级别既简单又鼓励(毕竟,如果您只需要内存、磁盘和网络功能,那么已经有多个缓存库可用!)。
让我们看看如何做到这一点
class MyLevel: CacheLevel {
typealias KeyType = Int
typealias OutputType = Float
func get(_ key: KeyType) -> AnyPublisher<OutputType, Error> {
Future {
// Perform the fetch and either succeed or fail
}.eraseToAnyPublisher()
}
func set(_ value: OutputType, forKey key: KeyType) -> AnyPublisher<Void, Error> {
Future {
// Store the value (db, memory, file, etc) and call this on completion:
}.eraseToAnyPublisher()
}
func clear() {
// Clear the stored values
}
func onMemoryWarning() {
// A memory warning event came. React appropriately
}
}
上面的类符合 CacheLevel
协议。 我们首先需要声明我们接受哪些键类型以及我们返回哪些输出类型。 在此示例中,我们有 Int
键和 Float
输出值。
需要实现的必需方法有 4 个:get
、set
、clear
和 onMemoryWarning
。 现在可以将此示例缓存流水线式地连接到其他缓存列表,如果需要,可以转换其键或值,正如我们在前面的段落中看到的那样。
在 Carlos 0.4
中,引入了 Fetcher
协议,以使库用户更容易创建可以作为缓存中的只读级别使用的自定义获取器。 一直包含在 Carlos
中的“伪装的 Fetcher
”的一个例子是 NetworkFetcher
:您只能使用它从网络读取,而不能写入(set
、clear
和 onMemoryWarning
是 **空操作**)。
现在实现您的自定义获取器就是这么容易
class CustomFetcher: Fetcher {
typealias KeyType = String
typealias OutputType = String
func get(_ key: KeyType) -> Anypublisher<OutputType, Error> {
return Just("Found an hardcoded value :)").eraseToAnyPublisher()
}
}
您仍然需要声明您的 CacheLevel
处理什么 KeyType
和 OutputType
,当然,但然后您只需要实现 get
。 减少了您的样板代码!
Carlos
默认带有 3 个缓存级别
MemoryCacheLevel
DiskCacheLevel
NetworkFetcher
0.5
版本以来,还提供了一个 UserDefaultsCacheLevel
MemoryCacheLevel 是一个易失性缓存,它在内部将其值存储在 NSCache
实例中。 容量可以通过初始化程序指定,并且它支持在内存压力下进行清除(如果该级别 订阅了内存警告通知)。 它接受符合 StringConvertible
协议的任何给定类型的键,并且可以存储符合 ExpensiveObject
协议的任何给定类型的值。 Data
、NSData
、String
、NSString
、UIImage
、URL
已经默认符合后一个协议,而 String
、NSString
和 URL
符合 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
}
Carlos
经过全面测试,以确保其设计提供的功能对于重构和尽可能地消除错误是安全的。
我们使用 Quick 和 Nimble 而不是 XCTest
,以便拥有一个良好的 BDD 测试布局。
截至今天,Carlos
大约有 1000 个测试 (参见 Tests
文件夹),并且总体而言,测试代码库的规模是生产代码库的两倍。
Carlos
正在开发中,您可以在 这里 看到所有未解决的问题。 它们被分配给里程碑,以便您可以了解特定功能何时发布。
如果您想为此存储库做出贡献,请
正在使用 Carlos 吗? 请通过 Pull request 告知我们,我们很乐意提及您的应用程序!
Carlos
是由 WeltN24 内部开发的
Vittorio Monaco, vittorio.monaco@weltn24.de, @vittoriom on Github, @Vittorio_Monaco on Twitter
Esad Hajdarevic, @esad
Carlos
在 MIT 许可下可用。 有关更多信息,请参见 LICENSE 文件。
Carlos
内部使用
DiskCacheLevel 类受到 Haneke 的启发。 源代码已进行了大量修改,但是改编原始文件已被证明对 Carlos
的开发非常有价值。