Alamofire源码解读系列(六)之Task代理(TaskDelegate)

本篇介绍Task代理(TaskDelegate.swift)

前言

我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很多人都做了二次封装,但事实上,这个二次封装却又异常简单或者是简陋。这篇文章的内容是Task代理,是一篇很独立的文章,大家可以通过这篇文章了解iOS中网络开发是怎么一回事。

那么一条最普通的网络请求,究竟是怎样的一个过程?首先我们根据一个URL和若干个参数生成Request,然后根据Request生成一个会话Session,再根据这个Session生成Task,我们开启Task就完成了这个请求。当然这一过程之中还会包含重定向,数据上传,挑战,证书等等一系列的配置信息。

我们再聊聊代理的问题,不管是在网络请求中,还是再其他的地方,代理都类似于一个管理员的身份。这在业务架构中是一个很好的主意。假如我把代理想象成一个人,那么这个人的工作是什么呢?

  1. 提供我所需要的数据,这一点很重要。获取的数据也分两种:加工过的和未加工过的。举个例子,有一个控制器,里边需要处理好几个不同类型的cell,每个cell都有属于自己的适配器来控制样式和数据处理,面对这种情况,可以设计一个代理,每当刷新界面的时候,直接从代理中请求处理过的适配器数据,那么这种情况下的数据就是被加工过的,如果数据加工是一个复杂的工作,可以设计一个数据加工的代理。这就涉及了下边的代理的另外一项工作。

  2. 提供处理业务的方法,这往往被用于事件的传递。这是一种狭义上的代理,是一个协议。在开发中,不管是view还是Controller,都可以利用代理传递事件。但我们这里说的不是协议,协议必须要求我们实现它的属性和方法,这对上边提到的‘人’是不友好的。在真实的理想的场景中,我和代理的交互只有两种情况:

    1. 给我想要的数据
    2. 我知道这个业务你比较精通,当有和你相关的事情的时候,我会通知你

如果对上边的内容不太明白,只需要明白,有的代理是一个协议,有的代理是一个'人',在某些让你头疼的复杂的业务中,用代理'人'去处理。我在想是不是不叫代理,叫Manager更合适。

URLSessionTask

URLSessionTask是对task最基本的封装。按照请求的目的和响应的结果可以分为:

  • 获取Data
  • 下载
  • 上传
  • Stream, 这个在这篇中不做讲解

上边图中表示了一种继承关系,与之相对应的代理如下图:

我会在下边详细讲解这些代理方法。

TaskDelegate

TaskDelegate位于继承链的最底层,因此它提供了一些最基础的东西,这些东西也是其他Delegate共享的,我们先看看属性:

   // MARK: Properties

    /// The serial operation queue used to execute all operations after the task completes.
    open let queue: OperationQueue

    /// The data returned by the server.
    public var data: Data? { return nil }

    /// The error generated throughout the lifecyle of the task.
    public var error: Error?

    var task: URLSessionTask? {
        didSet { reset() }
    }

    var initialResponseTime: CFAbsoluteTime?
    var credential: URLCredential?
    var metrics: AnyObject? // URLSessionTaskMetrics

我们来分析下这些属性:

  • queue: OperationQueue 很明显这是一个队列,队列中可以添加很多operation。队列是可以被暂停的,通过把isSuspended设置为true就可以让队列中的所有operation暂停,直到isSuspended设置为false后,operation才会开始执行。在Alamofire中,放入该队列的operation有一下几种情况:

    • 注意:该队列会在任务完成之后把isSuspended设置为false
    • 一个Request的endTime,在任务完成后调用,就可以为Request设置请求结束时间
    • response处理,Alamofire中的响应回调是链式的,原理就是把这些回调函数通过operation添加到队列中,因此也保证了回调函数的访问顺序是正确的
    • 上传数据成功后,删除临时文件,这个后续的文章会解释的
  • data: Data? 表示服务器返回的Data,这个可能为空

  • error: Error?表示该代理生命周期内有可能出现的错误,这一点很重要,我们在写一个代理或者manager的时候,可以添加这么一个错误属性,专门抓取生命周期内的错误。

  • task: URLSessionTask? 表示一个task,对于本代理而言,task是很重要的一个属性。

  • initialResponseTime: CFAbsoluteTime? 当task是URLSessionDataTask时,表示接收到数据的时间;当task是URLSessionDownloadTask时,表示开始写数据的时间;当task是URLSessionUploadTask时,表示上传数据的时间;

  • credential: URLCredential? 表示证书,如果给该代理设置了这个属性,在它里边的证书验证方法中会备用到

  • metrics: AnyObject? apple提供了一个统计task信息的类URLSessionTaskMetrics,可以统计跟task相关的一些信息,包括和task相关的所有事务,task的开始和结束时间,task的重定向的次数。

