Kingfisher源碼導讀

前言

在使用第三方庫的時候,每每只用其功能,不多去關注背後的實現細節。本文從源碼(Kingfisher v5.13.0)角度,學習與研究Kingfisher背後實現細節。git

kf屬性

kf是計算屬性,用以返回包裹系統類UIIMageView,UIButton,NSButton的KingfisherWrapper,也就是用struct裝飾泛型Base。github

public struct KingfisherWrapper<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

extension KingfisherCompatible {
    /// Gets a namespace holder for Kingfisher compatible types.
    public var kf: KingfisherWrapper<Self> {
        get { return KingfisherWrapper(self) }
        set { }
    }
}

extension KingfisherCompatibleValue {
    /// Gets a namespace holder for Kingfisher compatible types.
    public var kf: KingfisherWrapper<Self> {
        get { return KingfisherWrapper(self) }
        set { }
    }
}
複製代碼

setImage()

在設置圖片的時候,不管是UIImageView,UIButton,NSButton都調用了setImage() 。這個方法核心的功能,在於使用KingfisherManager的retrieveImage來生成下載任務,而且將下載任務存儲到當前實例的KingfisherWrapper的imageTask屬性。可是在KingfisherWrapper中並無定義imageTask屬性,這個怎麼作到的?使用了關聯對象來動態存取。緩存

private var imageTask: DownloadTask? {
    get { return getAssociatedObject(base, &imageTaskKey) }
    set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)}
}

public private(set) var taskIdentifier: Source.Identifier.Value? {
    get {
        let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey)
        return box?.value
    }
    set {
        let box = newValue.map { Box($0) }
        setRetainedAssociatedObject(base, &taskIdentifierKey, box)
    }
}
複製代碼

每一個下載任務都有本身的Identifier,這個Identifier也是經過關聯對象動態設置爲KingfisherWrapper的屬性。被關聯的對象必須是具備內存管理的類型,所以,這裏將UInt類型的Identifier封裝在了Box類裏,間接完成Identifier的設置。bash

class Box<T> {
    var value: T
    
    init(_ value: T) {
        self.value = value
    }
}

複製代碼

setImage()方法是整個Kingfisher提供給外部對象導入使用加載網絡圖片。網絡

@discardableResult
public func setImage(
    with source: Source?,
    placeholder: Placeholder? = nil,
    options: KingfisherOptionsInfo? = nil,
    progressBlock: DownloadProgressBlock? = nil,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
    var mutatingSelf = self
    // source判空
    guard let source = source else {
        mutatingSelf.placeholder = placeholder
        mutatingSelf.taskIdentifier = nil
        completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
        return nil
    }
    // 加載配置
    var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
    let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
    if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet {
        // Always set placeholder while there is no image/placeholder yet.
        mutatingSelf.placeholder = placeholder
    }

    let maybeIndicator = indicator
    maybeIndicator?.startAnimatingView()
    // 任務Identifier自增長,以確保每次調用此方法,都是一個新的加載任務。
    let issuedIdentifier = Source.Identifier.next()
    mutatingSelf.taskIdentifier = issuedIdentifier

    if base.shouldPreloadAllAnimation() {
        options.preloadAllAnimationData = true
    }

    if let block = progressBlock {
        options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
    }
    // 圖片若是是有Provider提供,直接獲取。
    if let provider = ImageProgressiveProvider(options, refresh: { image in
        self.base.image = image
    }) {
        options.onDataReceived = (options.onDataReceived ?? []) + [provider]
    }
    
    options.onDataReceived?.forEach {
        $0.onShouldApply = { issuedIdentifier == self.taskIdentifier }
    }
    // 非以上情形,執行KingfisherManager裏,加載圖片。
    let task = KingfisherManager.shared.retrieveImage(
        with: source,
        options: options,
        downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
        completionHandler: { result in
            // 成功獲取到了圖片,執行回調
            CallbackQueue.mainCurrentOrAsync.execute {
                // 指示器中止動畫
                maybeIndicator?.stopAnimatingView()
                guard issuedIdentifier == self.taskIdentifier else {
                    let reason: KingfisherError.ImageSettingErrorReason
                    do {
                        let value = try result.get()
                        reason = .notCurrentSourceTask(result: value, error: nil, source: source)
                    } catch {
                        reason = .notCurrentSourceTask(result: nil, error: error, source: source)
                    }
                    let error = KingfisherError.imageSettingError(reason: reason)
                    completionHandler?(.failure(error))
                    return
                }
                
                mutatingSelf.imageTask = nil
                mutatingSelf.taskIdentifier = nil
                
                switch result {
                case .success(let value):
                    // 是否有Transition過程
                    guard self.needsTransition(options: options, cacheType: value.cacheType) else {
                        mutatingSelf.placeholder = nil
                        self.base.image = value.image
                        completionHandler?(result)
                        return
                    }
                    // 執行Transition,並更新圖片
                    self.makeTransition(image: value.image, transition: options.transition) {
                        completionHandler?(result)
                    }
                    
                case .failure:
                    if let image = options.onFailureImage {
                        self.base.image = image
                    }
                    completionHandler?(result)
                }
            }
        }
    )
    mutatingSelf.imageTask = task
    return task
}

