| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- //
- // ImageDownloader.swift
- // Kingfisher
- //
- // Created by Wei Wang on 15/4/6.
- //
- // Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- #if os(macOS)
- import AppKit
- #else
- import UIKit
- #endif
- public struct ImageDownloadResult {
- public let image: Image
- public let url: URL
- public let originalData: Data
- }
- public struct DownloadTask {
- let sessionTask: SessionDataTask
- let cancelToken: SessionDataTask.CancelToken
- public func cancel() {
- sessionTask.cancel(token: cancelToken)
- }
- }
- /// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
- open class ImageDownloader {
- /// The default downloader.
- public static let `default` = ImageDownloader(name: "default")
- // MARK: - Public property
- /// The duration before the download is timeout. Default is 15 seconds.
- open var downloadTimeout: TimeInterval = 15.0
-
- /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this
- /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't
- /// specify the `authenticationChallengeResponder`.
- ///
- /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of
- /// `authenticationChallengeResponder` will be used instead.
- open var trustedHosts: Set<String>?
-
- /// Use this to set supply a configuration for the downloader. By default,
- /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used.
- ///
- /// You could change the configuration before a downloading task starts.
- /// A configuration without persistent storage for caches is requested for downloader working correctly.
- open var sessionConfiguration = URLSessionConfiguration.ephemeral {
- didSet {
- session.invalidateAndCancel()
- session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: nil)
- }
- }
-
- /// Whether the download requests should use pipline or not. Default is false.
- open var requestsUsePipelining = false
- /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
- open weak var delegate: ImageDownloaderDelegate?
-
- /// A responder for authentication challenge.
- /// Downloader will forward the received authentication challenge for the downloading session to this responder.
- open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
- let processQueue: DispatchQueue
- private let sessionHandler: SessionDelegate
- private var session: URLSession
- /// Creates a downloader with name.
- ///
- /// - Parameter name: The name for the downloader. It should not be empty.
- public init(name: String) {
- if name.isEmpty {
- fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
- }
-
- processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)")
- sessionHandler = SessionDelegate()
- session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: nil)
- authenticationChallengeResponder = self
- setupSessionHandler()
- }
- deinit { session.invalidateAndCancel() }
- private func setupSessionHandler() {
- sessionHandler.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in
- self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2)
- }
- sessionHandler.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in
- self.authenticationChallengeResponder?.downloader(
- self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3)
- }
- sessionHandler.onValidStatusCode.delegate(on: self) { (self, code) in
- return (self.delegate ?? self).isValidStatusCode(code, for: self)
- }
- sessionHandler.onDownloadingFinished.delegate(on: self) { (self, result) in
- // self.delegate?.imageDownloader(
- // self, didFinishDownloadingImageForURL: result.value?.0, with: result.value?.1, error: result.error)
- }
- sessionHandler.onDidDownloadData.delegate(on: self) { (self, task) in
- guard let url = task.task.originalRequest?.url else {
- return task.mutableData
- }
- guard let delegate = self.delegate else {
- return task.mutableData
- }
- return delegate.imageDownloader(self, didDownload: task.mutableData, for: url)
- }
- }
- /// Download an image with a URL and option.
- ///
- /// - Parameters:
- /// - url: Target URL.
- /// - options: The options could control download behavior. See `KingfisherOptionsInfo`.
- /// - progressBlock: Called when the download progress updated.
- /// - 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,
- options: KingfisherOptionsInfo? = nil,
- progressBlock: DownloadProgressBlock? = nil,
- completionHandler: ((Result<ImageDownloadResult>) -> Void)? = nil) -> DownloadTask?
- {
- var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout)
- request.httpShouldUsePipelining = requestsUsePipelining
- let options = options ?? .empty
- guard let r = options.modifier.modified(for: request) else {
- completionHandler?(.failure(KingfisherError2.requestError(reason: .emptyRequest)))
- return nil
- }
- request = r
-
- // There is a possibility that request modifier changed the url to `nil` or empty.
- guard let url = request.url, !url.absoluteString.isEmpty else {
- completionHandler?(.failure(KingfisherError2.requestError(reason: .invalidURL(request: request))))
- return nil
- }
- let onProgress = Delegate<(Int64, Int64), Void>()
- onProgress.delegate(on: self) { (_, progress) in
- let (downloaded, total) = progress
- progressBlock?(downloaded, total)
- }
- let onCompleted = Delegate<Result<ImageDownloadResult>, Void>()
- onCompleted.delegate(on: self) { (_, result) in
- completionHandler?(result)
- }
- let callback = SessionDataTask.TaskCallback(
- onProgress: onProgress, onCompleted: onCompleted, options: options)
- let downloadTask = sessionHandler.add(request, in: session, callback: callback)
- let task = downloadTask.sessionTask
- task.onTaskDone.delegate(on: self) { (self, done) in
- let (result, callbacks) = done
- self.delegate?.imageDownloader(
- self,
- didFinishDownloadingImageForURL: url,
- with: result.value?.1,
- error: result.error)
- switch result {
- case .success(let (data, response)):
- let prosessor = ImageDataProcessor(data: data, callbacks: callbacks)
- prosessor.onImageProcessed.delegate(on: self) { (self, result) in
- let (result, callback) = result
- if let image = result.value {
- self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response)
- }
- let imageResult = result.map { ImageDownloadResult(image: $0, url: url, originalData: data) }
- let queue = callback.options.callbackDispatchQueue
- queue.async { callback.onCompleted?.call(imageResult) }
- }
- self.processQueue.async { prosessor.process() }
- case .failure(let error):
- callbacks.forEach { callback in
- let queue = callback.options.callbackDispatchQueue
- queue.async { callback.onCompleted?.call(.failure(error)) }
- }
- }
- }
- if !task.started {
- delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
- task.resume()
- }
- return downloadTask
- }
- }
- // MARK: - Download method
- extension ImageDownloader {
- /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
- /// downloading tasks with an NSURLErrorCancelled error.
- ///
- /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
- /// returned by the downloading methods.
- public func cancelAll() {
- sessionHandler.cancelAll()
- }
- }
- extension ImageDownloader: AuthenticationChallengeResponsable {}
- // Placeholder. For retrieving extension methods of ImageDownloaderDelegate
- extension ImageDownloader: ImageDownloaderDelegate {}
- class SessionDelegate: NSObject {
- private var tasks: [URL: SessionDataTask] = [:]
- private let lock = NSLock()
- let onValidStatusCode = Delegate<Int, Bool>()
- let onDownloadingFinished = Delegate<Result<(URL, URLResponse)>, Void>()
- let onDidDownloadData = Delegate<SessionDataTask, Data?>()
- let onReceiveSessionChallenge = Delegate<(
- URLSession,
- URLAuthenticationChallenge,
- (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
- ),
- Void>()
- let onReceiveSessionTaskChallenge = Delegate<(
- URLSession,
- URLSessionTask,
- URLAuthenticationChallenge,
- (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
- ),
- Void>()
- func add(
- _ requst: URLRequest,
- in session: URLSession,
- callback: SessionDataTask.TaskCallback) -> DownloadTask
- {
- lock.lock()
- defer { lock.unlock() }
- let url = requst.url!
- if let task = tasks[url] {
- let token = task.addCallback(callback)
- return DownloadTask(sessionTask: task, cancelToken: token)
- } else {
- let task = SessionDataTask(session: session, request: requst)
- task.onTaskCancelled.delegate(on: self) { [unowned task] (self, value) in
- let (token, callback) = value
- let error = KingfisherError2.requestError(reason: .taskCancelled(task: task, token: token))
- task.onTaskDone.call((.failure(error), [callback]))
- if !task.containsCallbacks {
- self.tasks[url] = nil
- }
- }
- let token = task.addCallback(callback)
- tasks[url] = task
- return DownloadTask(sessionTask: task, cancelToken: token)
- }
- }
-
- func remove(_ task: URLSessionTask) {
- guard let url = task.originalRequest?.url else {
- return
- }
- lock.lock()
- defer { lock.unlock() }
- tasks[url] = nil
- }
-
- func task(for task: URLSessionTask) -> SessionDataTask? {
- guard let url = task.originalRequest?.url else {
- return nil
- }
- guard let sessionTask = tasks[url] else {
- return nil
- }
- guard sessionTask.task.taskIdentifier == task.taskIdentifier else {
- return nil
- }
- return sessionTask
- }
- func cancelAll() {
- lock.lock()
- defer { lock.unlock() }
- for task in tasks.values {
- task.forceCancel()
- }
- }
- }
- extension SessionDelegate: URLSessionDataDelegate {
- func urlSession(
- _ session: URLSession,
- dataTask: URLSessionDataTask,
- didReceive response: URLResponse,
- completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
- {
- lock.lock()
- defer { lock.unlock() }
- guard let httpResponse = response as? HTTPURLResponse else {
- let error = KingfisherError2.responseError(reason: .invalidURLResponse(response: response))
- onCompleted(task: dataTask, result: .failure(error))
- completionHandler(.cancel)
- return
- }
- let httpStatusCode = httpResponse.statusCode
- guard onValidStatusCode.call(httpStatusCode) == true else {
- let error = KingfisherError2.responseError(reason: .invalidHTTPStatusCode(response: httpResponse))
- onCompleted(task: dataTask, result: .failure(error))
- completionHandler(.cancel)
- return
- }
- completionHandler(.allow)
- }
- func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
- lock.lock()
- defer { lock.unlock() }
- guard let task = self.task(for: dataTask) else {
- return
- }
- task.didReceiveData(data)
- if let expectedContentLength = dataTask.response?.expectedContentLength, expectedContentLength != -1 {
- DispatchQueue.main.async {
- task.callbacks.forEach { callback in
- callback.onProgress?.call((Int64(task.mutableData.count), expectedContentLength))
- }
- }
- }
- }
- func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
- lock.lock()
- defer { lock.unlock() }
- guard let sessionTask = self.task(for: task) else {
- return
- }
- let result: Result<(Data, URLResponse?)>
- if let error = error {
- result = .failure(KingfisherError2.responseError(reason: .URLSessionError(error: error)))
- } else {
- if let data = onDidDownloadData.call(sessionTask), let finalData = data {
- result = .success((finalData, task.response))
- } else {
- result = .failure(KingfisherError2.responseError(reason: .dataModifyingFailed(task: sessionTask)))
- }
- }
- onCompleted(task: task, result: result)
- }
- func urlSession(
- _ session: URLSession,
- didReceive challenge: URLAuthenticationChallenge,
- completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
- {
- onReceiveSessionChallenge.call((session, challenge, completionHandler))
- }
- func urlSession(
- _ session: URLSession,
- task: URLSessionTask,
- didReceive challenge: URLAuthenticationChallenge,
- completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
- {
- onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler))
- }
- private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?)>) {
- guard let sessionTask = self.task(for: task) else {
- return
- }
- onCompleted(sessionTask: sessionTask, result: result)
- }
- private func onCompleted(sessionTask: SessionDataTask, result: Result<(Data, URLResponse?)>) {
- guard let url = sessionTask.task.originalRequest?.url else {
- return
- }
- tasks[url] = nil
- sessionTask.onTaskDone.call((result, Array(sessionTask.callbacks)))
- }
- }
- public class SessionDataTask {
- public typealias CancelToken = Int
- struct TaskCallback {
- let onProgress: Delegate<(Int64, Int64), Void>?
- let onCompleted: Delegate<Result<ImageDownloadResult>, Void>?
- let options: KingfisherOptionsInfo
- }
-
- public private(set) var mutableData: Data
- public let task: URLSessionDataTask
-
- private var callbacksStore = [CancelToken: TaskCallback]()
- var callbacks: Dictionary<SessionDataTask.CancelToken, SessionDataTask.TaskCallback>.Values {
- return callbacksStore.values
- }
- var currentToken = 0
- private let lock = NSLock()
- let onTaskDone = Delegate<(Result<(Data, URLResponse?)>, [TaskCallback]), Void>()
- let onTaskCancelled = Delegate<(CancelToken, TaskCallback), Void>()
- var started = false
- var containsCallbacks: Bool {
- // We should be able to use `task.state != .running` to check it.
- // However, in some rare cases, cancelling the task does not change
- // task state to `.cancelling`, but still in `.running`. So we need
- // to check callbacks count to for sure that it is safe to remove the
- // task in delegate.
- return !callbacks.isEmpty
- }
-
- init(session: URLSession, request: URLRequest) {
- task = session.dataTask(with: request)
- mutableData = Data()
- }
- func addCallback(_ callback: TaskCallback) -> CancelToken {
- lock.lock()
- defer { lock.unlock() }
- callbacksStore[currentToken] = callback
- defer { currentToken += 1 }
- return currentToken
- }
- func removeCallback(_ token: CancelToken) -> TaskCallback? {
- lock.lock()
- defer { lock.unlock() }
- if let callback = callbacksStore[token] {
- callbacksStore[token] = nil
- return callback
- }
- return nil
- }
-
- func resume() {
- started = true
- task.resume()
- }
- func cancel(token: CancelToken) {
- let result = removeCallback(token)
- if let callback = result {
- if callbacksStore.count == 0 {
- task.cancel()
- }
- onTaskCancelled.call((token, callback))
- }
- }
- func forceCancel() {
- for token in callbacksStore.keys {
- cancel(token: token)
- }
- }
-
- func didReceiveData(_ data: Data) {
- mutableData.append(data)
- }
- }
|