上边的这些属性中,可以留意一下queue的使用方法,尽量为设计的代理添加错误处理机制。

Alamofire使用类似Properties,Lifecycle等等关键字来分割文件的,看完了上边的属性,我们在看看Lifecycle。

 // MARK: Lifecycle

    init(task: URLSessionTask?) {
        self.task = task

        self.queue = {
            let operationQueue = OperationQueue()

            operationQueue.maxConcurrentOperationCount = 1
            operationQueue.isSuspended = true
            operationQueue.qualityOfService = .utility

            return operationQueue
        }()
    }

    func reset() {
        error = nil
        initialResponseTime = nil
    }

reset函数把error和initialResponseTime都置为nil,这个没什么好说的,在init函数中队列的创建很有意思。在swift中,如果我们要创建一个对象,不管是view还是别的,都可以采用这样的方式:创建一个函数,然后立刻调用,这很像JavaScript的用法。

lazy var label: UILabel = {
        let view = UILabel()
        view.backgroundColor = .clear
        view.textAlignment = .center
        view.textColor = .white
        view.font = .boldSystemFont(ofSize: 18)
        self.contentView.addSubview(view)
        return view
    }()

operationQueue.isSuspended = true可以保证队列中的operation都是暂停状态,正常情况下,operation在被加入到队列中后,会尽快执行。

在swift中函数是一等公民,可以当参数和返回值来使用。同oc的block一样,我们可以把他们当成一个属性,目的是提前告诉代理当遇到指定的事件时应该怎么做?在YYModel中有一小段代码就是关于Block当返回值的妙用。我们看看TaskDelegate下的四个相关函数:

// MARK: URLSessionTaskDelegate

    var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?
    var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?
    var taskNeedNewBodyStream: ((URLSession, URLSessionTask) -> InputStream?)?
    var taskDidCompleteWithError: ((URLSession, URLSessionTask, Error?) -> Void)?

我们先看第一个函数,这个函数对应的代理方法如下:

 @objc(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        willPerformHTTPRedirection response: HTTPURLResponse,
        newRequest request: URLRequest,
        completionHandler: @escaping (URLRequest?) -> Void)
    {
        var redirectRequest: URLRequest? = request

        if let taskWillPerformHTTPRedirection = taskWillPerformHTTPRedirection {
            redirectRequest = taskWillPerformHTTPRedirection(session, task, response, request)
        }

        completionHandler(redirectRequest)
    }

上边的函数处理的问题是请求重定向问题,我们大概讲一下重定向是怎么一回事:Web服务器有时会返回重定向响应而不是成功的报文。Web服务器可以将浏览器重定向到其他地方来执行请求。重定向响应由返回码3XX说明。Location响应首部包含了内容的新地址或优选地址的URI重定向可用于下列情况:

  1. 永久撤离的资源:资源可能被移动到新的位置,或者被重新命名,有了一个新的URI。Web服务器返回301 Moved Permanently
  2. 临时撤离的资源:如果资源被临时撤离或重命名,Web服务器返回303 See Other 或307 Temproary Redirect
  3. URL增强:服务器通常用重定向来重写URL,往往用于嵌入上下文。Web服务器返回303 See Other 或307 Temproary Redirect
  4. 负载均衡:如果一个超载的服务器收到一条请求,服务器可以将客户端重新定向到一个负载不大的服务器。Web服务器返回303 See Other 或307 Temproary Redirect
  5. 服务器关联:Web服务器可能会有某些用户的本地信息,服务器可以将客户端重新定向到包含那个客户端信息的服务器上去。Web服务器返回303 See Other 或307 Temproary Redirect
  6. 规范目录名称:客户端请求的URI是一个不带尾部斜线的目录名时,大多数服务器都会将客户端重定向到Hige加了尾部斜线的URI上。