複製代碼

管理者 - KingfisherManager

獲取圖片

從setImage()到是否執行請求下載圖片的邏輯,是由KingfisherManager的retrieveImage()處理的。retrieveImage()的處理邏輯是:session

  1. 判斷圖片是否須要強制刷新,若是是,就執行loadAndCacheImage方法生成下載任務
  2. 圖片在緩存中存在且不強制刷新,而且當前retrieveImage方法返回nil,不生成新的下載任務
  3. 圖片在緩存中不存在且不強制刷新;生成下載任務並緩存圖片。
private func retrieveImage(
    with source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
{
    let options = context.options
	  // 判斷是否須要強制刷新,是執行loadAndCacheImage方法生成下載任務
    if options.forceRefresh {
        return loadAndCacheImage(
            source: source,
            context: context,
            completionHandler: completionHandler)?.value
        
    } else {
    	  // 從緩存中加載
        let loadedFromCache = retrieveImageFromCache(
            source: source,
            context: context,
            completionHandler: completionHandler)
        // 若是緩存中有這張圖片,當前返回nil,不生成下載任務,執行結束
        if loadedFromCache {
            return nil
        }
        // 未從緩存中找到圖片,但配置是隻從緩存中讀取圖片,此時,拋出Error。
        if options.onlyFromCache {
            let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))
            completionHandler?(.failure(error))
            return nil
        }
        
        return loadAndCacheImage(
            source: source,
            context: context,
            completionHandler: completionHandler)?.value
    }
}
複製代碼

能夠看出,不管是在哪一種狀況下,只須要調用kf.setImage()方法去設置圖片;圖片的下載,緩存,以及是否須要開啓網絡請求下載圖片都隱藏於這行代碼之下了。閉包

從緩存中獲取圖片 - retrieveImageFromCache

這個retrieveImageFromCache()包含了從內存 / 硬盤中查找圖片,返回值爲Bool,顯示緩存中有無這張圖片。app

func retrieveImageFromCache(
        source: Source,
        context: RetrievingContext,
        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool
    {
        let options = context.options
        // 1. 判斷圖片是否已經存在目標緩存中
        let targetCache = options.targetCache ?? cache
        let key = source.cacheKey
        let targetImageCached = targetCache.imageCachedType(
            forKey: key, processorIdentifier: options.processor.identifier)
        let validCache = targetImageCached.cached &&
            (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory)
        if validCache {
        	  // 獲取圖片
            targetCache.retrieveImage(forKey: key, options: options) { result in
                guard let completionHandler = completionHandler else { return }
                options.callbackQueue.execute {
                    result.match(
                        onSuccess: { cacheResult in
                            let value: Result<RetrieveImageResult, KingfisherError>
                            if let image = cacheResult.image {
                                value = result.map {
                                    RetrieveImageResult(
                                        image: image,
                                        cacheType: $0.cacheType,
                                        source: source,
                                        originalSource: context.originalSource
                                    )
                                }
                            } else {
                                value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))
                            }
                            completionHandler(value)
                        },
                        onFailure: { _ in
                            completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key:key))))
                        }
                    )
                }
            }
            return true
        }

        // 2. 判斷原始的圖片是否已經緩存,若是存在緩存圖片,那麼直接返回。
        let originalCache = options.originalCache ?? targetCache
        // No need to store the same file in the same cache again.
        if originalCache === targetCache && options.processor == DefaultImageProcessor.default {
            return false
        }

        // 檢查是否存在未處理的圖片
        let originalImageCacheType = originalCache.imageCachedType(
            forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier)
        let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh
        
        let canUseOriginalImageCache =
            (canAcceptDiskCache && originalImageCacheType.cached) ||
            (!canAcceptDiskCache && originalImageCacheType == .memory)
        
        if canUseOriginalImageCache {
            // 找到緩存的圖片,處理爲原始的數據
            var optionsWithoutProcessor = options
            optionsWithoutProcessor.processor = DefaultImageProcessor.default
            originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in

                result.match(
                    onSuccess: { cacheResult in
                        guard let image = cacheResult.image else {
                            assertionFailure("The image (under key: \(key) should be existing in the original cache.")
                            return
                        }

                        let processor = options.processor
                        (options.processingQueue ?? self.processingQueue).execute {
                            let item = ImageProcessItem.image(image)
                            guard let processedImage = processor.process(item: item, options: options) else {
                                let error = KingfisherError.processorError(
                                    reason: .processingFailed(processor: processor, item: item))
                                options.callbackQueue.execute { completionHandler?(.failure(error)) }
                                return
                            }

                            var cacheOptions = options
                            cacheOptions.callbackQueue = .untouch
                            // 回調協做器
                            let coordinator = CacheCallbackCoordinator(
                                shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false)
                            // 緩存已經處理過的圖片
                            targetCache.store(
                                processedImage,
                                forKey: key,
                                options: cacheOptions,
                                toDisk: !options.cacheMemoryOnly)
                            {
                                _ in
                                coordinator.apply(.cachingImage) {
                                    let value = RetrieveImageResult(
                                        image: processedImage,
                                        cacheType: .none,
                                        source: source,
                                        originalSource: context.originalSource
                                    )
                                    options.callbackQueue.execute { completionHandler?(.success(value)) }
                                }
                            }

                            coordinator.apply(.cacheInitiated) {
                                let value = RetrieveImageResult(
                                    image: processedImage,
                                    cacheType: .none,
                                    source: source,
                                    originalSource: context.originalSource
                                )
                                options.callbackQueue.execute { completionHandler?(.success(value)) }
                            }
                        }
                    },
                    onFailure: { _ in
                        // 失敗回調
                        options.callbackQueue.execute {
                            completionHandler?(
                                .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))
                            )
                        }
                    }
                )
            }
            return true
        }

        return false
    }

