preface

When using third-party libraries, they tend to focus only on their functions and pay little attention to the implementation details behind them. In this paper, from the source code (Kingfisher V5.13.0) perspective, learning and studying the implementation details behind Kingfisher.

Kf property

Kf is the calculation property that returns the KingfisherWrapper that wraps the system classes UIIMageView, UIButton, and NSButton, which decorates the generic Base with structs.

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{}}}Copy the code

setImage()

So when you set an image, UIImageView, UIButton, NSButton all call setImage(). The core function of this method is to use the retrieveImage of the KingfisherManager to generate the download task and store the download task in the imageTask property of the KingfisherWrapper of the current instance. But the imageTask property is not defined in KingfisherWrapper. How is this done? Associated objects are used for dynamic access.

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)
        returnbox? .value }set {
        let box = newValue.map { Box($0)}setRetainedAssociatedObject(base, &taskIdentifierKey, box)
    }
}
Copy the code

Each download task has its own Identifier, which is also dynamically set to a KingfisherWrapper property via the associated object. The associated object must be of a memory-managed type, so the Identifier of type UInt is encapsulated in the Box class, indirectly completing the Identifier setting.

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

Copy the code

The setImage() method is provided throughout Kingfisher for external object import using load network images.

@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 //sourceSentenced to empty guardlet source = source else{ mutatingSelf.placeholder = placeholder mutatingSelf.taskIdentifier = nil completionHandler? (.failure(KingfisherError.imageSettingError(reason: .emptySource)))returnNil} / var/loading configuration options = KingfisherParsedOptionsInfo (KingfisherManager. Shared. DefaultOptions + (options??. The empty))let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
    if! options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet { // Alwaysset placeholder while there is no image/placeholder yet.
        mutatingSelf.placeholder = placeholder
    }

    letmaybeIndicator = indicator maybeIndicator? .startAnimatingView() // Task Identifier is incremented to ensure that each time this method is called, a new load task is created.let issuedIdentifier = Source.Identifier.next()
    mutatingSelf.taskIdentifier = issuedIdentifier

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

    if letblock = progressBlock { options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect (block)]} / / pictures if they are supplied with the Provider, direct access.if let provider = ImageProgressiveProvider(options, refresh: { image inself.base.image = image }) { options.onDataReceived = (options.onDataReceived ?? []) + [provider] } options.onDataReceived? .forEach {$0.onshouldapply = {issuedIdentifier == self.taskIdentifier}} // if not, execute KingfisherManager to load the picture.let task = KingfisherManager.shared.retrieveImage(
        with: source,
        options: options,
        downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
        completionHandler: { result in/ / success obtained by pictures, execute callback CallbackQueue. MainCurrentOrAsync. Execute {/ / indicator stop animation maybeIndicator? .stopAnimatingView() guard issuedIdentifier == self.taskIdentifierelse {
                    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)}leterror = KingfisherError.imageSettingError(reason: reason) completionHandler? (.failure(error))return
                }
                
                mutatingSelf.imageTask = nil
                mutatingSelf.taskIdentifier = nil
                
                switch result {
                case .success(letGuard self.needsTransition(options: options, cacheType: value.cacheType) : // Transition guard self.needsTransition(options: options, cacheType: value.cachetype)else{ mutatingSelf.placeholder = nil self.base.image = value.image completionHandler? (result)return} // Execute Transition and update the image self.maketransition (image: value.image, Transition: options.transition) {completionHandler? (result) }case .failure:
                    if letimage = options.onFailureImage { self.base.image = image } completionHandler? (result) } } } ) mutatingSelf.imageTask = taskreturn task
}

Copy the code

Manager – KingfisherManager

Get photo

The logic from setImage() to whether to perform the requested image download is handled by the KingfisherManager’s retrieveImage(). The processing logic of retrieveImage() is:

  1. Determine whether the image needs to be forcibly refreshed, and if so, execute the loadAndCacheImage method to generate the download task
  2. The image exists in the cache and does not force a refresh, and the current retrieveImage method returns nil and no new download task is generated
  3. Image does not exist in cache and is not forced to refresh; Generate download tasks and cache images.
private func retrieveImage(
    with source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
{
    letOptions = context.options // To determine whether to force a refresh, run the loadAndCacheImage method to generate a download taskif options.forceRefresh {
        return loadAndCacheImage(
            source: source, context: context, completionHandler: completionHandler)? .value }else{// load from cachelet loadedFromCache = retrieveImageFromCache(
            source: source, context: context, completionHandler: completionHandler) // If the image is in the cache, nil is currently returned, no download task is generated, and execution is completeif loadedFromCache {
            returnNil} // Image not found in cache, but configured to read only images from cache, Error is raised.if options.onlyFromCache {
            leterror = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) completionHandler? (.failure(error))return nil
        }
        
        return loadAndCacheImage(
            source: source, context: context, completionHandler: completionHandler)? .value } }Copy the code

