iOS网络协议栈原理(二) -- URLSessionTask层

1,852 阅读4分钟

iOS网络协议栈原理(二) -- URLSessionTask层

上一篇文章总结到, URLSession 是一个生产URLSessionDataTask的工厂, 其中有几个关键内容, 会在这一节中解释:

  1. 在创建task时, 会将 URLSession实例传入, 也就是说 task 可能会在内部持有 URLSession
  2. 真正发起请求时, 需要调用task.resume()方法 -- 那么Task究竟是如何工作的

1. Task的初始化创建与HTTP Body包装

简单归纳一下Task的初始化的关键信息:

  1. Task的回调方式: 关联回调方式, 是通过Behaviour + taskIdentifier 存储在URLSession
  2. _Body枚举: 根据URLRequesthttpBody or httpBodyStream的成员, 在内部统一使用_Body枚举收口, 具会分成4类.
  3. 参考以下代码注释
open class URLSessionTask : NSObject, NSCopying {
    ...

    // 构造 SessionTask 的 Body 可能有4种情况
    enum _Body {
        case none
        case data(DispatchData) // 为啥用 DispatchData 是一个内部二进制容器类型, 实际会防止copy, 和传入的 request.httpBody 共用同一份内存
        /// Body data is read from the given file URL
        case file(URL) // 其他的 Task会使用 URL模式的 body
        case stream(InputStream)
    }

    // 核心方法!
    internal convenience init(session: URLSession, request: URLRequest, taskIdentifier: Int) {
        if let bodyData = request.httpBody, !bodyData.isEmpty {
            self.init(session: session, request: request, taskIdentifier: taskIdentifier, body: _Body.data(createDispatchData(bodyData)))
        } else if let bodyStream = request.httpBodyStream {
            self.init(session: session, request: request, taskIdentifier: taskIdentifier, body: _Body.stream(bodyStream))
        } else {
            // 其他场景, 没有body -> 例如使用 GET 请求时
            self.init(session: session, request: request, taskIdentifier: taskIdentifier, body: _Body.none)
        }
    }

    // 更加重要的方法
    internal init(session: URLSession, request: URLRequest, taskIdentifier: Int, body: _Body?) {
        // 1. task 会强引用创建它的 session, 并且在 task 结束的时候 self.session = nil
        self.session = session

        // 2. 将 workQueue的 targetQueue 设置成 session.workQueue 
        // 3. 后续有大量关于 task.state 的操作都在这个 serial queue 中完成!!!
        self.workQueue = DispatchQueue.init(label: "org.swift.URLSessionTask.WorkQueue", target: session.workQueue)
        // 4. 唯一 task Identifier, 使用这个唯一标记, 可以通过 URLSession 关联 CompletionHandler
        self.taskIdentifier = taskIdentifier
        // 5. 持有一份外部传入的 Request!!! 因为整个请求过程中, 可能遇到 HTTP Status Code = 302的情况, 需要重定向, 可能一次Task过程会发起多个请求
        self.originalRequest = request // 原始的请求
        // 6. 收口以后的 http body
        self.knownBody = body
        super.init()
        
        // 7. 同 5, 初始化时, 当前的请求就是初始化的request (后面如果有302重定向场景, Task会创建一个新的指向重定向路径的request, 那时会修改 currentRequest)
        self.currentRequest = request
        
        // 8. 忽略...
        self.progress.cancellationHandler = { [weak self] in
            self?.cancel()
        }
    }
}

2. Task的内部状态机

前面我们知道, 在使用Session工厂, 创建Task实例以后, 需要主动调用task.resume()方法启动这个任务!!! why???

实际上在task内部维护了一个state!!! 并且通过一个成员变量suspendCount来记录这个Task任务是否在挂起状态!!!

extension URLSessionTask {
    
    /// How many times the task has been suspended, 0 indicating a running task.
    internal var suspendCount = 1

    public enum State : Int {
        /// The task is currently being serviced by the session
        case running
        case suspended // 初始是 suspended
        /// The task has been told to cancel.  The session will receive a URLSession:task:didCompleteWithError: message.
        case canceling // 退出 ing!!!!!! 只是标记 canceling -> 但是没 completed
        /// The task has completed and the session will receive no more delegate notifications
        case completed
    }

    /// Updates the (public) state based on private / internal state.
    ///
    /// - Note: This must be called on the `workQueue`.
    internal func updateTaskState() {
        func calculateState() -> URLSessionTask.State {
            if suspendCount == 0 {
                return .running
            } else {
                return .suspended
            }
        }
        state = calculateState()
    }

    // 核心方法
    open func resume() {
        workQueue.sync {
            // 忽略 canceling 和 completed
            guard self.state != .canceling, self.state != .completed else {
                return
            }
            
            if self.suspendCount > 0 {
                self.suspendCount -= 1
            }
            
            self.updateTaskState()
            
            if self.suspendCount == 0 {
                ...

                // 真正的启动任务
            }
        }
    }

    // 核心方法
    open func cancel() {
        workQueue.sync {
            let canceled = self.syncQ.sync { () -> Bool in
                guard self._state == .running || self._state == .suspended else {
                    return true 
                }
                self._state = .canceling
                return false
            }
            guard !canceled else {
                return 
            }

            ...
        }
    }
}

通过以上代码能看出task的两个核心方法, 主要就是切换task.state, 并且关于state的操作都是在workQueue中完成的:

  1. 初始化时, task.state => .suspended
  2. 当外部调用resume(), task.state => .running, 然后真正启动耗时任务, 具体的耗时任务是交给另外一个URLProtocl的实例完成的, 具体启动任务的逻辑我们后面分析!!!
  3. 当外部调用cance(), task.state => .canceling, 表示task已经取消, 然后去处理底层的资源清理工作
  4. task关联的URLProtocl的实例的清理工作完成以后, 会调用task.state => .completed!!!

因此, 简单来说, Task并不是自己去执行网络请求, 而是又抽象了一层, 真正的苦力活都是交给URLProtocol去做的!!!

另外Task功能性不同, Apple 创建多个子类来进行差异化任务的实现, 另外, task progress 以及HTTP 302重定向等逻辑都是在task层实现的:

open class URLSessionDataTask: URLSessionTask {}

/*
 * An URLSessionUploadTask does not currently provide any additional
 * functionality over an URLSessionDataTask.  All delegate messages
 * that may be sent referencing an URLSessionDataTask equally apply
 * to URLSessionUploadTasks.
 */
open class URLSessionUploadTask: URLSessionDataTask {}

/*
 * URLSessionDownloadTask is a task that represents a download to
 * local storage.
 */
open class URLSessionDownloadTask: URLSessionTask {
    ...
}