複製代碼

從網絡中加載並緩存圖片 - loadAndCacheImage

什麼時候會執行從網絡中加載並緩存圖片 ? 分爲兩種狀況:框架

  • 設置圖片時,緩存(內存 / 硬盤)中沒有這張圖片。
  • 設置圖片時,強制刷新圖片數據。

loadAndCacheImage方法作了個判斷。判斷當前圖片是否須要從網絡中下載獲取(從網絡中獲取,執行下載,並緩存圖片),仍是經過ImageProvider獲取(直接獲取圖片並緩存),並生成對應的下載任務。async

@discardableResult
func loadAndCacheImage(
    source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
{
    let options = context.options
    // 定義內嵌函數
    func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
        cacheImage(
            source: source,
            options: options,
            context: context,
            result: result,
            completionHandler: completionHandler
        )
    }
    
    switch source {
    // 從網絡中加載圖片
    case .network(let resource):
        let downloader = options.downloader ?? self.downloader
        let task = downloader.downloadImage(
            with: resource.downloadURL, options: options, completionHandler: _cacheImage
        )
        return task.map(DownloadTask.WrappedTask.download)
    // 從ImageProvider中加載圖片
    case .provider(let provider):
        provideImage(provider: provider, options: options, completionHandler: _cacheImage)
        return .dataProviding
    }
}

複製代碼

緩存細節

loadAndCacheImage中的緩存邏輯

loadAndCacheImage中,執行了下載圖片任務後,都會執行_cacheImage()內嵌函數。_cacheImage內部執行cacheImage。cacheImage的實現:

private func cacheImage(
    source: Source,
    options: KingfisherParsedOptionsInfo,
    context: RetrievingContext,
    result: Result<ImageLoadingResult, KingfisherError>,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?)
{
    switch result {
    case .success(let value):
        let needToCacheOriginalImage = options.cacheOriginalImage &&
                                       options.processor != DefaultImageProcessor.default
                                           
        let coordinator = CacheCallbackCoordinator(
            shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage)
        // 將圖片緩存
        let targetCache = options.targetCache ?? self.cache
        targetCache.store(
            value.image,
            original: value.originalData,
            forKey: source.cacheKey,
            options: options,
            toDisk: !options.cacheMemoryOnly)
        {
            _ in
            coordinator.apply(.cachingImage) {
                let result = RetrieveImageResult(
                    image: value.image,
                    cacheType: .none,
                    source: source,
                    originalSource: context.originalSource
                )
                completionHandler?(.success(result))
            }
        }
        // 判斷是否須要緩存原始圖片
        if needToCacheOriginalImage {
            let originalCache = options.originalCache ?? targetCache
            originalCache.storeToDisk(
                value.originalData,
                forKey: source.cacheKey,
                processorIdentifier: DefaultImageProcessor.default.identifier,
                expiration: options.diskCacheExpiration)
            {
                _ in
                coordinator.apply(.cachingOriginalImage) {
                    let result = RetrieveImageResult(
                        image: value.image,
                        cacheType: .none,
                        source: source,
                        originalSource: context.originalSource
                    )
                    completionHandler?(.success(result))
                }
            }
        }

        coordinator.apply(.cacheInitiated) {
            let result = RetrieveImageResult(
                image: value.image,
                cacheType: .none,
                source: source,
                originalSource: context.originalSource
            )
            completionHandler?(.success(result))
        }

    case .failure(let error):
        completionHandler?(.failure(error))
    }
}
複製代碼

