在使用第三方庫的時候,每每只用其功能,不多去關注背後的實現細節。本文從源碼(Kingfisher v5.13.0)角度,學習與研究Kingfisher背後實現細節。git
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 { }
}
}
複製代碼
在設置圖片的時候,不管是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
}
複製代碼
從setImage()到是否執行請求下載圖片的邏輯,是由KingfisherManager的retrieveImage()
處理的。retrieveImage()
的處理邏輯是:session
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()
包含了從內存 / 硬盤中查找圖片,返回值爲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
方法作了個判斷。判斷當前圖片是否須要從網絡中下載獲取(從網絡中獲取,執行下載,並緩存圖片),仍是經過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
中,執行了下載圖片任務後,都會執行_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
實際是KingfisherOptionsInfoItem
的結構體形式,便於生成控制Kingfisher中,加載網絡圖片,緩存等行爲的配置信息。
緩存回調協做器內部會更根據當前的緩存狀態與操做來斷定,是否觸發trigger
,trigger
是空閉包,傳入緩存圖片成功後的邏輯進行處理。協做器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是一個單例,內部持有內存存儲(MemoryStorage)和硬盤存儲(DiskStorage)的實例,擔任着真正的將圖片緩存在內存/硬盤的職責。在初始化的時候,內部監聽三個通知
當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管理每一個要進行下載的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))
}
複製代碼
圖片的下載是經過URLSession完成的。建立一個URLSession,須要指定URLSessionConfiguration。因爲在每一個會話數據任務中,都不是固定某種會話類型。因此這裏使用URLSessionConfiguration.ephemeral(非持久化類型的URLSessionConfiguration)。建立了圖片下載器後,調用downloadImage()開啓下載。在這個方法裏,作了以下幾個事情:
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實例的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()
,就將圖片的下載,緩存,重複調用等邏輯組合在一塊兒,最終完成加載網絡圖片的過程。
在SessionDataTask中,能夠看到兩個屬性onTaskDone
與onCallbackCancelled
。它們的類型都是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()
}
複製代碼
斷言是在不指望的情形出現時,中止程序運行。
assertionFailure
斷言assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)")
複製代碼
RequestErrorReason
,ResponseErrorReason
,CacheErrorReason
,ProcessorErrorReason
,ImageSettingErrorReason
。同時與NSError橋接,遵循了LocalizedError
與CustomNSError
協議。在某個圖片未被成功下載前,重複調用kf.setImage(),會有什麼效果?
這種狀況會在UITableViewCell / UICollectionViewCell中出現,當某個圖片沒有被緩存時,快速上下滑動。會出現下載任務(DownloadTask)會屢次建立,可是與圖片相關的SessionDataTask
,同一個URL,只會建立1次。一旦圖片下載成功並緩存了之後,再調用setImage(),就不會產生新的下載任務(DownloadTask),同時也不會建立SessionDataTask
。
Error與NSError的關係:www.jianshu.com/p/a36047852…
Kingfisher:github.com/onevcat/Kin…