URL Loading System

文章目录
  1. 1. 简介
  2. 2. 核心 URLSession & URLSessionTask
    1. 2.1. Session Configuration
    2. 2.2. session 网络请求
  3. 3. 实际使用
    1. 3.1. 从网站获取数据到内存
      1. 3.1.1. 使用回调处理接受结果:使用closure 回调得到 data
      2. 3.1.2. 接受请求详情&结果:使用 Delegate

简介

  • 在 Foundation 框架中
  • 支持协议:
    • FTP协议(ftp://)
    • 超文本传输协议(http://)
    • 加密超文本传输协议(https://)
    • 本地资源(file://)
    • 数据URLs(data://)
  • 关键类:URLSession,URL,URLRequest,Tasks,Data
  • 支持:Authorization credentials,cache 和cookies, 配置管理
  • 使用 closure 和 delegate 处理 Response

url_loading_system 核心类 分 6 个部分

核心 URLSession & URLSessionTask

一个 session 请求网络数据需要的核心内容

urlsession

Session Configuration

每个配置属性都值得研究
configuration
这篇文章有各个参数翻译

总共有三种 configuration

  • Default: 使用本地沙盒缓存,用户credential保存在 keychain 中
  • Ephemeral: 不使用沙盒,所有缓存credential 等数据存在 与 session 绑定的 RAM 中,session 失效后内存自动清理
  • Background: 跟 default session 相似,但是使用的是另外一个 进程(process) 处理数据传输,因为是跨进程,所以 background session 有些限制操作。(嗯后台session 使用了进程间通信)

为什么要有 configuration?
session 相当一个 manager,用来管理组织 task,网络事件处理的枢纽,configuration是这个枢纽的参数信息!相当于 一个 app 他都有 自己 preference

1
2
3
4
5
6
7
8
9
10
11
12
13
let configuration = URLSessionConfiguration.background(withIdentifier: "com.czw.backgroundDownload")
let configuration1 = URLSessionConfiguration.default// 允许用户拥有沙盒缓存器,当session 释放的时候,数据依然存在
let configuration2 = URLSessionConfiguration.ephemeral// session 无效时,东西就会消失

URLSession.init(configuration: configuration).dataTask(with: URL(string: urlDownloadStr)!)
.resume()// 后台 sessionConfiguration 不能设置 completeHandler,需要自己设置代理

print("background-内存大小:\(String(describing: configuration.urlCache.memoryCapacity))")
print("background-沙盒大小:\(String(describing: configuration.urlCache.diskCapacity))")
print("default-内存大小:\(String(describing: configuration1.urlCache.memoryCapacity))")
print("default-沙盒大小:\(String(describing: configuration1.urlCache.diskCapacity))")
print("ephemeral-内存大小:\(String(describing: configuration2.urlCache.memoryCapacity))")
print("ephemeral-沙盒大小:\(String(describing: configuration2.urlCache.diskCapacity))")

打印结果:
background-内存大小:nil
background-沙盒大小:nil
default-内存大小:512000
default-沙盒大小:10000000
ephemeral-内存大小:512000
ephemeral-沙盒大小:0

就像文档说的,当session 失效以后 ephemeral 的 cache 会消失,因为他没有沙盒空间

session 网络请求

urlsession request flow

我们要做的

  1. 配置需要的 session configuration,
  2. 配置 session
    1. 系统提供的 shared 单例,系统自己配置 configuration,sessionDelegate,delegateQueue
    2. 自定义 session,自己设置 configuration,sessionDelegate,delegateQueue(是 serial queue)
  3. 提供 request & url 给 session
  4. 生成 task
    1. URLSessionTask
      • URLSessionDataTask
      • URLSessionUploadTask
      • URLSessionDownloadTask
      • URLSessionStreamTask
      • URLSessionWebSocketTask(iOS 13.0)
  5. 配置SessionDelegate, SessionTaskDelegate 对网络事件处理 (URLSessionTaskDelegate: URLSessionDelegate
    • URLSessionDataDelegate
    • URLSessionDownloadDelegate
    • URLSessionStreamDelegate
    • URLSessionWebSocketDelegate(iOS 13.0)

SessionDelegate 只有3个接口

1
2
3
4
5
6
7
8
@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, didBecomeInvalidWithError error: Error?)

@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

@available(iOS 7.0, *)
optional func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession)

至于各个 taskDelegate,对应不同的task 类型有个字不同协议接口
这里主要看 SessionTaskDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public protocol URLSessionTaskDelegate : URLSessionDelegate { 
@available(iOS 11.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, willBeginDelayedRequest request: URLRequest, completionHandler: @escaping (URLSession.DelayedRequestDisposition, URLRequest?) -> Void)

@available(iOS 11.0, *)
optional func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask)

@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest, completionHandler: @escaping (URLRequest?) -> Void)

@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, needNewBodyStream completionHandler: @escaping (InputStream?) -> Void)

@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)

@available(iOS 10.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)

@available(iOS 7.0, *)
optional func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
}

常用的是 dataTask,downloadTask,uploadTask

实际使用

从网站获取数据到内存

使用回调处理接受结果:使用closure 回调得到 data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// task 的状态
extension URLSessionTask {
@available(iOS 7.0, *)
public enum State : Int {
case running
case suspended // 初试状态
case canceling
case completed
}
}

func startLoad() {
let url = URL(string: "https://www.example.com/")!
let task = URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
self.handleClientError(error)
return
}
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
self.handleServerError(response)
return
}
if let mimeType = httpResponse.mimeType, mimeType == "text/html",
let data = data,
let string = String(data: data, encoding: .utf8) {
DispatchQueue.main.async {
self.webView.loadHTMLString(string, baseURL: url)
}
}
}

// session create 出来的 task 是 suspend 的状态
task.resume()
}

注意点:

  1. session create 出来的 task 初始状态是 suspend
  2. 回调处理是在 serial queue 任务放到子线程处理的,通知 UI 变化应切换主线程

接受请求详情&结果:使用 Delegate

这个时候 session 不应该使用 shared,而是要自己配置

1
2
3
4
5
6
7
private lazy var session: URLSession = {
let configuration = URLSessionConfiguration.default
configuration.waitsForConnectivity = true
return URLSession(configuration: configuration,
delegate: self,
delegateQueue: nil)
}()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
var receivedData: Data?

func startLoad() {
loadButton.isEnabled = false
let url = URL(string: "https://www.example.com/")!
receivedData = Data()
let task = session.dataTask(with: url)
task.resume()
}

// delegate methods

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse,
completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
guard let response = response as? HTTPURLResponse,
(200...299).contains(response.statusCode),
let mimeType = response.mimeType,
mimeType == "text/html" else {
completionHandler(.cancel)
return
}
completionHandler(.allow)
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
self.receivedData?.append(data)
}

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
DispatchQueue.main.async {
self.loadButton.isEnabled = true
if let error = error {
handleClientError(error)
} else if let receivedData = self.receivedData,
let string = String(data: receivedData, encoding: .utf8) {
self.webView.loadHTMLString(string, baseURL: task.currentRequest?.url)
}
}
}

使用代理的方式,可以处理很多特殊的情况:

  • authentication challenges
  • following redirects

URL Loading System