上边的重定向函数要求返回一个redirectRequest,就是重定向的Request,Alamofire的处理方法就是,如果给代理的重定向处理函数赋值了,就返回代理函数的返回值,否则返回服务器返回的Request。

第二个函数用于处理验证相关的事务。我们先讲讲disposition,他的类型是URLSession.AuthChallengeDisposition,其实就是一个枚举:

  @available(iOS 7.0, *)
    public enum AuthChallengeDisposition : Int {

        
        case useCredential

        case performDefaultHandling

        case cancelAuthenticationChallenge

        case rejectProtectionSpace
    }

这个授权配置一共有四个选项:

  • useCredential 表示使用证书

  • performDefaultHandling 表示采用默认的方式,这个方式跟服务器返回authenticationMethod有很大关系,我简单列一些常用的method

    • NSURLAuthenticationMethodHTTPBasic 基本认证
    • NSURLAuthenticationMethodHTTPDigest 摘要认证
    • NSURLAuthenticationMethodClientCertificate 客户端证书认证
    • NSURLAuthenticationMethodServerTrust 表示返回的证书要使用serverTrust创建
  • cancelAuthenticationChallenge 表示取消认证

  • rejectProtectionSpace 表示拒绝认证

对于验证而言,有三种方式:

  1. 客户端验证
  2. 服务器验证
  3. 双向验证

我们先给出Alamofire中的函数,然后在分析:

 @objc(URLSession:task:didReceiveChallenge:completionHandler:)
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didReceive challenge: URLAuthenticationChallenge,
        completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
    {
        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
        var credential: URLCredential?

        if let taskDidReceiveChallenge = taskDidReceiveChallenge {
            (disposition, credential) = taskDidReceiveChallenge(session, task, challenge)
        } else if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
            let host = challenge.protectionSpace.host

            if
                let serverTrustPolicy = session.serverTrustPolicyManager?.serverTrustPolicy(forHost: host),
                let serverTrust = challenge.protectionSpace.serverTrust
            {
                if serverTrustPolicy.evaluate(serverTrust, forHost: host) {
                    disposition = .useCredential
                    credential = URLCredential(trust: serverTrust)
                } else {
                    disposition = .cancelAuthenticationChallenge
                }
            }
        } else {
            if challenge.previousFailureCount > 0 {
                disposition = .rejectProtectionSpace
            } else {
                credential = self.credential ?? session.configuration.urlCredentialStorage?.defaultCredential(for: challenge.protectionSpace)

                if credential != nil {
                    disposition = .useCredential
                }
            }
        }

        completionHandler(disposition, credential)
    }