As you can see, in either case, just call the kf.setimage () method to set the image; Image downloads, caching, and whether or not you need to turn on a network request to download images are all hidden under this line of code.

Retrieves the image from the cache – retrieveImageFromCache

The retrieveImageFromCache() contains the retrieval of images from memory/disk and returns a Bool indicating whether or not the image is in the cache.

func retrieveImageFromCache(
        source: Source,
        context: RetrievingContext,
        completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool
    {
        letoptions = context.options // 1. Determines whether the image already exists in the target cachelet 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)
        ifValidCache {/ / get photo 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. Check whether the original image has been cached, if there are cached images, then return directly.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} // Check for unprocessed imageslet originalImageCacheType = originalCache.imageCachedType(
            forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier)
        letcanAcceptDiskCache = ! options.fromMemoryCacheOrRefreshletcanUseOriginalImageCache = (canAcceptDiskCache && originalImageCacheType.cached) || (! canAcceptDiskCache && originalImageCacheType == .memory)ifCanUseOriginalImageCache {// Find the cached image, Processing data for the original 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 {
                                leterror = KingfisherError.processorError( reason: .processingFailed(processor: processor, item: item)) options.callbackQueue.execute { completionHandler? (.failure(error)) }return} var cacheOptions = options cacheOptions. CallbackQueue =. Untouch / / callback collaboratorlet coordinator = CacheCallbackCoordinator(
                                shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false// cache the processedImage targetcache.store (procescacheage,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/ / failure callback options. CallbackQueue. Execute {completionHandler? ( .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) ) } } ) }return true
        }

        return false
    }

Copy the code

Load and cache images from the network – loadAndCacheImage

When will images be loaded and cached from the network? There are two cases:

  • The image does not exist in cache (memory/hard disk) when setting up the image.
  • When setting a picture, force refreshing the picture data.

The loadAndCacheImage method makes a call. Determine whether the current image needs to be downloaded from the network (obtain the image from the network, perform the download, and cache the image) or obtain the image through the ImageProvider (directly obtain the image and cache the image), and generate the corresponding download task.

@discardableResult
func loadAndCacheImage(
    source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
{
    letOptions = context.options // define nested function func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) { cacheImage(source: source,
            options: options,
            context: context,
            result: result,
            completionHandler: completionHandler
        )
    }
    
    switch source{// Load images from the networkcase .network(let resource):
        let downloader = options.downloader ?? self.downloader
        let task = downloader.downloadImage(
            with: resource.downloadURL, options: options, completionHandler: _cacheImage
        )
        returnTask. The map (DownloadTask. WrappedTask. Download) / / loading images from ImageProvidercase .provider(let provider):
        provideImage(provider: provider, options: options, completionHandler: _cacheImage)
        return .dataProviding
    }
}

Copy the code

Cache details

Cache logic in loadAndCacheImage

In loadAndCacheImage, the _cacheImage() inline function is executed after the image download task is performed. _cacheImage An internal cacheImage is executed. Implementation of 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):
        letneedToCacheOriginalImage = options.cacheOriginalImage && options.processor ! = DefaultImageProcessor.defaultletcoordinator = CacheCallbackCoordinator( shouldWaitForCache: options.waitForCache, shouldCacheOriginal: NeedToCacheOriginalImage) // Cache imageslet 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))}} // Determine whether to cache the original imageif 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(leterror): completionHandler? (.failure(error)) } }Copy the code

In the option configuration, get targetCache/originalCache (essentially ImageCache) to perform image caching; After the image is cached, the callback is performed by generating the CacheCallbackCoordinator (CacheCallbackCoordinator) to perform the cache callback logic in the case of cache initialization/caching of the original image/in the cache /.

The incoming – KingfisherParsedOptionsInfo configuration information

KingfisherParsedOptionsInfo actually KingfisherOptionsInfoItem form of structure, easy to generate control Kingfisher, load network images, cache behavior such as configuration information.

CacheCallbackCoordinator – CacheCallbackCoordinator

The cache callback collaborator will determine whether to trigger trigger according to the current cache state and operation. Trigger is an empty closure, which will be processed by the logic after the successful passing of the cached picture. The Switch statement inside the apply method of the collaborator uses tuples for judgment.

    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)")}}Copy the code