在option配置裏,獲取到 targetCache / originalCache(本質都是ImageCache),來執行圖片的緩存;緩存圖片以後,執行回調是經過生成緩存回調協做器(CacheCallbackCoordinator)完成在緩存初始化/緩存原始圖片/緩存中/的情形下執行緩存回調邏輯。

傳入的配置信息 - KingfisherParsedOptionsInfo

KingfisherParsedOptionsInfo實際是KingfisherOptionsInfoItem的結構體形式,便於生成控制Kingfisher中,加載網絡圖片,緩存等行爲的配置信息。

緩存回調協做器 - CacheCallbackCoordinator

緩存回調協做器內部會更根據當前的緩存狀態與操做來斷定,是否觸發triggertrigger是空閉包,傳入緩存圖片成功後的邏輯進行處理。協做器apply方法內部的Switch語句使用了元組來作判斷。

func apply(_ action: Action, trigger: () -> Void) {
        switch (state, action) {
        case (.done, _):
            break

        // From .idle
        case (.idle, .cacheInitiated):
            if !shouldWaitForCache {
                state = .done
                trigger()
            }
        case (.idle, .cachingImage):
            if shouldCacheOriginal {
                state = .imageCached
            } else {
                state = .done
                trigger()
            }
        case (.idle, .cachingOriginalImage):
            state = .originalImageCached

        // From .imageCached
        case (.imageCached, .cachingOriginalImage):
            state = .done
            trigger()

        // From .originalImageCached
        case (.originalImageCached, .cachingImage):
            state = .done
            trigger()

        default:
            assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)")
        }
    }
複製代碼

圖片緩存 - ImageCache

ImageCache是一個單例,內部持有內存存儲(MemoryStorage)和硬盤存儲(DiskStorage)的實例,擔任着真正的將圖片緩存在內存/硬盤的職責。在初始化的時候,內部監聽三個通知

  • UIApplication.didReceiveMemoryWarningNotification - 清理內存緩存
  • UIApplication.willTerminateNotification - 清理過時緩存
  • UIApplication.didEnterBackgroundNotification - 後臺清理過時緩存

ImageCache中的圖片存儲邏輯

當ImageCache調用了store(),其內部會將該圖片存儲在Memory中,而後再判斷是否須要存儲在硬盤中,如須要,就將該張圖片序列化爲Data進行存儲。

open func store(_ image: KFCrossPlatformImage,
                    original: Data? = nil,
                    forKey key: String,
                    options: KingfisherParsedOptionsInfo,
                    toDisk: Bool = true,
                    completionHandler: ((CacheStoreResult) -> Void)? = nil)
    {
        let identifier = options.processor.identifier
        let callbackQueue = options.callbackQueue
        
        let computedKey = key.computedKey(with: identifier)
        // 圖片存儲在內存
        memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
        
        guard toDisk else {
            if let completionHandler = completionHandler {
                let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
                callbackQueue.execute { completionHandler(result) }
            }
            return
        }
        
        ioQueue.async {
        	  //序列化圖片,存儲在硬盤
            let serializer = options.cacheSerializer
            if let data = serializer.data(with: image, original: original) {
                self.syncStoreToDisk(
                    data,
                    forKey: key,
                    processorIdentifier: identifier,
                    callbackQueue: callbackQueue,
                    expiration: options.diskCacheExpiration,
                    completionHandler: completionHandler)
            } else {
                guard let completionHandler = completionHandler else { return }
                
                let diskError = KingfisherError.cacheError(
                    reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))
                let result = CacheStoreResult(
                    memoryCacheResult: .success(()),
                    diskCacheResult: .failure(diskError))
                callbackQueue.execute { completionHandler(result) }
            }
        }
    }

複製代碼

存儲至內存

將圖片存在內存中,調用MemoryStorage實例的storeNoThrow()。存儲過程是經過NSCache來完成的,storage是個NSCache的實例;同時,存儲對應的Key,keys是個String類型的Set集合,在存儲的時候,只須要保證key不重複便可。同時,StorageObject是用於封裝須要存儲的對象。

// MemoryStorage中的storeNoThrow()
	func storeNoThrow(
        value: T,
        forKey key: String,
        expiration: StorageExpiration? = nil)
    {
        lock.lock()
        defer { lock.unlock() }
        let expiration = expiration ?? config.expiration
        // The expiration indicates that already expired, no need to store.
        guard !expiration.isExpired else { return }
        
        let object = StorageObject(value, key: key, expiration: expiration)
        storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
        keys.insert(key)
    }
