Swift开发实战Swift 入门到入门

Kingfisher学习笔记

2017-07-06  本文已影响418人  mobilefellow

Kingfisher是一个使用Swift编写的用于下载和缓存图片的iOS库,作者王巍受SDWebImage的启发开发了这个纯Swift的库。Kingfisher的完整特性可以从下面的链接中获取,本文主要是学习项目源码的一些心得。
https://github.com/onevcat/Kingfisher

kf_xxx >> kf.xxx

参考[RxCocoa] Move from rx_ prefix to a rx. proxy (for Swift 3 update ?

传统的Objective-C语法对于扩展的method,推荐使用kf_xxx方式命名,但是这种命名不太符合Swift的风格,且看起来很丑。因此越来越多的Swift项目开始参考LazySequence模式,使用类似下面的风格:

myArray.map { ... }
myArray.lazy.map { ... }

Kingfisher也由kf_xxx转向了kf.xxx风格,如imageView?.kf.indicatorType = .activity。要实现这个机制,需要以下几步:

ImageDownloader

ImageDownloader是这个库的两大核心之一,另一个是ImageCache。

ImageDownloader represents a downloading manager for requesting the image with a URL from server.

ImageFetchLoad

它是ImageDownloader的一个内部class,主要用于避免一个URL的资源被同时下载多次。定义如下:

class ImageFetchLoad {
    var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
    var responseData = NSMutableData()

    var downloadTaskCount = 0
    var downloadTask: RetrieveImageDownloadTask?
    var cancelSemaphore: DispatchSemaphore?
}

这个类通过ImageDownloader.fetchLoads与某个URL挂钩。对于一个URL,如果用户反复下载,

Properties

fetchLoads
是一个[URL: ImageFetchLoad],如上所说,用于记录URL和ImageFetchLoad的关系。

存在3个不同的DispatchQueue

  1. barrierQueue
    concurrent
    用于thread safe地操作fetchLoads
if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
    if fetchLoad.cancelSemaphore == nil {
        fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
    }
    cancelQueue.async {
        _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
        fetchLoad.cancelSemaphore = nil
        prepareFetchLoad()
    }
} else {
    prepareFetchLoad()
}
private func callCompletionHandlerFailure(error: Error, url: URL) {
    guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else {
        return
    }

    // We need to clean the fetch load first, before actually calling completion handler.
    cleanFetchLoad(for: url)

    var leftSignal: Int
    repeat {
        leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
    } while leftSignal != 0

    for content in fetchLoad.contents {
        content.options.callbackDispatchQueue.safeAsync {
            content.callback.completionHandler?(nil, error as NSError, url, nil)
        }
    }
}

sessionHandler
用于实现URLSessionDataDelegate,并避免由于session导致的retain cycle。https://github.com/onevcat/Kingfisher/issues/235

APIs