如果服务器需要验证客户端的,我们只需要给TaskDelegate的 var taskDidReceiveChallenge: ((URLSession, URLSessionTask, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?赋值就行了。

这里有一个很重要的问题,HTTPS并不会触发上边的回调函数,原因就是NSURLSession内部有一个根证书,内部会跟服务器的证书进行验证,如果服务器的证书是证书机构颁发的话,就可以顺利通过验证,否则会报错。

另一个很重要的问题是,上边的方法在何种情况下触发,按照apple的说法URL Session Programming Guide,当服务器响应头中包含WWW-Authenticate或者使用 proxy authentication TLS trust validation时,上边的方法就会被触发,其他情况下都不会触发。

Alamofire是双向验证的

双向验证的过程:

  • 当服务器返回的challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust时,服务器提供了一个服务器信任的证书,但这个证书也仅仅是服务器自己信任的,攻击者完全可以提供一个证书骗过客户端,基于这个问题,客户端需要验证服务端的证书
  • Alamofire中通过ServerTrustPolicy.swift这个类来验证证书,大概过程就是拿本地的证书跟服务端返回的证书进行对比,如果客户端存在相同的证书就表示通过,这个过程我会在ServerTrustPolicy.swift那篇文章中给出详细的解答

因此,客户端和服务端要建立SSL只需要两步就行了:

  1. 服务端返回WWW-Authenticate响应头,并返回自己信任证书
  2. 客户端验证证书,然后用证书中的公钥把数据加密后发送给服务端

第三个函数:

///This delegate method is called under two circumstances:
    ///To provide the initial request body stream if the task was created with uploadTask(withStreamedRequest:)
    ///To provide a replacement request body stream if the task needs to resend a request that has a body stream because of an authentication challenge or other recoverable server error.
    ///Note
    ///You do not need to implement this if your code provides the request body using a file URL or an NSData object.
    @objc(URLSession:task:needNewBodyStream:)
    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)
    {
        var bodyStream: InputStream?

        if let taskNeedNewBodyStream = taskNeedNewBodyStream {
            bodyStream = taskNeedNewBodyStream(session, task)
        }

        completionHandler(bodyStream)
    }

当给task的Request提供一个body stream时才会调用,我们不需要关心这个方法,即使我们通过fileURL或者NSData上传数据时,该函数也不会被调用,使用场景很少。

第四个函数:

@objc(URLSession:task:didCompleteWithError:)
    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
        if let taskDidCompleteWithError = taskDidCompleteWithError {
            taskDidCompleteWithError(session, task, error)
        } else {
            if let error = error {
                if self.error == nil { self.error = error }

                if
                    let downloadDelegate = self as? DownloadTaskDelegate,
                    let resumeData = (error as NSError).userInfo[NSURLSessionDownloadTaskResumeData] as? Data
                {
                    downloadDelegate.resumeData = resumeData
                }
            }

            queue.isSuspended = false
        }
    }

该函数在请求完成后被调用,值得注意的是error不为nil的情况,除了给自身的error属性赋值外,针对下载任务做了特殊处理,就是把当前已经下载的数据保存在downloadDelegate.resumeData中,有点像断点下载。

DataTaskDelegate

DataTaskDelegate继承自TaskDelegate,实现了URLSessionDataDelegate协议。因此下边我们也会讲解URLSessionDataDelegate协议的方法。我们还是先看这里边的属性:

 // MARK: Properties

    var dataTask: URLSessionDataTask { return task as! URLSessionDataTask }

    override var data: Data? {
        if dataStream != nil {
            return nil
        } else {
            return mutableData
        }
    }

    var progress: Progress
    var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?

    var dataStream: ((_ data: Data) -> Void)?

    private var totalBytesReceived: Int64 = 0
    private var mutableData: Data

    private var expectedContentLength: Int64?

我们对这些属性给出一定的解释:

  • dataTask: URLSessionDataTask DataTaskDelegate管理URLSessionDataTask
  • data: Data? 同样是返回Data,但这里有一点不同,如果定义了dataStream方法的话,这个data返回为nil
  • progress: Progress 进度
  • progressHandler 这不是函数,是一个元组,什么时候调用,在下边的方法中给出说明
  • dataStream 自定义的数据处理函数
  • totalBytesReceived 已经接受的数据
  • mutableData 保存数据的容器
  • expectedContentLength 需要接受的数据的总大小

DataTaskDelegate的生命周期:

// MARK: Lifecycle

    override init(task: URLSessionTask?) {
        mutableData = Data()
        progress = Progress(totalUnitCount: 0)

        super.init(task: task)
    }

    override func reset() {
        super.reset()

        progress = Progress(totalUnitCount: 0)
        totalBytesReceived = 0
        mutableData = Data()
        expectedContentLength = nil
    }

这些没什么好说的,我们在看看有哪些函数:

    // MARK: URLSessionDataDelegate

    var dataTaskDidReceiveResponse: ((URLSession, URLSessionDataTask, URLResponse) -> URLSession.ResponseDisposition)?
    var dataTaskDidBecomeDownloadTask: ((URLSession, URLSessionDataTask, URLSessionDownloadTask) -> Void)?
    var dataTaskDidReceiveData: ((URLSession, URLSessionDataTask, Data) -> Void)?
    var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

