Kingfisher源码解析系列,因为水平有限,哪里有错,肯请不吝赐教swift
Kingfisher中ImageCache里提供内存缓存和磁盘缓存,分别是MemoryStorage.Backend<KFCrossPlatformImage>
和DiskStorage.Backend<Data>
来实现的,注:内存缓存和磁盘缓存都是经过class Backend
,不过这2个类,是彻底不一样的类,使用枚举来充当命名空间来区分的,分别定义在MemoryStorage.swift
和DiskStorage.swift
中缓存
内存缓存一共有三个类构成,Backend
提供缓存的功能,Config
提供缓存的配置项,StorageObject<T>
缓存的封装类型安全
Config
的主要内容public struct Config {
//内存缓存的最大容量,ImageCache.default中提供的默认值是设备物理内存的四分之一
public var totalCostLimit: Int
//内存缓存的最大长度
public var countLimit: Int = .max
//内存缓存的的过时时长
public var expiration: StorageExpiration = .seconds(300)
//清除过时缓存的时间间隔
public let cleanInterval: TimeInterval
}
复制代码
StorageObject<T>
的主要内容class StorageObject<T> {
//缓存的真正的值
let value: T
//存活时间,也就是多久以后过时
let expiration: StorageExpiration
//缓存e的key
let key: String
//过时时间,默认值是当前时间加上expiration
private(set) var estimatedExpiration: Date
// 更新过时时间
func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
switch extendingExpiration {
case .none://不更新过时时间
return
case .cacheTime://把过时时间设置为当前时间加上存活时间
self.estimatedExpiration = expiration.estimatedExpirationSinceNow
case .expirationTime(let expirationTime)://把过时时间设置为指定时间
self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
}
}
// 是否已通过期
var expired: Bool {
//estimatedExpiration.isPast 是对Date的一个扩展方法,判断estimatedExpiration是否小于当前时间
return estimatedExpiration.isPast
}
}
复制代码
Backend
的主要内容public class Backend<T: CacheCostCalculable> {
//使用NSCache进行缓存
let storage = NSCache<NSString, StorageObject<T>>()
//存放全部缓存的key,在删除过时缓存是有用
var keys = Set<String>()
//定时器,用于定时清除过时数据
private var cleanTimer: Timer? = nil
//配置项
public var config: Config
...下面还有一些缓存数据,读取数据,删除缓存,是否已缓存,删除过时数据等方法
}
复制代码
由上面咱们能够看出,Kingfisher中内存缓存是用NSCache实现的,NSCache是一个相似于Dictionary的类,拥有类似的API,不过区别于Dictionary的是,NSCache是线程安全的,而且提供了设置最大缓存个数和最大缓存大小的配置,Backend就是经过设置NSCache的countLimit
和totalCostLimit
来实现最大缓存个数和最大缓存大小。bash
经过下面的代码,看下Backend是如何缓存数据,读取数据,判断是否已缓存,删除缓存,删除过时数据的?代码中有详细的注释,注:下面的代码删除了一些非核心代码,好比异常,加锁保证线程安全等app
func store(value: T,forKey key: String,expiration: StorageExpiration? = nil) {
//获取存活时间,若缓存时没设置,则从配置中获取
let expiration = expiration ?? config.expiration
//判断是否过时,若已通过期直接返回
guard !expiration.isExpired else { return }
//把要缓存的值封装成StorageObject类型
let object = StorageObject(value, key: key, expiration: expiration)
//把结果缓存起来
storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
//把key保存起来
keys.insert(key)
}
复制代码
// 读取数据
func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
//从NSCache中获取数据,如获取不到直接返回nil
guard let object = storage.object(forKey: key as NSString) else { return nil }
//判断是否过时,若过时直接返回nil
if object.expired { return nil }
//去更新过时时间
object.extendExpiration(extendingExpiration)
return object.value
}
// 判断是否缓存,其本质就是去读取数据,只是不更新缓存时间,若取到,则已缓存,不然未缓存
func isCached(forKey key: String) -> Bool {
guard let _ = value(forKey: key, extendingExpiration: .none) else {
return false
}
return true
}
复制代码
func remove(forKey key: String) throws {
storage.removeObject(forKey: key as NSString)
keys.remove(key)
}
复制代码
func removeExpired() {
for key in keys {
let nsKey = key as NSString
//经过key获取数据,若获取失败,则删除从keys中删除key
guard let object = storage.object(forKey: nsKey) else {
keys.remove(key)
continue
}
//判断object是否过时,若过时,则从cache中删除数据,从keys中删除key
if object.estimatedExpiration.isPast {
storage.removeObject(forKey: nsKey)
keys.remove(key)
}
}
}
复制代码
Kingfisher中磁盘缓存是经过文件系统来实现的,也就是说每一个缓存的数据都对应一个文件,其中Kingfisher把文件的建立时间修改成最后一次读取的时间,把文件的修改时间修改成过时时间。异步
磁盘缓存一共有三个类构成,Backend
提供缓存的功能,Config
提供缓存的配置项,FileMeta
存储着文件信息。async
Config
的主要内容public struct Config {
//磁盘缓存占用磁盘的最大值,为0z时,表示不限制
public var sizeLimit: UInt
//存活时间
public var expiration: StorageExpiration = .days(7)
//文件的扩展名
public var pathExtension: String? = nil
//是否须要把文件名哈希
public var usesHashedFileName = true
//操做文件的FileManager
let fileManager: FileManager
//文件缓存所在的文件夹,默认在cache文件夹里
let directory: URL?
}
复制代码
FileMeta
的主要内容struct FileMeta {
//文件路径
let url: URL
//文件最后一次读取时间
//这个在超过sizeLimit大小时,须要删除文件时,用此属性进行排序,把时间较早的删除掉
let lastAccessDate: Date?
//过时时间
let estimatedExpirationDate: Date?
//是不是个文件夹
let isDirectory: Bool
//文件大小
let fileSize: Int
}
复制代码
Backend
的主要内容public class Backend<T: DataTransformable> {
//配置信息
public var config: Config
//写入文件所在的文件夹,默认在cache文件夹里
public let directoryURL: URL
//修改文件原信息时,所在的队列
let metaChangingQueue: DispatchQueue
//该方法会在init着调用,保证directoryURLs文件夹,已经被建立过了
func prepareDirectory() throws {
let fileManager = config.fileManager
let path = directoryURL.path
guard !fileManager.fileExists(atPath: path) else { return }
try fileManager.createDirectory(atPath: path,withIntermediateDirectories: true,attributes: nil)
}
...下面还有缓存数据,读取数据,判断是否已缓存,删除缓存,删除过时缓存,删除超过sizeLimit的缓存,统计缓存大小等
}
复制代码
经过下面的代码看Backend
是如何缓存数据,读取数据,判断是否已缓存,删除缓存,删除过时缓存,删除超过sizeLimit的缓存,统计缓存大小以及如何经过key生成文件名的?代码中有详细的注释。注:下面的代码删除了一些非核心代码,好比异常,加锁保证线程安全等post
下面那段代码和源码中不太同样,但逻辑是同样的,我改为这样是由于方面我描述测试
//首先判断是否使用key的MD5值当作文件名,如果,则把filename设置成key.MD5
//而后再判断是否设置了扩展名,若设置了,则把扩展名拼接到filename上
func cacheFileName(forKey key: String) -> String {
var filename = key
if config.usesHashedFileName {
filename = key.kf.md5
}
if let ext = config.pathExtension {
filename = "\(filename).\(ext)"
}
return filename
}
复制代码
func store(
value: T,
forKey key: String,
expiration: StorageExpiration? = nil) throws
{
//获取存活时间,若缓存时没设置,则从配置中获取
let expiration = expiration ?? config.expiration
//判断是否过时,若已通过期直接返回
guard !expiration.isExpired else { return }
// 把value转成data,这里value类型是DataTransformable,须要实现toData等其余方法
let data: try value.toData()
//经过cacheKeyc生成一个完整的路径
//完整的路径等于directoryURL+filename
let fileURL = cacheFileURL(forKey: key)
let now = Date()
//把当前时间设置为文件的建立时间,把过时时间设置为文件的修改时间
let attributes: [FileAttributeKey : Any] = [
.creationDate: now.fileAttributeDate,
.modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
]
//经过fileManager把data写入文件
config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
}
复制代码
上面代码中给文件设置建立时间和修改时间用的是给Date扩展的计算属性fileAttributeDate,fileAttributeDate返回的是Date(timeIntervalSince1970: ceil(timeIntervalSince1970)),也就是说把date的秒值向上取整后再转成date,为何要这么作呢?做者解释说,date在内容中实际是一个double类型的值,而在file的属性中,只接受Int类型的值,会默认舍去小数部分,会致使对测试不友好,因此就改为这样了,我不是很理解为何对测试不友好,难道是会致使提早一会结束过时吗?fetch
func value(
forKey key: String,/
referenceDate: Date,
actuallyLoad: Bool,
extendingExpiration: ExpirationExtending) throws -> T?
{
let fileManager = config.fileManager
//经过cacheKeyc生成一个完整的路径
let fileURL = cacheFileURL(forKey: key)
let filePath = fileURL.path
//判断是否存在该文件是否存在
guard fileManager.fileExists(atPath: filePath) else {
return nil
}
//经过fileURL生成一个FileMeta文件描述信息的类
let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
let meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)
//判断文件的过时时间是否大于referenceDate
if meta.expired(referenceDate: referenceDate) {
return nil
}
//判断是不是真的须要去加载数据,好比判断是否已缓存的时候,就不须要真的去加载,只要知道有就行了
if !actuallyLoad { return T.empty }
//读取文件
let data = try Data(contentsOf: fileURL)
let obj = try T.fromData(data)
//更新文件的描述信息,本质也是为了h更新最后一次的读取时间和过时时间
metaChangingQueue.async { meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration) }
}
复制代码
经过调用value方法,判断value的返回值是否为nil,调用时会把actuallyLoad参数传为false,这样就不会去读取文件
//经过key生成URL,而后把该文件删除
func remove(forKey key: String) throws {
let fileURL = cacheFileURL(forKey: key)
config.fileManager.removeItem(at: url)
}
//直接把文件夹删除
func removeAll(skipCreatingDirectory: Bool) throws {
try config.fileManager.removeItem(at: directoryURL)
if !skipCreatingDirectory {
try prepareDirectory()
}
}
复制代码
获取文件夹下的全部文件,并把每一个文件的大小加起来
//删除在指定时间过时的缓存,若传入当前时间,则是删除如今已通过期的文件
//返回值:删除的文件路径
func removeExpiredValues(referenceDate: Date = Date()) throws -> [URL] {
let propertyKeys: [URLResourceKey] = [
.isDirectoryKey,
.contentModificationDateKey
]
//获取全部的文件URL
let urls = try allFileURLs(for: propertyKeys)
let keys = Set(propertyKeys)
//过滤出过时的文件URL
let expiredFiles = urls.filter { fileURL in
let meta = FileMeta(fileURL: fileURL, resourceKeys: keys)
if meta.isDirectory {
return false
}
return meta.expired(referenceDate: referenceDate)
}
//遍历全部的过时的文件UR,依次删除它们
try expiredFiles.forEach { url in
try removeFile(at: url)
}
return expiredFiles
}
复制代码
func removeSizeExceededValues() throws -> [URL] {
//若是sizeLimit == 0表明不限制大小,直接返回
if config.sizeLimit == 0 { return [] }
var size = try totalSize()
//若是当前的缓存大小小于sizeLimit直接返回
if size < config.sizeLimit { return [] }
let urls = 获取全部的URLs
//经过urls生成全部的文件信息,这里包含的信息有是不是文件夹,建立时间,和文件大小
var pendings: [FileMeta] = urls.compactMap { fileURL in
guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else {
return nil
}
return meta
}
//经过建立时间排序,也就是经过最后一次的读取时间
pendings.sort(by: FileMeta.lastAccessDate)
var removed: [URL] = []
let target = config.sizeLimit / 2
//直到当前缓存大小小于sizeLimit的2分之一,不然按照最后的读取时间一次删除
while size > target, let meta = pendings.popLast() {
size -= UInt(meta.fileSize)
try removeFile(at: meta.url)
removed.append(meta.url)
}
return removed
}
复制代码
在ImageCache里监听了三个通知,分别是收到内存警告,应用即将被杀死,应用已经进入到后台,在这三个通知里分别作了,清空内存缓存,异步的清除磁盘过时缓存和磁盘大小超过simeLimit清除缓存,在后台清除磁盘过时缓存和磁盘大小超过simeLimit清除缓存