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:
- Determine whether the image needs to be forcibly refreshed, and if so, execute the loadAndCacheImage method to generate the download task
- 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
- 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.
- In the CacheCallbackCoordinator trigger() check, use
assertionFailure
assertions
assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)")
Copy the code
- 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 occur
RequestErrorReason
.ResponseErrorReason
.CacheErrorReason
.ProcessorErrorReason
.ImageSettingErrorReason
. Also bridge with NSError, followedLocalizedError
withCustomNSError
The agreement.
Points of concern
- 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.
- 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…