URLSessionDataDelegate有四个函数,我们先看第一个函数:

  func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didReceive response: URLResponse,
        completionHandler: @escaping (URLSession.ResponseDisposition) -> Void)
    {
        var disposition: URLSession.ResponseDisposition = .allow

        expectedContentLength = response.expectedContentLength

        if let dataTaskDidReceiveResponse = dataTaskDidReceiveResponse {
            disposition = dataTaskDidReceiveResponse(session, dataTask, response)
        }

        completionHandler(disposition)
    }

当收到服务端的响应后,该方法被触发。在这个函数中,我们能够获取到和数据相关的一些参数,大家可以想象成响应头。Alamofire中给出了一个函数dataTaskDidReceiveResponse,我们可以利用这个函数控制是不是要继续获取数据,默认是.allow。

我们看第二个函数:

func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        didBecome downloadTask: URLSessionDownloadTask)
    {
        dataTaskDidBecomeDownloadTask?(session, dataTask, downloadTask)
    }

在上边的disposition配置中,disposition的类型是URLSession.ResponseDisposition,我们看看这个枚举:

 public enum ResponseDisposition : Int {

        
        case cancel /* Cancel the load, this is the same as -[task cancel] */

        case allow /* Allow the load to continue */

        case becomeDownload /* Turn this request into a download */

        @available(iOS 9.0, *)
        case becomeStream /* Turn this task into a stream task */
    }

当选择了becomeDownload后,就会触发上边的第二个函数,在函数中会提供一个新的downloadTask。这就给我们一个把dataTask转换成downloadTask的机会

那么我们把dataTask转换成downloadTask究竟有什么用呢?我想到了一个使用场景,假如给定一个URL,返回的数据是Data,其实我想把这些数据下载下来,那么就可以使用上边的这种技术了。举个例子,https://baidu.com打开这个url会直接显示网页,使用上边的技术,打开这个url会直接下载网页。我并没有验证上边的想法。

我们继续看第三个函数:

 func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

        if let dataTaskDidReceiveData = dataTaskDidReceiveData {
            dataTaskDidReceiveData(session, dataTask, data)
        } else {
            if let dataStream = dataStream {
                dataStream(data)
            } else {
                mutableData.append(data)
            }

            let bytesReceived = Int64(data.count)
            totalBytesReceived += bytesReceived
            let totalBytesExpected = dataTask.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown

            progress.totalUnitCount = totalBytesExpected
            progress.completedUnitCount = totalBytesReceived

            if let progressHandler = progressHandler {
                progressHandler.queue.async { progressHandler.closure(self.progress) }
            }
        }
    }

这个方法算是核心方法,我在MCDownloadManager中实现下载的核心方法就是这个方法,不同之处是,Alamofire把数据放入对象中,而我把数据写入本地文件中。对这个函数内部就不做解释了,主要就是对自定义函数和进度的一些处理。

我们看第四个函数:

func urlSession(
        _ session: URLSession,
        dataTask: URLSessionDataTask,
        willCacheResponse proposedResponse: CachedURLResponse,
        completionHandler: @escaping (CachedURLResponse?) -> Void)
    {
        var cachedResponse: CachedURLResponse? = proposedResponse

        if let dataTaskWillCacheResponse = dataTaskWillCacheResponse {
            cachedResponse = dataTaskWillCacheResponse(session, dataTask, proposedResponse)
        }

        completionHandler(cachedResponse)
    }

其实,往往这样的函数才是我们应该注意的,最常见的接受响应,处理数据,请求完成都是我们很熟悉的方法,因此更应该多多注意这些不太熟悉的方法。

该函数用于处理是否需要缓存响应,Alamofire默认是缓存这些response的,但是每次发请求,它不会再缓存中读取。

DownloadTaskDelegate