ImageCache – ImageCache

ImageCache is a singleton that internally holds MemoryStorage and DiskStorage instances and is responsible for the actual caching of images in memory/disk. At initialization, three notifications are listened for internally

  • UIApplication. DidReceiveMemoryWarningNotification – clear the memory cache
  • UIApplication. WillTerminateNotification – clear cache date
  • UIApplication. DidEnterBackgroundNotification clean up overdue cache – the background

Image storage logic in ImageCache

When the ImageCache calls Store (), the ImageCache internally stores the image in Memory and determines whether it needs to be stored on hard disk. If so, the image is serialized as Data for storage.

    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
        
        letComputedKey = key.com putedKey (with: identifier) / / picture memoryStorage stored in memory. The 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) }
            }
            returnIoqueue.async {// serialize images to disklet 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) }
            }
        }
    }

Copy the code

Store to memory

To store the image in memory, call storeNoThrow() of the MemoryStorage instance. Storage is an instance of NSCache. At the same time, store corresponding keys. Keys is a Set of String type. When storing keys, you only need to ensure that the keys are not duplicated. Meanwhile, a StorageObject is used to encapsulate objects that need to be stored.

// storeNoThrow() func storeNoThrow(value: T,forKey key: String,
        expiration: StorageExpiration? = nil)
    {
        lock.lock()
        defer { lock.unlock() }
        letexpiration = expiration ?? config.expiration // The expiration indicates that already expired, no need to store. guard ! expiration.isExpiredelse { return }
        
        let object = StorageObject(value, key: key, expiration: expiration)
        storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
        keys.insert(key)
    }
Copy the code

Storage to Hard Disk

To save the image to the disk, call store() of the DiskStorage instance. In fact, the Data Data after the serialization of the picture is written into a file and saved in the hard disk.

Func store(value: T,forKey key: String,
        expiration: StorageExpiration? = nil) throws
    {
        letexpiration = expiration ?? Config. expiration // No need to store guard! expiration.isExpiredelse { return }
        
        let data: Data
        do{ data = try value.toData() } catch { throw KingfisherError.cacheError(reason: .cannotConvertToData(object: Value, error: error))} // Write image data to filelet 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()
        letAttributes: [FileAttributeKey: Any] = [// Update creation date.creationDate: now. FileAttributeDate, // Update modified date.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
                )
            )
        }
    }

Copy the code

Remove the cache

Decide to remove cache from memory/hard disk location according to the condition of cached image.

    open func removeImage(forKey key: String,
                          processorIdentifier identifier: String = "",
                          fromMemory: Bool = true,
                          fromDisk: Bool = true,
                          callbackQueue: CallbackQueue = .untouch,
                          completionHandler: (() -> Void)? = nil)
    {
        letComputedKey = key.putedKey (with: Identifier) // Removed from memoryif fromMemory {
            try? memoryStorage.remove(forKey: computedKey)} // Remove from the hard diskif fromDisk {
            ioQueue.async{
                try? self.diskStorage.remove(forKey: computedKey)
                if let completionHandler = completionHandler {
                    callbackQueue.execute { completionHandler() }
                }
            }
        } else {
            if let completionHandler = completionHandler {
                callbackQueue.execute { completionHandler() }
            }
        }
    }

Copy the code

To remove an object from memory, use the key to remove the object in the corresponding NSCache.

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

Copy the code

Remove a class from the hard disk. Use FileManager to remove the corresponding category.

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

Copy the code

Removing an expired cache

Remove expired caches and objects from memory and hard disk.

Removes expired images from memory

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)
        }
    }
}

Copy the code

Remove expired images from disk In ImageCache, remove expired images by calling removeExpiredValues(), which holds the DiskStorage instance internally.

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 {}
    }
}

Copy the code

RemoveExpiredValues () in DiskStorage; First find the URL of all cache files, and then obtain expired files (remove folders) remove.

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
}

Copy the code
// Get the 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
}
Copy the code

The network details