downloadImage()
对外的API主要是这个方法,定义如下:

    /**
     Download an image with a URL and option.
     
     - parameter url:               Target URL.
     - parameter retrieveImageTask: The task to cooporate with cache. Pass `nil` if you are not trying to use downloader and cache.
     - parameter options:           The options could control download behavior. See `KingfisherOptionsInfo`.
     - parameter progressBlock:     Called when the download progress updated.
     - parameter completionHandler: Called when the download progress finishes.
     
     - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
     */
    @discardableResult
    open func downloadImage(with url: URL,
                       retrieveImageTask: RetrieveImageTask? = nil,
                       options: KingfisherOptionsInfo? = nil,
                       progressBlock: ImageDownloaderProgressBlock? = nil,
                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?

ImageCache

ImageCache是这个库的两大核心之一,另一个是ImageDownloader。

ImageCache represents both the memory and disk cache system of Kingfisher. While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create your own cache object and configure it as your need. You could use an ImageCache object to manipulate memory and disk cache for Kingfisher.

ImageCache的原理比较简单。比较值得学习的是它里面用到了2个单独的DispatchQueue,以免堵塞主线程。

ImageProcessor

An ImageProcessor would be used to convert some downloaded data to an image.

它是一个Protocol,用于将下载的数据转换为image,定义如下:

public protocol ImageProcessor {
    /// Identifier of the processor.
    var identifier: String { get }
    
    /// Process an input `ImageProcessItem` item to an image for this processor.
    ///
    /// - parameter item:    Input item which will be processed by `self`
    /// - parameter options: Options when processing the item.
    ///
    /// - returns: The processed image.
    ///
    /// - Note: The return value will be `nil` if processing failed while converting data to image.
    ///         If input item is already an image and there is any errors in processing, the input 
    ///         image itself will be returned.
    /// - Note: Most processor only supports CG-based images. 
    ///         watchOS is not supported for processers containing filter, the input image will be returned directly on watchOS.
    func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image?
}

Kingfisher提供了以下默认的实现。

DefaultImageProcessor

The default processor. It convert the input data to a valid image. Images of .PNG, .JPEG and .GIF format are supported. If an image is given, DefaultImageProcessor will do nothing on it and just return that image.

public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
    switch item {
    case .image(let image):
        return image
    case .data(let data):
        return Kingfisher<Image>.image(
            data: data,
            scale: options.scaleFactor,
            preloadAllAnimationData: options.preloadAllAnimationData,
            onlyFirstFrame: options.onlyLoadFirstFrame)
    }
}

GeneralProcessor

private class。主要用于将两个ImageProcessor合并起来。这个合并很精巧,它定义了一个内部block p,初始化时p会hold住两个旧的ImageProcessor,process时会逐一使用它们。

public extension ImageProcessor {
    
    /// Append an `ImageProcessor` to another. The identifier of the new `ImageProcessor` 
    /// will be "\(self.identifier)|>\(another.identifier)".
    ///
    /// - parameter another: An `ImageProcessor` you want to append to `self`.
    ///
    /// - returns: The new `ImageProcessor` will process the image in the order
    ///            of the two processors concatenated.
    public func append(another: ImageProcessor) -> ImageProcessor {
        let newIdentifier = identifier.appending("|>\(another.identifier)")
        return GeneralProcessor(identifier: newIdentifier) {
            item, options in
            if let image = self.process(item: item, options: options) {
                return another.process(item: .image(image), options: options)
            } else {
                return nil
            }
        }
    }
}

typealias ProcessorImp = ((ImageProcessItem, KingfisherOptionsInfo) -> Image?)

fileprivate struct GeneralProcessor: ImageProcessor {
    let identifier: String
    let p: ProcessorImp
    func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? {
        return p(item, options)
    }
}

其他ImageProcessor

其他的ImageProcessor包括:

以上这些Processor,对于输入的ImageProcessItem,如果item

Tips

Collection

public extension Collection where Iterator.Element == KingfisherOptionsInfoItem {
    /// The target `ImageCache` which is used.
    public var targetCache: ImageCache {
        if let item = lastMatchIgnoringAssociatedValue(.targetCache(.default)),
            case .targetCache(let cache) = item
        {
            return cache
        }
        return ImageCache.default
    }
}
public init(resources: [Resource],
       options: KingfisherOptionsInfo? = nil,
       progressBlock: PrefetcherProgressBlock? = nil,
       completionHandler: PrefetcherCompletionHandler? = nil)
{
    prefetchResources = resources
    pendingResources = ArraySlice(resources)
    。。。。
}

public func start()
{
    。。。
    let initialConcurentDownloads = min(self.prefetchResources.count, self.maxConcurrentDownloads)
    for _ in 0 ..< initialConcurentDownloads {
        if let resource = self.pendingResources.popFirst() {
            self.startPrefetching(resource)
        }
    }
}

DispatchQueue

extension DispatchQueue {
    // This method will dispatch the `block` to self.
    // If `self` is the main queue, and current thread is main thread, the block
    // will be invoked immediately instead of being dispatched.
    func safeAsync(_ block: @escaping ()->()) {
        if self === DispatchQueue.main && Thread.isMainThread {
            block()
        } else {
            async { block() }
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读