DownloadTaskDelegate继承自TaskDelegate,实现了URLSessionDownloadDelegate协议。因此下边我们也会讲解URLSessionDownloadDelegate协议的方法。我们还是先看这里边的属性:

 // MARK: Properties

    var downloadTask: URLSessionDownloadTask { return task as! URLSessionDownloadTask }

    var progress: Progress
    var progressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?

    var resumeData: Data?
    override var data: Data? { return resumeData }

    var destination: DownloadRequest.DownloadFileDestination?

    var temporaryURL: URL?
    var destinationURL: URL?

    var fileURL: URL? { return destination != nil ? destinationURL : temporaryURL }

这些属性中有和上边介绍的属性重复的部分,我们只对不重复的部分给出说明:

  • downloadTask 和URLSessionDownloadDelegate相对应的URLSessionDownloadTask
  • resumeData 在上边我们提到过,当请求完成后,如果error不为nil,如果是DownloadTaskDelegate,就会给这个属性赋值
  • data 返回resumeData
  • destination 通过这个函数可以自定义文件保存目录和保存方式,这个保存方式分两种,为URl创建文件夹,删除已经下载且存在的文件,这个会在后续的文章中提到
  • temporaryURL 临时的URL
  • destinationURL 数据存储URL
  • fileURL返回文件的路径,如果destination不为nil,就返回destinationURL,否则返回temporaryURL

生命周期:

  // MARK: Lifecycle

    override init(task: URLSessionTask?) {
        progress = Progress(totalUnitCount: 0)
        super.init(task: task)
    }

    override func reset() {
        super.reset()

        progress = Progress(totalUnitCount: 0)
        resumeData = nil
    }

和下载相关的代理函数有三个:

// MARK: URLSessionDownloadDelegate

    var downloadTaskDidFinishDownloadingToURL: ((URLSession, URLSessionDownloadTask, URL) -> URL)?
    var downloadTaskDidWriteData: ((URLSession, URLSessionDownloadTask, Int64, Int64, Int64) -> Void)?
    var downloadTaskDidResumeAtOffset: ((URLSession, URLSessionDownloadTask, Int64, Int64) -> Void)?

我们先看看第一个函数:

 func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didFinishDownloadingTo location: URL)
    {
        temporaryURL = location

        guard
            let destination = destination,
            let response = downloadTask.response as? HTTPURLResponse
        else { return }

        let result = destination(location, response)
        let destinationURL = result.destinationURL
        let options = result.options

        self.destinationURL = destinationURL
/// 说明在编码过程中,对于存在可能出现错误的地方,一定要做error处理
        do {
            if options.contains(.removePreviousFile), FileManager.default.fileExists(atPath: destinationURL.path) {
                try FileManager.default.removeItem(at: destinationURL)
            }

            if options.contains(.createIntermediateDirectories) {
                let directory = destinationURL.deletingLastPathComponent()
                try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
            }

            try FileManager.default.moveItem(at: location, to: destinationURL)
        } catch {
            self.error = error
        }
    }

对于这样的代理方法,我们首先要做的就是弄明白在什么情况下它会被触发。当数据下载完成后,该函数被触发。系统会把数据下载到一个临时的locationURL的地方,我们就是通过这个URL拿到数据的。上边函数内的代码主要是把数据复制到目标路径中。

但是我有一个疑问?按照apple文档的内容:If you choose to open the file for reading, you should do the actual reading in another thread to avoid blocking the delegate queue.应该在另一个线程来读取数据,这样才不会阻塞当前的代理线程,不知道有什么影响?

我们来看第二个函数:

func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didWriteData bytesWritten: Int64,
        totalBytesWritten: Int64,
        totalBytesExpectedToWrite: Int64)
    {
        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

        if let downloadTaskDidWriteData = downloadTaskDidWriteData {
            downloadTaskDidWriteData(
                session,
                downloadTask,
                bytesWritten,
                totalBytesWritten,
                totalBytesExpectedToWrite
            )
        } else {
            progress.totalUnitCount = totalBytesExpectedToWrite
            progress.completedUnitCount = totalBytesWritten

            if let progressHandler = progressHandler {
                progressHandler.queue.async { progressHandler.closure(self.progress) }
            }
        }
    }