In the previous section, it was mentioned that setting up web images using kf.setimage () is done by decorating the URL as a 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.ifmultiple `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 funccancel() {
        sessionTask.cancel(token: cancelToken)
    }
}

Copy the code

Obviously, the real core of DownloadTask is in SessionDataTask, and DownloadTask is a structure, and SessionDataTask is a class.

SessionDataTask – SessionDataTask

SessionDataTask manages each URLSessionDataTask to be downloaded; Receive downloaded data; When you cancel the download task, cancel the corresponding callback. The SessionDataTask attribute illustrates this. So it’s SessionDataTask that really plays the leading role in the image download task execution.

/// Data downloaded by the current task public private(setVar mutableData: Data /// the current download task publicletTask: URLSessionDataTask private var callbacksStore = [CancelToken: TaskCallback]() var callbacks: [SessionDataTask.TaskCallback] { lock.lock() defer { lock.unlock() }returnArray(callbacksStore.values)} /// The current task Token, also equivalent to the identity ID. Private var currentToken = 0 /// Lock privateletLock = NSLock() /// Download task end DelegateletonTaskDone = Delegate<(Result<(Data, URLResponse?) , KingfisherError>, [TaskCallback]), Void>() /// cancel the callback Delegate.let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>()

Copy the code

CancelToken CancelToken CancelToken CancelToken CancelToken CancelToken CancelToken CancelToken CancelToken CancelToken

func cancel(token: CancelToken) {
    guard let callback = removeCallback(token) else {
        return
    }
    if callbacksStore.count == 0 {
        task.cancel()
    }
    onCallbackCancelled.call((token, callback))
}
Copy the code

ImageDownloader – ImageDownloader

Images are downloaded through URLSession. To create an URLSession, specify the URLSessionConfiguration. There is no fixed session type in each session data task. So here use URLSessionConfiguration. Ephemeral (non-persistent type URLSessionConfiguration). Once the image loader is created, call downloadImage() to start the download. In this method, the following things are done:

  • Create a URLRequest
  • Set the callback after the download task is complete
  • Add the DownloadTask to the SessionDelegate for management and enable the DownloadTask
  • Download task result processing
open func downloadImage( with url: URL, options: KingfisherParsedOptionsInfo, completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? {/ / create the default Request var Request = URLRequest (url: the url, cachePolicy: reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) request.httpShouldUsePipelining = requestsUsePipeliningif let requestModifier = options.requestModifier {
            guard let r = requestModifier.modified(for: request) else{ options.callbackQueue.execute { completionHandler? (.failure(KingfisherError.requestError(reason: .emptyRequest))) }returnNil} request = r} // URL nulls (maybe the URL was passed nil) guardleturl = request.url, ! url.absoluteString.isEmptyelse{ options.callbackQueue.execute { completionHandler? (.failure(KingfisherError.requestError(reason: .invalidURL(request: request)))) }returnNil} // Decorate onCompleted/completionHandlerlet 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
        }

        letCallback = SessionDataTask. TaskCallback (onCompleted: onCompleted, options, options) / / ready to downloadlet 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)
        }

        letSessionTask = downloadTask.sessionTask // Start from sessionTask to downloadif! sessionTask.started { sessionTask.onTaskDone.delegate(on: self) { (self,done) in// result: result <(Data, URLResponse?) >, callbacks: [TaskCallback]let (result, callbacks) = done// Delegate handles the logic before processing the downloaded datado {
                    letvalue = 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 {// Download succeeded, process the downloaded data into imagescase .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) }
                        letqueue = callback.options.callbackQueue queue.execute { callback.onCompleted? .call(imageResult)}} processor.process() // Download failed, throw an exceptioncase .failure(let error):
                    callbacks.forEach { callback in
                        letqueue = callback.options.callbackQueue queue.execute { callback.onCompleted? .call(.failure(error)) } } } } delegate? .imageDownloader(self, willDownloadImageForURL: url, with: request) sessionTask.resume() }return downloadTask
    }

Copy the code

Image loader task manager – SessionDelegate

This is the direct manager of the download task. The generated download tasks are stored in the Tasks dictionary of the SessionDelegate instance, which uses the URL as the key and the DownloadTask as the value. So, when kf.setimage () is called repeatedly, as long as the URL is the same, the task is not added twice. It simply updates its callbacks with CancelToken. In short, the image download task of the same URL is the same and does not update; However, its callback may have changed and need to be updated.

Add a single Task to the Tasks dictionary in the SessionDelegate.

func add( _ dataTask: URLSessionDataTask, url: URL, callback: SessionDataTask. TaskCallback) - > DownloadTask {/ / lock lock. The lock () defer {. Lock unlock ()} / / create a new SessionDataTaskletTask = SessionDataTask (task: dataTask) / / configure the SessionDataTask cancel the operation 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 {letDataTask = task.task self.remove(dataTask)}} // Add callback to SessionDelegateletToken = task.addCallback(callback) // Add task to tasks dictionary tasks[url] = taskreturn DownloadTask(sessionTask: task, cancelToken: token)
}

Copy the code

When a download task exists in the Tasks dictionary and the same download task is followed, only the callback associated with the download task is updated.

func append(_ task: SessionDataTask, url: URL, callback: SessionDataTask. TaskCallback) - > DownloadTask {/ / callback to update to the callback and get the latest CancelToken in memorylet token = task.addCallback(callback)
    return DownloadTask(sessionTask: task, cancelToken: token)
}

Copy the code

The logic that adds the callback to the callback storage container.

func addCallback(_ callback: TaskCallback) -> CancelToken { lock.lock() defer { lock.unlock() } callbacksStore[currentToken] = callback // Add Token+1 to keep defer up to date {currentToken += 1}return currentToken
}

Copy the code

Remove a download task from the download task Manager. Set the value of the URL in the Tasks dictionary to nil.

private func remove(_ task: URLSessionTask) {
    guard leturl = task.originalRequest? .urlelse {
        return
    }
    lock.lock()
    defer {lock.unlock()}
    tasks[url] = nil
}
Copy the code

At this point, you can see that the image download is finally started with the URLSessionDataTask. The complication is that when setting images for UIImageView, UIButton, NSButton and other objects, setImage() is used uniformly to combine the logic of image downloading, caching, and repeated calls, and finally complete the process of loading network images.

Avoid circular references – Delegate<Input, Output>

In SessionDataTask, you can see two properties onTaskDone and onCallbackCancelled. They are both of type Delegate

.
,>

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

Copy the code

This type, in fact, defines an intermediate type; After the instance is initialized, the delegate() passes in the external method’s property block the actions/callbacks that need to be done.

/// 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 youdo not need to worry about it in the caller side.
        self.block = { [weak target] input in
            guard let target = target else { return nil }
            returnblock? (target, input) } } func call(_ input: Input) -> Output? {returnblock? (input) } } extension Delegatewhere Input == Void {
    // To make syntax better for `Void` input.
    func call() -> Output? {
        return call(())
    }
}

Copy the code

OnTaskDone call() is called after the SessionDataTask download task is complete. OnTaskDone’s delegate() is called in the downloadImage() of the ImageDownloader instance, which passes the downloaded action to onTaskDone in downloadImage().

// ImageDownloader downloadImage() // start downloadingif! Sessiontask. started {// After downloading, the callback is passed to onTaskDone. sessionTask.onTaskDone.delegate(on: self) { (self,done) in// result: Result<(Data, URLResponse?) >, callbacks: [TaskCallback]let (result, callbacks) = done// Delegate handles the logic before processing the downloaded datado {
            letvalue = 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 {// Download succeeded, process the downloaded data into imagescase .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) }
                letqueue = callback.options.callbackQueue queue.execute { callback.onCompleted? .call(imageResult)}} processor.process() // Download failed, throw an exceptioncase .failure(let error):
            callbacks.forEach { callback in
                letqueue = callback.options.callbackQueue queue.execute { callback.onCompleted? .call(.failure(error)) } } } } delegate? .imageDownloader(self, willDownloadImageForURL: url, with: request) sessionTask.resume() }Copy the code

Assertions and errors

Assertions stop a program when an unexpected situation occurs.

  1. In the CacheCallbackCoordinator trigger() check, useassertionFailureassertions
assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)")

Copy the code
  1. KingfisherError of type enum is used as the error type used in the framework; And KingfisherError, as the error type namespace, internally defines the types of errors that can occurRequestErrorReason.ResponseErrorReason.CacheErrorReason.ProcessorErrorReason.ImageSettingErrorReason. Also bridge with NSError, followedLocalizedErrorwithCustomNSErrorThe agreement.

Points of concern

  1. Inheritance is not seen in the Kingfisher framework. Even when you add extra properties or methods to the system classes UIImageView, UIButton, and NSButton, you do it indirectly by encapsulating those classes and their associated objects in a structure.
  2. In multiple closures, instead of circular references, you can add an intermediate class; The operation to be done is passed to an instance of the intermediate class.

The problem

What happens if you call kf.setimage () repeatedly before an image is successfully downloaded?

This happens in the UITableViewCell/UICollectionViewCell, which slides up and down quickly when an image is not cached. The DownloadTask will be created multiple times, but the image related SessionDataTask will only be created once for the same URL. Once the image has been downloaded and cached, setImage() is called again. No new DownloadTask is created and no SessionDataTask is created.

Refer to the link

The relationship between the Error and NSError: www.jianshu.com/p/a36047852…

Kingfisher:github.com/onevcat/Kin…