複製代碼

存儲至硬盤

將圖片存在硬盤中,調用DiskStorage實例的store()。實則是將圖片序列化以後的Data數據寫入文件保存在硬盤中。

// DiskStorage中的store() 
    func store(
        value: T,
        forKey key: String,
        expiration: StorageExpiration? = nil) throws
    {
        let expiration = expiration ?? config.expiration
        // 圖片數據過時,不須要存儲
        guard !expiration.isExpired else { return }
        
        let data: Data
        do {
            data = try value.toData()
        } catch {
            throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
        }
		  // 圖片數據寫入文件
        let fileURL = cacheFileURL(forKey: key)
        do {
            try data.write(to: fileURL)
        } catch {
            throw KingfisherError.cacheError(
                reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)
            )
        }
		
        let now = Date()
        let attributes: [FileAttributeKey : Any] = [
            // 更新建立日期
            .creationDate: now.fileAttributeDate,
            // 更新修改日期
            .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
        ]
        do {
            try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path)
        } catch {
            try? config.fileManager.removeItem(at: fileURL)
            throw KingfisherError.cacheError(
                reason: .cannotSetCacheFileAttribute(
                    filePath: fileURL.path,
                    attributes: attributes,
                    error: error
                )
            )
        }
    }

複製代碼

移除緩存

依據緩存圖片的條件,斷定從內存 / 硬盤位置移除緩存。

open func removeImage(forKey key: String,
                          processorIdentifier identifier: String = "",
                          fromMemory: Bool = true,
                          fromDisk: Bool = true,
                          callbackQueue: CallbackQueue = .untouch,
                          completionHandler: (() -> Void)? = nil)
    {
        let computedKey = key.computedKey(with: identifier)
		  // 從內存中移除
        if fromMemory {
            try? memoryStorage.remove(forKey: computedKey)
        }
        // 從硬盤中移除
        if fromDisk {
            ioQueue.async{
                try? self.diskStorage.remove(forKey: computedKey)
                if let completionHandler = completionHandler {
                    callbackQueue.execute { completionHandler() }
                }
            }
        } else {
            if let completionHandler = completionHandler {
                callbackQueue.execute { completionHandler() }
            }
        }
    }

複製代碼

從內存中移除,只須要經過key在對應的NSCache中移除對象。

func remove(forKey key: String) throws {
        lock.lock()
        defer { lock.unlock() }
        storage.removeObject(forKey: key as NSString)
        keys.remove(key)
    }

複製代碼

從硬盤中移除,經過FileManager移除對應的類目。

func removeFile(at url: URL) throws {
        try config.fileManager.removeItem(at: url)
    }

複製代碼

移除過時緩存

移除過時的緩存,從內存與硬盤上移除對應過時的對象。

從內存中移除過時圖片

func removeExpired() {
    lock.lock()
    defer { lock.unlock() }
    for key in keys {
        let nsKey = key as NSString
        guard let object = storage.object(forKey: nsKey) else {
            keys.remove(key)
            continue
        }
        if object.estimatedExpiration.isPast {
            storage.removeObject(forKey: nsKey)
            keys.remove(key)
        }
    }
}

複製代碼

從硬盤中移除過時圖片 在ImageCache中,移除過時圖片,經過調用內部持有DiskStorage實例的removeExpiredValues()完成。

open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) {
    ioQueue.async {
        do {
            var removed: [URL] = []
            let removedExpired = try self.diskStorage.removeExpiredValues()
            removed.append(contentsOf: removedExpired)

            let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues()
            removed.append(contentsOf: removedSizeExceeded)

            if !removed.isEmpty {
                DispatchQueue.main.async {
                    let cleanedHashes = removed.map { $0.lastPathComponent }
                    NotificationCenter.default.post(
                        name: .KingfisherDidCleanDiskCache,
                        object: self,
                        userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
                }
            }

            if let handler = handler {
                DispatchQueue.main.async { handler() }
            }
        } catch {}
    }
}

複製代碼

DiskStorage中的removeExpiredValues();先查找到全部緩存文件的URL,再獲取到過時的文件(去除文件夾)移除。

func removeExpiredValues(referenceDate: Date = Date()) throws -> [URL] {
    let propertyKeys: [URLResourceKey] = [
        .isDirectoryKey,
        .contentModificationDateKey
    ]

    let urls = try allFileURLs(for: propertyKeys)
    let keys = Set(propertyKeys)
    let expiredFiles = urls.filter { fileURL in
        do {
            let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys)
            if meta.isDirectory {
                return false
            }
            return meta.expired(referenceDate: referenceDate)
        } catch {
            return true
        }
    }
    try expiredFiles.forEach { url in
        try removeFile(at: url)
    }
    return expiredFiles
}

