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清除緩存