iOS網路協議棧原理(六) -- URLProtocolClient

語言: CN / TW / HK

iOS網路協議棧原理(六) -- URLProtocolClient

URLProtocolClient - 資料從_NativeProtocol 與上層互動的協議

從前面看出, 在curl真實的回調出發時, 部分事件是需要_NativeProtocol/_HTTPURLProtocol向上層回撥的, APPLE 抽象了一個協議來約束具體的方法內容:

```swift /! @protocol URLProtocolClient @discussion URLProtocolClient provides the interface to the URL loading system that is intended for use by URLProtocol implementors. / public protocol URLProtocolClient : NSObjectProtocol { /! @method URLProtocol:wasRedirectedToRequest: @abstract Indicates to an URLProtocolClient that a redirect has occurred. @param URLProtocol the URLProtocol object sending the message. @param request the NSURLRequest to which the protocol implementation has redirected. / func urlProtocol(_ protocol: URLProtocol, wasRedirectedTo request: URLRequest, redirectResponse: URLResponse)

/*!
 @method URLProtocol:cachedResponseIsValid:
 @abstract Indicates to an URLProtocolClient that the protocol
 implementation has examined a cached response and has
 determined that it is valid.
 @param URLProtocol the URLProtocol object sending the message.
 @param cachedResponse the NSCachedURLResponse object that has
 examined and is valid.
 */
func urlProtocol(_ protocol: URLProtocol, cachedResponseIsValid cachedResponse: CachedURLResponse)

/*!
 @method URLProtocol:didReceiveResponse:
 @abstract Indicates to an URLProtocolClient that the protocol
 implementation has created an URLResponse for the current load.
 @param URLProtocol the URLProtocol object sending the message.
 @param response the URLResponse object the protocol implementation
 has created.
 @param cacheStoragePolicy The URLCache.StoragePolicy the protocol
 has determined should be used for the given response if the
 response is to be stored in a cache.
 */
func urlProtocol(_ protocol: URLProtocol, didReceive response: URLResponse, cacheStoragePolicy policy: URLCache.StoragePolicy)

/*!
 @method URLProtocol:didLoadData:
 @abstract Indicates to an NSURLProtocolClient that the protocol
 implementation has loaded URL data.
 @discussion The data object must contain only new data loaded since
 the previous call to this method (if any), not cumulative data for
 the entire load.
 @param URLProtocol the NSURLProtocol object sending the message.
 @param data URL load data being made available.
 */
func urlProtocol(_ protocol: URLProtocol, didLoad data: Data)

/*!
 @method URLProtocolDidFinishLoading:
 @abstract Indicates to an NSURLProtocolClient that the protocol
 implementation has finished loading successfully.
 @param URLProtocol the NSURLProtocol object sending the message.
 */
func urlProtocolDidFinishLoading(_ protocol: URLProtocol)

/*!
 @method URLProtocol:didFailWithError:
 @abstract Indicates to an NSURLProtocolClient that the protocol
 implementation has failed to load successfully.
 @param URLProtocol the NSURLProtocol object sending the message.
 @param error The error that caused the load to fail.
 */
func urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error)

/*!
 @method URLProtocol:didReceiveAuthenticationChallenge:
 @abstract Start authentication for the specified request
 @param protocol The protocol object requesting authentication.
 @param challenge The authentication challenge.
 @discussion The protocol client guarantees that it will answer the
 request on the same thread that called this method. It may add a
 default credential to the challenge it issues to the connection delegate,
 if the protocol did not provide one.
 */
func urlProtocol(_ protocol: URLProtocol, didReceive challenge: URLAuthenticationChallenge)

/*!
 @method URLProtocol:didCancelAuthenticationChallenge:
 @abstract Cancel authentication for the specified request
 @param protocol The protocol object cancelling authentication.
 @param challenge The authentication challenge.
 */
func urlProtocol(_ protocol: URLProtocol, didCancel challenge: URLAuthenticationChallenge)

} ```

這套協議, 在URLProtocol的幾個方法中用的非常多, 例如 resumecompleteTask 方法:

```swift internal class _NativeProtocol: URLProtocol, _EasyHandleDelegate {

...

func resume() {
    if case .initial = self.internalState {
        guard let r = task?.originalRequest else {
            fatalError("Task has no original request.")
        }

        // 先去檢測 cache!!!
        // Check if the cached response is good to use:
        if let cachedResponse = cachedResponse, canRespondFromCache(using: cachedResponse) {
            // cacheQueue 中非同步回撥
            self.internalState = .fulfillingFromCache(cachedResponse)

            // 我們自定義的API中, 拿不到 workQueue!!! 因此這裡只能用比較hack的方法 呼叫
            // 但是這裡本來就是在 workQueue 中執行, 又重新非同步 workQueue.async ??
            task?.workQueue.async {
                // 真實的服務 -> 無所謂呼叫
                self.client?.urlProtocol(self, cachedResponseIsValid: cachedResponse)
                // 直接呼叫 receive Responsd
                self.client?.urlProtocol(self, didReceive: cachedResponse.response, cacheStoragePolicy: .notAllowed)
                // 呼叫 didLoad:(data)
                if !cachedResponse.data.isEmpty {
                    self.client?.urlProtocol(self, didLoad: cachedResponse.data)
                }
                // 呼叫didFinishLoading
                self.client?.urlProtocolDidFinishLoading(self)
                self.internalState = .taskCompleted
            }
        } else {
            // workQueue 中執行!
            startNewTransfer(with: r)
        }
    }

    if case .transferReady(let transferState) = self.internalState {
        self.internalState = .transferInProgress(transferState)
    }
}


func completeTask() {
    guard case .transferCompleted(response: let response, bodyDataDrain: let bodyDataDrain) = self.internalState else {
        fatalError("Trying to complete the task, but its transfer isn't complete.")
    }
    task?.response = response
    // We don't want a timeout to be triggered after this. The timeout timer needs to be cancelled.
    easyHandle.timeoutTimer = nil
    // because we deregister the task with the session on internalState being set to taskCompleted
    // we need to do the latter after the delegate/handler was notified/invoked
    if case .inMemory(let bodyData) = bodyDataDrain {
        var data = Data()
        if let body = bodyData {
            withExtendedLifetime(body) {
                data = Data(bytes: body.bytes, count: body.length)
            }
        }
        self.client?.urlProtocol(self, didLoad: data)
        self.internalState = .taskCompleted
    } else if case .toFile(let url, let fileHandle?) = bodyDataDrain {
        self.properties[.temporaryFileURL] = url
        fileHandle.closeFile()
    } else if task is URLSessionDownloadTask {
        let fileHandle = try! FileHandle(forWritingTo: self.tempFileURL)
        fileHandle.closeFile()
        self.properties[.temporaryFileURL] = self.tempFileURL
    }

    // 呼叫 loadData -> 直接呼叫 FinishLoading
    self.client?.urlProtocolDidFinishLoading(self)
    self.internalState = .taskCompleted
}

} ````

從以上的 Protocol ProtocolClient{ ... } 的核心方法基本都是 URLProtocol 物件與上層URLSessionTask通訊的方法!!!

這裡URLProtocol/URLProtocolClient使用的代理模式, 我們可以將_NativeProtocol物件中的open var client: URLProtocolClient?屬性, 直接看做這樣var delegate: URLProtocolDelegate?!!! 就能很好理解了.

另外, 從全域性來看:

  1. URLSessionTask的核心資料請求的過程交給了URLProtocol
  2. URLProtocol為了隔離向上回撥的資料, 使用了代理模式, 並且代理物件是URLProtocol自己 !!! 在建構函式時, 內部建立一個private var _client : URLProtocolClient?, 實際是class _ProtocolClient: URLProtocolClient 的例項物件.
  3. _ProtocolClient包裝了很多向上回調的方法, 完全從URLProtocol的作用域中剝離出來:
    1. URLProtocol收到HTTP Response Header時 回撥 -- urlProtocol(_ protocol: URLProtocol, didReceive response: URLResponse, cacheStoragePolicy policy: URLCache.StoragePolicy)
    2. URLProtocol 收到 HTTP Response Data時 回撥(可能多次回撥) -- urlProtocol(_ protocol: URLProtocol, didLoad data: Data)
    3. URLProtocol 結束資料傳輸時回撥 -- urlProtocolDidFinishLoading(_ protocol: URLProtocol)
    4. URLProtocol 在資料傳輸中出錯時回撥 -- urlProtocol(_ protocol: URLProtocol, didFailWithError error: Error)
    5. ...

_ProtocolClient - 真正幫助_NativeProtocol進行向上回撥的包裝類

先簡單看一下, 它的定義, 然後實現了URLProtocolClient擴充套件:

```swift /* 多個維度的快取: 1. 快取策略 cachePolicy 2. cacheableData 二進位制 3. response 快取

實現 ProtocolClient Protocol */ internal class _ProtocolClient : NSObject { var cachePolicy: URLCache.StoragePolicy = .notAllowed var cacheableData: [Data]? var cacheableResponse: URLResponse? }

/// 具體的程式碼可以參考 swift-foundation 原始碼 extension _ProtocolClient: URLProtocolClient {

...

} ```

從原始碼中可以整理出, _ProtocolClient 在實現URLProtocolClient 過程中, 還幫助完成了如下的事情:

  1. response cache: HTTPResponse Cache相關的內容, 包括 response header 和 response data!
  2. authenticationChallenge: HTTPS 證書鑑權的工作, 以及對Session幾遍的HTTPS鑑權結果進行快取, 等待後續複用.
  3. 根據Task的回撥方式(delegate, block), 呼叫task.delegate不同的回撥方法!!!
  4. 在URLProtocol告知Client請求結束時, 進行session.taskRegistry.remove(task)操作!!!

兩句話小結URLProtocolURLProtocolClient的關係:

  1. URLProtocol的本職工作是獲取資料, 它只關心獲取資料的過程與結果, 具體將結果交給誰, 它並不關心!!!
  2. 引入URLProtocolClient來解耦URLProtocol, 將URLProtocol的非本職工作統一提取到URLProtocolClient. 本質上是委託的設計模式!!!