複製代碼
// 獲取全部文件的URL
func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] {
    let fileManager = config.fileManager

    guard let directoryEnumerator = fileManager.enumerator(
        at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else
    {
        throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL))
    }

    guard let urls = directoryEnumerator.allObjects as? [URL] else {
        throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL))
    }
    return urls
}
複製代碼

網絡細節

在前面的講述中,提到在使用kf.setImage()設置網絡圖片的時候都是經過將URL裝飾成一個的下載任務(DownloadTask)。

public struct DownloadTask {

    /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer
    /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task
    /// for the same URL resource at the same time.
    ///
    /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through.
    /// You can use them to identify the cancelled task.
    public let sessionTask: SessionDataTask

    /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled.
    /// To cancel a `DownloadTask`, use `cancel` instead.
    public let cancelToken: SessionDataTask.CancelToken

    /// Cancel this task if it is running. It will do nothing if this task is not running.
    ///
    /// - Note:
    /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being
    /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created
    /// and returned when you call related methods, but it will share the session downloading task with a previous task.
    /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask`
    /// does not affect other `DownloadTask`s.
    ///
    /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel
    /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`.
    public func cancel() {
        sessionTask.cancel(token: cancelToken)
    }
}

複製代碼

很顯然,DownloadTask中真正核心的內容在SessionDataTask,並且DownloadTask是個結構體,SessionDataTask是個類。

會話數據任務 - SessionDataTask

SessionDataTask管理每一個要進行下載的URLSessionDataTask;接收下載的數據;取消下載任務時,同時取消對應的回調。SessionDataTask的屬性就已說明了這些。因此真正在圖片下載任務執行過程當中擔任主角的是SessionDataTask。

/// 當前任務下載的數據.
    public private(set) var mutableData: Data

    /// 當前下載任務
    public let task: URLSessionDataTask
    /// 回調容器
    private var callbacksStore = [CancelToken: TaskCallback]()

    var callbacks: [SessionDataTask.TaskCallback] {
        lock.lock()
        defer { lock.unlock() }
        return Array(callbacksStore.values)
    }
	 /// 當前任務Token,也至關於身份ID。
    private var currentToken = 0
    /// 鎖
    private let lock = NSLock()
	 /// 下載任務結束Delegate
    let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>()
    /// 取消回調Delegate。
    let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()

複製代碼

取消下載任務,經過當前下載任務的CancelToken,先移除這個下載任務的回調,再調用cancel()方法取消。

func cancel(token: CancelToken) {
    guard let callback = removeCallback(token) else {
        return
    }
    if callbacksStore.count == 0 {
        task.cancel()
    }
    onCallbackCancelled.call((token, callback))
}
複製代碼

圖片下載器 - ImageDownloader

圖片的下載是經過URLSession完成的。建立一個URLSession,須要指定URLSessionConfiguration。因爲在每一個會話數據任務中,都不是固定某種會話類型。因此這裏使用URLSessionConfiguration.ephemeral(非持久化類型的URLSessionConfiguration)。建立了圖片下載器後,調用downloadImage()開啓下載。在這個方法裏,作了以下幾個事情:

  • 建立URLRequest
  • 設置下載任務完成後的回調
  • 將下載任務DownloadTask添加進SessionDelegate進行管理,並開啓下載任務
  • 下載任務結果處理