该代理方法在数据下载过程中被触发,主要的作用就是提供下载进度。这个比较简单,我们看看第三个函数:

 func urlSession(
        _ session: URLSession,
        downloadTask: URLSessionDownloadTask,
        didResumeAtOffset fileOffset: Int64,
        expectedTotalBytes: Int64)
    {
        if let downloadTaskDidResumeAtOffset = downloadTaskDidResumeAtOffset {
            downloadTaskDidResumeAtOffset(session, downloadTask, fileOffset, expectedTotalBytes)
        } else {
            progress.totalUnitCount = expectedTotalBytes
            progress.completedUnitCount = fileOffset
        }
    }

是这样的,如果一个下载的task是可以恢复的,那么当下载被取消或者失败后,系统会返回一个resume​Data对象,这个对象包含了一些跟这个下载task相关的一些信息,有了它就能重新创建下载task,创建方法有两个:download​Task(with​Resume​Data:​)download​Task(with​Resume​Data:​completion​Handler:​),当task开始后,上边的代理方法就会被触发。

UploadTaskDelegate

UploadTaskDelegate继承自DataTaskDelegate。对于上传数据来说最麻烦的就是多表单数据的上传,这个我们会在后续的MultipartFormData.swift给出详细的解释。

我们先看看它的属性有哪些?

 // MARK: Properties

    var uploadTask: URLSessionUploadTask { return task as! URLSessionUploadTask }

    var uploadProgress: Progress
    var uploadProgressHandler: (closure: Request.ProgressHandler, queue: DispatchQueue)?

这些和上边出现的内容有重叠,在这里就不多做解释了,我们再看看生命周期:

    // MARK: Lifecycle

    override init(task: URLSessionTask?) {
        uploadProgress = Progress(totalUnitCount: 0)
        super.init(task: task)
    }

    override func reset() {
        super.reset()
        uploadProgress = Progress(totalUnitCount: 0)
    }

也没什么好说的,再看看函数:

 // MARK: URLSessionTaskDelegate

    var taskDidSendBodyData: ((URLSession, URLSessionTask, Int64, Int64, Int64) -> Void)?

    func URLSession(
        _ session: URLSession,
        task: URLSessionTask,
        didSendBodyData bytesSent: Int64,
        totalBytesSent: Int64,
        totalBytesExpectedToSend: Int64)
    {
        if initialResponseTime == nil { initialResponseTime = CFAbsoluteTimeGetCurrent() }

        if let taskDidSendBodyData = taskDidSendBodyData {
            taskDidSendBodyData(session, task, bytesSent, totalBytesSent, totalBytesExpectedToSend)
        } else {
            uploadProgress.totalUnitCount = totalBytesExpectedToSend
            uploadProgress.completedUnitCount = totalBytesSent

            if let uploadProgressHandler = uploadProgressHandler {
                uploadProgressHandler.queue.async { uploadProgressHandler.closure(self.uploadProgress) }
            }
        }
    }

该函数主要目的是提供上传的进度,在Alamofire中,上传数据用的是stream,这个会在后续文章中给出详细的解释。

总结

我个人解读源码的方式可能比较特别,我喜欢把所有的代码都写到文章之中。因为人的记忆都是有问题的,好多东西当时记住了,过段时间就忘了,为了方便日后查看这些笔记,我觉得还是把代码都弄上来比较好。

同一段代码,不同的人看会有不同的想法,这些解读也可以给别人一些参考。我现在越来越觉得代码的设计很重要了。

由于知识水平有限,如有错误,还望指出

链接

Alamofire源码解读系列(一)之概述和使用 简书-----博客园

Alamofire源码解读系列(二)之错误处理(AFError) 简书-----博客园

Alamofire源码解读系列(三)之通知处理(Notification) 简书-----博客园

Alamofire源码解读系列(四)之参数编码(ParameterEncoding) 简书-----博客园

Alamofire源码解读系列(五)之结果封装(Result) 简书-----博客园

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容