Kingfisher源碼解析之ImageCache

Kingfisher源碼解析系列,因爲水平有限,哪裏有錯,肯請不吝賜教swift

Kingfisher中ImageCache裏提供內存緩存和磁盤緩存,分別是MemoryStorage.Backend<KFCrossPlatformImage>DiskStorage.Backend<Data>來實現的,注:內存緩存和磁盤緩存都是經過class Backend,不過這2個類,是徹底不一樣的類,使用枚舉來充當命名空間來區分的,分別定義在MemoryStorage.swiftDiskStorage.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的countLimittotalCostLimit來實現最大緩存個數和最大緩存大小。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)
}
複製代碼
刪除過時數據,這裏使用Set存儲key的緣由是NSCache,並無像Dictionary同樣提供獲取allKeys或allValues的方法
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生成文件名

下面那段代碼和源碼中不太同樣,但邏輯是同樣的,我改爲這樣是由於方面我描述測試

//首先判斷是否使用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刪除緩存,以及刪除全部緩存
//經過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
    }
複製代碼
緩存大小超過sizeLimit時刪除緩存
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清除緩存

相關文章
相關標籤/搜索