iOS網路協議棧原理(二) -- URLSessionTask層

語言: CN / TW / HK

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. 參考以下程式碼註釋

```swift 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任務是否在掛起狀態!!!

```swift 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層實現的:

```swift 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 { ... } ```