open func downloadImage(
        with url: URL,
        options: KingfisherParsedOptionsInfo,
        completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
    {
        // 建立默認的Request
        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
        request.httpShouldUsePipelining = requestsUsePipelining

        if let requestModifier = options.requestModifier {
            guard let r = requestModifier.modified(for: request) else {
                options.callbackQueue.execute {
                    completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest)))
                }
                return nil
            }
            request = r
        }
        
        // url判空(有可能url傳入了nil)
        guard let url = request.url, !url.absoluteString.isEmpty else {
            options.callbackQueue.execute {
                completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request))))
            }
            return nil
        }

        // 裝飾onCompleted / completionHandler
        let onCompleted = completionHandler.map {
            block -> Delegate<Result<ImageLoadingResult, KingfisherError>, Void> in
            let delegate =  Delegate<Result<ImageLoadingResult, KingfisherError>, Void>()
            delegate.delegate(on: self) { (_, callback) in
                block(callback)
            }
            return delegate
        }

        let callback = SessionDataTask.TaskCallback(
            onCompleted: onCompleted,
            options: options
        )

		  // 準備下載
        let downloadTask: DownloadTask
        if let existingTask = sessionDelegate.task(for: url) {
            downloadTask = sessionDelegate.append(existingTask, url: url, callback: callback)
        } else {
            let sessionDataTask = session.dataTask(with: request)
            sessionDataTask.priority = options.downloadPriority
            downloadTask = sessionDelegate.add(sessionDataTask, url: url, callback: callback)
        }

        let sessionTask = downloadTask.sessionTask

        // 由sessionTask開始下載
        if !sessionTask.started {
            sessionTask.onTaskDone.delegate(on: self) { (self, done) in
                // 下載完成後回調
                // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
                let (result, callbacks) = done

                // 處理下載的數據前,Delegate處理邏輯
                do {
                    let value = try result.get()
                    self.delegate?.imageDownloader(
                        self,
                        didFinishDownloadingImageForURL: url,
                        with: value.1,
                        error: nil
                    )
                } catch {
                    self.delegate?.imageDownloader(
                        self,
                        didFinishDownloadingImageForURL: url,
                        with: nil,
                        error: error
                    )
                }

                switch result {
                // 下載成功,處理下載的數據轉化爲圖片
                case .success(let (data, response)):
                    let processor = ImageDataProcessor(
                        data: data, callbacks: callbacks, processingQueue: options.processingQueue)
                    processor.onImageProcessed.delegate(on: self) { (self, result) in
                        // result: Result<Image>, callback: SessionDataTask.TaskCallback
                        let (result, callback) = result

                        if let image = try? result.get() {
                            self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
                        }

                        let imageResult = result.map { ImageLoadingResult(image: $0, url: url, originalData: data) }
                        let queue = callback.options.callbackQueue
                        queue.execute { callback.onCompleted?.call(imageResult) }
                    }
                    processor.process()
				   // 下載失敗,拋出異常
                case .failure(let error):
                    callbacks.forEach { callback in
                        let queue = callback.options.callbackQueue
                        queue.execute { callback.onCompleted?.call(.failure(error)) }
                    }
                }
            }
            delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
            sessionTask.resume()
        }
        return downloadTask
    }

複製代碼

圖片下載器的任務管理者 - SessionDelegate

這是下載任務的直接管理者。產生的下載任務均是存放在SessionDelegate實例的tasks字典中,這個字典是以url爲key,DownloadTask爲value。因此,當重複調用kf.setImage(),只要是URL是同一個,任務不會被重複添加。只是會更新它的回調與CancelToken。簡而言之,同一個URL的圖片下載任務是相同的,不更新;可是,它的回調可能改變了,須要更新。

SessionDelegate中將添加單個Task到tasks字典。

func add(
    _ dataTask: URLSessionDataTask,
    url: URL,
    callback: SessionDataTask.TaskCallback) -> DownloadTask
{	  
	  // 加鎖
    lock.lock()
    defer { lock.unlock() }

    // 建立新的SessionDataTask
    let task = SessionDataTask(task: dataTask)
    // 配置這個sessionDataTask的取消操做
    task.onCallbackCancelled.delegate(on: self) { [unowned task] (self, value) in
        let (token, callback) = value

        let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token))
        task.onTaskDone.call((.failure(error), [callback]))
        // No other callbacks waiting, we can clear the task now.
        if !task.containsCallbacks {
            let dataTask = task.task
            self.remove(dataTask)
        }
    }
    // 回調添加進SessionDelegate
    let token = task.addCallback(callback)
    // 添加task到tasks字典中
    tasks[url] = task
    return DownloadTask(sessionTask: task, cancelToken: token)
}

複製代碼

當tasks字典中存在某個下載任務,後續再有相同的下載任務,只更新與下載任務搭配的回調。

func append(_ task: SessionDataTask, url: URL, callback: SessionDataTask.TaskCallback) -> DownloadTask {
	 // 將回調更新到回調存儲器裏並獲取到最新的CancelToken
    let token = task.addCallback(callback)
    return DownloadTask(sessionTask: task, cancelToken: token)
}

複製代碼

將回調添加到回調存儲容器裏的邏輯。

func addCallback(_ callback: TaskCallback) -> CancelToken {
    lock.lock()
    defer { lock.unlock() }
    callbacksStore[currentToken] = callback
	 // 將Token+1以保持最新
    defer { currentToken += 1 }
    return currentToken
}

複製代碼

下載任務管理者中,移除某個下載任務;直接將tasks字典中,url對應的值設置爲nil。

private func remove(_ task: URLSessionTask) {
    guard let url = task.originalRequest?.url else {
        return
    }
    lock.lock()
    defer {lock.unlock()}
    tasks[url] = nil
}
複製代碼

至此,能夠看出圖片的下載最終是經過URLSessionDataTask,來開啓。複雜的是在於,給UIImageView,UIButton,NSButton等對象設置圖片的時候,統一的經過一個方法setImage(),就將圖片的下載,緩存,重複調用等邏輯組合在一塊兒,最終完成加載網絡圖片的過程。

避免循環引用 - Delegate<Input, Output>

在SessionDataTask中,能夠看到兩個屬性onTaskDoneonCallbackCancelled。它們的類型都是Delegate<Input, Output>

let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>()
let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()

複製代碼

這個類型,其實定義了一箇中間類型;在初始化了實例以後,經過delegate()將外部方法中須要完成的操做 / 回調,傳入它的屬性block。

/// A delegate helper type to "shadow" weak `self`, to prevent creating an unexpected retain cycle.
class Delegate<Input, Output> {
    init() {}
    
    private var block: ((Input) -> Output?)?
    
    func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {
        // The `target` is weak inside block, so you do not need to worry about it in the caller side.
        self.block = { [weak target] input in
            guard let target = target else { return nil }
            return block?(target, input)
        }
    }
    
    func call(_ input: Input) -> Output? {
        return block?(input)
    }
}

extension Delegate where Input == Void {
    // To make syntax better for `Void` input.
    func call() -> Output? {
        return call(())
    }
}

複製代碼

在SessionDataTask的下載任務完成後,會調用onTaskDone的call()。onTaskDone的delegate()是在ImageDownloader實例的downloadImage()中調用的,也就是將下載任務完成後的操做,在downloadImage()中傳遞給了onTaskDone。

// ImageDownloader的downloadImage()
// 開始下載
if !sessionTask.started {
    // 下載完成後回調操做傳遞給onTaskDone。
    sessionTask.onTaskDone.delegate(on: self) { (self, done) in
        // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback]
        let (result, callbacks) = done

        // 處理下載的數據前,Delegate處理邏輯
        do {
            let value = try result.get()
            self.delegate?.imageDownloader(
                self,
                didFinishDownloadingImageForURL: url,
                with: value.1,
                error: nil
            )
        } catch {
            self.delegate?.imageDownloader(
                self,
                didFinishDownloadingImageForURL: url,
                with: nil,
                error: error
            )
        }

        switch result {
        // 下載成功,處理下載的數據轉化爲圖片
        case .success(let (data, response)):
            let processor = ImageDataProcessor(
                data: data, callbacks: callbacks, processingQueue: options.processingQueue)
            processor.onImageProcessed.delegate(on: self) { (self, result) in
                // result: Result<Image>, callback: SessionDataTask.TaskCallback
                let (result, callback) = result

                if let image = try? result.get() {
                    self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
                }

                let imageResult = result.map { ImageLoadingResult(image: $0, url: url, originalData: data) }
                let queue = callback.options.callbackQueue
                queue.execute { callback.onCompleted?.call(imageResult) }
            }
            processor.process()
		   // 下載失敗,拋出異常
        case .failure(let error):
            callbacks.forEach { callback in
                let queue = callback.options.callbackQueue
                queue.execute { callback.onCompleted?.call(.failure(error)) }
            }
        }
    }
    delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
    sessionTask.resume()

}

複製代碼

斷言與錯誤

斷言是在不指望的情形出現時,中止程序運行。

  1. 在CacheCallbackCoordinator的trigger()觸發判斷裏,使用assertionFailure斷言
assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)")

複製代碼
  1. 用了enum類型的KingfisherError,作爲框架中使用的錯誤類型;且KingfisherError做爲錯誤類型名空間,內部詳細定義了會出現的錯誤類型RequestErrorReasonResponseErrorReasonCacheErrorReasonProcessorErrorReasonImageSettingErrorReason。同時與NSError橋接,遵循了LocalizedErrorCustomNSError協議。

值得關注的點

  1. 在Kingfisher框架中沒有看到繼承體系。即便是要實現給系統類UIImageView,UIButton,NSButton增長額外屬性或方法的時候,是經過結構體封裝這些類和關聯對象間接實現的。
  2. 在多重閉包中,避免循環引用,能夠新增一箇中間類;將要完成的操做傳遞給中間類的實例。

問題

在某個圖片未被成功下載前,重複調用kf.setImage(),會有什麼效果?

這種狀況會在UITableViewCell / UICollectionViewCell中出現,當某個圖片沒有被緩存時,快速上下滑動。會出現下載任務(DownloadTask)會屢次建立,可是與圖片相關的SessionDataTask,同一個URL,只會建立1次。一旦圖片下載成功並緩存了之後,再調用setImage(),就不會產生新的下載任務(DownloadTask),同時也不會建立SessionDataTask

參考連接

Error與NSError的關係:www.jianshu.com/p/a36047852…

Kingfisher:github.com/onevcat/Kin…

相關文章
相關標籤/搜索