一、NSURLSession
NSURLSession的主要功能是发起网络请求获取网络数据,是Apple的网络请求原生库之一。Alamofire就是对NSURLSession的封装,如果对NSURLSession不熟悉的话,那么Alamofire源码看起来会比较费劲的。因此我们先简单学习下NSURLSession。
从NSURLSession这个名字中我们不难看出,主要是URL + Session。顾名思义,NSURLSession是用来URL会话的。iOS的NSURLSession的主要功能是通过URL与服务器交流的。
一句话总结:我们的iOS客户端可以使用NSURLSession这个东西通过相应的URL与我们的服务器建立会话,然后通过此会话来完成一些交互任务(NSURLSessionTask)。Session有着不同的类型,每种类型的Session又可以执行不同类型的任务(Task)。接下来就来介绍一下Session的类型以及所执行的任务等。
1.NSURLSession的类型
在使用NSURLSession时你得知道你使用的是哪种类型的Session。从官方的NSURLSession API中不难看出,共有三种类型的Session:Default sessions,Ephemeral sessions,Background sessions。这三种Session我们可以通过NSURLSessionConfiguration来指定。
- 默认会话(Default Sessions)使用了持久的磁盘缓存,并且将证书存入用户的钥匙串中。它的特点是使用系统的缓存和凭证存储机制,适合处理需要缓存数据或保持会话状态的网络请求。
- 临时会话(Ephemeral Session)没有像磁盘中存入任何数据,与该会话相关的证书、缓存等都会存在RAM中。因此当你的App临时会话无效时,证书以及缓存等数据就会被清除掉。因此,它非常适合那些对隐私要求较高的场景。
- 后台会话(Background sessions)除了使用一个单独的线程来处理会话之外,与默认会话类似。适合处理需要长时间执行的任务,如文件下载和上传。此类会话通常会在后台传输文件,即使应用程序被挂起或终止,网络任务仍能继续执行,且完成后会通知应用。
Session在初始化时可以指定下方的任意一种SessionConfiguration:
let defaultSession = URLSession(configuration: .default)
let ephemeralSession = URLSession(configuration: .ephemeral)
let backgroundSession = URLSession(configuration: .background(withIdentifier: ".background"))
2. NSURLSession的各种任务
在一个Session会话中可以发起的任务可分为三种:数据任务(Data Task)、下载任务(Download Task)、上传任务(Upload Task)。在iOS8和OS X 10.10之前的版本中后台会话是不支持Data Task。下面来简述一下这三种任务。
- Data Task(数据任务)负责使用NSData对象来发送和接收数据。Data Task是为了那些简短的并且经常从服务器请求的数据而准备的。该任务可以每请求一次就对返回的数据进行一次处理。
- Download task(下载任务)以表单的形式接收一个文件的数据,该任务支持后台下载。
- Upload task(上传任务)以表单的形式上传一个文件的数据,该任务同样支持后台下载。
3、URL
(1)URL编码概述
无论是GET、POST还是其他的请求,与服务器交互的URL是需要进行编码的。因为进行URL编码的参数服务器那边才能进行解析,为了能和服务器正常的交互,我们需要对我们的参数进行转义和编码。URL就是互联网上资源的地址,用户和服务器都可以通过URL来找到其想访问的资源。RFC3986文档规定,URL中只允许包含英文字母(a-zA-Z)、数字(0-9)、“-_.~”4个特殊字符以及所有保留字符,如果你的URL中含有汉字,那么就需要对其进行转码了。RFC3986中指定了以下字符为保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ]。
(2)URL编码的规则
在URL编码时有一定的规则,如下图是我们今天主要使用的URL格式的一个规则的一个图解:
需要注意的是图中Query的部分。其中,Path和Query之间使用的是?号进行分隔的,问号后边就是我们要传给服务器的参数了,该参数就是下方的Query的部分。在GET请求中Query是存放在URL后边,而在POST中是放在Request的Body中。
如果你的参数只是一个key-Value, 那么Query的形式就是key = value。如果你的参数是一个数组比如key = [itme1, item2, item3,……],那么你的Query的格式就是key[]=item1&key[itme2]&key[item3]……。如果你的参数是一个字典比如key = ["subKey1":"item1", "subKey2":"item2"], 那么Query对应的形式就是key[subKey1]=item1&key[subKey2]=item2.
接下来我们要做的就是将字典进行URL编码。
(3)将Dictionary进行URL编码
在iOS开发中,有时候我们从VC层或者VM层获取到的数据是一个字典,字典中存储的就是要发给服务器的数据参数。直接将字典转成二进制数据发送给服务器,服务器那边是没法解析iOS这边的字典的,得有一个统一的交互标准,这个标准就是URL编码。我们要做的就是讲字典进行URL编码,然后将编码后的东西在传给服务器,这样一来服务器那边就能解析到我们请求的参数了。下方折叠的这段代码就是从AlamoFire框架中摘抄出来的几个方法,位于ParameterEncoding.swift文件中。该段代码就是负责将字典类型的参数进行URL编码的,在编码过程中进行转义是少不了的。
import Foundation
//MARK: Alamofire中的三个方法该方法将字典转换成URL编码的字符
/*
用于将键值对转换为 URL 查询参数的形式。它递归地处理嵌套的字典和数组,
并根据键和值生成合适的查询组件(键值对)。
这些查询组件通常用于将复杂的字典或数组编码成 HTTP 请求 URL 中的查询字符串参数。
*/
private func query(_ parameters: [String: Any]) -> String {
var components: [(String, String)] = []
for key in parameters.keys.sorted(by: <) {
let value = parameters[key]!
components += queryComponents(fromKey: key, value: value)
}
return components.map { "\($0)=\($1)" }.joined(separator: "&")
}
public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
var components: [(String, String)] = []
//如果 value 是一个字典类型 [String: Any],这里会将字典的键用方括号括起来进行拼接,比如如果传入的 key 是 "user",而 nestedKey 是 "name",那么组合后的键会变为 "user[name]",从而表示字典的嵌套结构。
if let dictionary = value as? [String: Any] {
for (nestedKey, value) in dictionary {
components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
}
} //如果 value 是数组类型 [Any],代码会遍历数组中的每个值。
else if let array = value as? [Any] {
for value in array {
components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
}
} //如果 value 是 NSNumber,首先判断它是否是布尔类型。
else if let value = value as? NSNumber {
if value.isBool {//如果是布尔值,使用 boolEncoding.encode 方法来处理布尔值的编码(通常布尔值会被转换为 "true" 或 "false" 字符串)。
components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
} else {//如果是数字类型,则直接将其转为字符串并添加到 components 数组中。
components.append((escape(key), escape("\(value)")))
}
} //如果 value 是布尔类型 Bool,直接使用 boolEncoding.encode 方法来进行编码,然后将结果添加到 components 数组中。
else if let bool = value as? Bool {
components.append((escape(key), escape(boolEncoding.encode(value: bool))))
} else {//如果 value 是其他任何类型(如字符串等),直接将 value 转换为字符串并添加到 components 中。
components.append((escape(key), escape("\(value)")))
}
return components
}
这个方法通过递归地处理字典、数组、布尔值和数字,将它们转换成一对对的查询参数。它能够处理复杂的嵌套数据结构,并将它们编码成 URL 查询字符串所需的形式。之所以进行递归,因为字典中有可能含有字典或者数组,数组中又可能嵌套着数组或者字典。所以要进行递归,直到找到key=value这种形式为止。
调用上述代码段的query()方法就可以对字典进行转义。query()方法的参数是一个[String, AnyObject]类型的字典,返回参数是一个字符串。这个返回的字符串就是将该字典进行编码后的结果。
总的来讲,就是与服务器交互发送的URL是需要编码的,而编码是需要遵从某种规则,这种规则在Alamofire中是以query()和queryComponents()方法中进行的,并且规则的本质上就是将键值对转换成一个字符串。
tips:关于数据的传输:
- GET 请求 将数据作为查询参数附加在 URL 中发送。
- POST 请求 将数据放在 请求体 中,并通过 Content-Type 告知服务器如何解析数据(例如:json)。
4.URLSessionDelegate 代理机制
URLSessionDelegate 代理机制是 NSURLSession 的一部分,用于处理与网络请求相关的各种事件。
NSURLSession用法:
1.GET--从服务器请求数据
import Foundation
//MARK: GET--从服务器请求数据
let myURLString = "https://httpbin.org/get"
let url = URL(string: myURLString)!
let urlRequest = URLRequest(url: url)
let session = URLSession.shared
// 创建 dataTask
let task = session.dataTask(with: urlRequest) { data, response, error in
// 检查是否有错误
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
// 检查响应是否为 HTTP 响应
if let httpResponse = response as? HTTPURLResponse {
print("Status code: \(httpResponse.statusCode)")
}
// 确保有数据返回
guard let data = data else {
print("No data received")
return
}
// 尝试将数据转换为 JSON 格式
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("JSON response: \(json)")
}
} catch {
print("Failed to serialize JSON: \(error.localizedDescription)")
}
}
// 启动任务
task.resume()
在GET样例代码中,可以看到,首先我们创建了一个URL,然后构建了URLRequest对象,接着通过 URLSession.shared 创建一个共享的会话(session),这样可以复用会话中的缓存、Cookie 等信息。紧接着使用 URLSession 的 dataTask(with:) 方法创建一个任务,并为其指定回调闭包,用于处理请求完成后的响应和数据。最后,我们将数据转成JSON格式。
2.POST--向服务器发送数据
import Foundation
let myURLString = "https://httpbin.org/post"
let url = URL(string: myURLString)!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
//设置请求头(Header)--“application/json” -> 告诉服务器我们将以 JSON 格式发送数据
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
//构建请求体(Parameters)--要发送的数据
let parameters: [String: Any] = [
"name": "John",
"age": 30
]
// 将请求体 转换为 JSON 格式 -- 使用 JSONSerialization 将字典序列化为 Data 对象(即 JSON 格式的二进制数据)
do {
let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])
urlRequest.httpBody = jsonData
} catch {
print("Error in JSON serialization: \(error.localizedDescription)")
}
// 创建 URLSession 并发送请求
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { data, response, error in
// 检查是否有错误
if let error = error {
print("Error: \(error.localizedDescription)")
return
}
// 检查响应是否为 HTTP 响应
if let httpResponse = response as? HTTPURLResponse {
print("Status code: \(httpResponse.statusCode)")
}
// 确保有数据返回
guard let data = data else {
print("No data received")
return
}
// 尝试将数据转换为 JSON 格式
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
print("Response JSON: \(json)")
}
} catch {
print("Failed to serialize JSON: \(error.localizedDescription)")
}
}
// 启动任务
task.resume()
在POST样例代码中,可以看到,我们需要发送请求给服务器,请求主要分为请求头、请求体:请求头是告诉服务器如何处理请求(告诉服务器应该如何解析数据),以及携带关于客户端的环境、数据格式、认证等内容、请求体是实际要通过 HTTP 请求传递给服务器的数据。
其余与GET大差不差。。。
基本流程:
创建一个URLRequest 对象,URLRequest 对象描述了发送到服务器的 HTTP 请求,包括 URL、请求体、请求头等信息。然后通过 NSURLSession 使用 dataTask 将 URLRequest 发送到服务器。
二、Alamofire
博客中的Alamofire源码的版本是以5.6.4版本为例。其平台较少,不像最新版本新添了visionOS等一系列新平台,框架较大不容易阅读。
在上述NSURLSession的学习中我们大概了解了NSURLSession是如何工作的:先建立URLRequest
, URLSession
, 接着创建 dataTask
,并调用方法resume()
去唤醒. 结束以后通过回调dataTask
告诉我们结果。当然最后需要通过序列化JSONSerialization
把二进制Data转换为JSON,或者处理错误errors。
Alamofire
实际上是封装了URLSession
,所以Alamofire
也会创建dataTask
,然后调用方法.resume()
去唤醒执行. 接下来就进一步揭露Alamofire
的神秘面纱。
我们从Alamofire的用法反推其具体流程。
以下是我在项目中Alamofire的一种用法:
AF.request("https://nlp.aliyuncs.com/v2/api/chat/send",
method: .post,
parameters: parameters,
encoding: JSONEncoding.default,
headers: headers).response
{ response in
}
我们看底层封装代码:
open func request(_ convertible: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil,
interceptor: RequestInterceptor? = nil,
requestModifier: RequestModifier? = nil) -> DataRequest {
let convertible = RequestConvertible(url: convertible,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers,
requestModifier: requestModifier)
return request(convertible, interceptor: interceptor)
}
方法参数:
- convertible: URLConvertible :请求的 URL,可以是 String 或 URL 类型,Alamofire 会将其转换为 URL。
- method: HTTPMethod = .get :HTTP 请求方法,默认为 GET,可设为 POST、PUT 等。
- parameters: Parameters? = nil :请求体、请求的参数(字典 [String: Any]),用于发送数据。
- encoding: ParameterEncoding = URLEncoding.default :参数的编码方式,常见有:
URLEncoding.default :GET 请求将参数编码为 URL 查询字符串。
JSONEncoding.default :POST 请求会将参数作为请求体的 JSON 数据发送。
- headers: HTTPHeaders? = nil :自定义请求头,可传入 HTTPHeaders 类型对象。
- interceptor: RequestInterceptor? = nil :拦截器,用于修改请求或处理重试策略等。
- requestModifier: RequestModifier? = nil :请求修改器,在请求创建后进一步调整请求。
此外,可以看到方法中构建了一个RequestConvertible对象,这是一个结构体对象,是 Alamofire 内部的一个工具类,用于将 URL、请求方法、参数、编码等组合成 URLRequest。
让我们看看RequestConvertible的源码:
struct RequestConvertible: URLRequestConvertible {
let url: URLConvertible
let method: HTTPMethod
let parameters: Parameters?
let encoding: ParameterEncoding
let headers: HTTPHeaders?
let requestModifier: RequestModifier?
func asURLRequest() throws -> URLRequest {
var request = try URLRequest(url: url, method: method, headers: headers)
try requestModifier?(&request)
return try encoding.encode(request, with: parameters)
}
}
我们重点关注下asURLRequest()方法:
var request = try URLRequest(url: url, method: method, headers: headers)
根据传入的 URL、HTTP 方法、和请求头来创建一个基础的 URLRequest 对象(可以看成一个数据包,描述了发送到服务器的 HTTP 请求,包括 URL、请求体、请求头等信息)。
return try encoding.encode(request, with: parameters)
encoding.encode:根据指定的 ParameterEncoding(如 URLEncoding 或 JSONEncoding),将请求参数编码到 URLRequest 中。
• GET 请求:参数通常会被编码为 URL 查询字符串(附加在 URL 后面)。
• POST 请求:参数通常会被编码为请求体中的 JSON。
• 最终返回经过编码的 URLRequest,这是发送 HTTP 请求的最终形态。
由此可见,RequestConvertible的作用是将 Alamofire 的网络请求参数(包括 URL、方法、参数、编码、请求头等)打包并生成 URLRequest。
接着我们返回到AF.request()方法中,我们再来深入了解下返回值:
return request(convertible, interceptor: interceptor)
底层实现:
open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest {
let request = DataRequest(convertible: convertible,
underlyingQueue: rootQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: self)
perform(request)
return request
}
其中convertible就是我们上面了解的RequestConvertible,也就是说,这里参数输入的是一个URLRequest对象,另外一个参数interceptor: RequestInterceptor?是请求拦截器,允许在请求执行之前或之后进行额外的操作,如重试请求、修改请求等。它是一个可选参数,默认值为 nil。
此外,DataRequest是 Alamofire 用于管理网络请求的核心对象。它继承自 Request,封装了请求的生命周期管理、进度跟踪、响应解析等功能。其底层是一些初始化,用于赋值,这里就不多赘述。
我们重点了解下perform(request)的底层代码:
var activeRequests: Set<Request> = []
public let rootQueue: DispatchQueue
func perform(_ request: Request) {
rootQueue.async {
guard !request.isCancelled else { return }
self.activeRequests.insert(request)//将当前请求加入到 activeRequests 集合中,这个集合存储所有正在执行的请求,便于管理和追踪。
self.requestQueue.async {
switch request {
case let r as UploadRequest: self.performUploadRequest(r) // UploadRequest must come before DataRequest due to subtype relationship.
case let r as DataRequest: self.performDataRequest(r)
case let r as DownloadRequest: self.performDownloadRequest(r)
case let r as DataStreamRequest: self.performDataStreamRequest(r)
default: fatalError("Attempted to perform unsupported Request subclass: \(type(of: request))")
}
}
}
}
perform(_ request: Request) 方法接收一个 Request 对象,并根据具体的请求类型(DataRequest、UploadRequest、DownloadRequest 等)选择合适的执行逻辑来处理该请求。
我们看下switch这条语句,可以看到Alamofire根据 request 的类型进行分派,选择合适的处理方法。其中request 的类型有:
- UploadRequest:执行上传请求逻辑,调用 performUploadRequest(_:)。
- DataRequest:执行普通的数据请求,调用 performDataRequest(_:),这是最常用的请求类型。
- DownloadRequest:执行下载请求逻辑,调用 performDownloadRequest(_:)。
- DataStreamRequest:处理数据流请求,调用 performDataStreamRequest(_:)。
- default:如果遇到不支持的请求类型,直接抛出致命错误。
其中performUploadRequest、performDataRequest这些方法都是由performSetupOperations()这个方法实现的。
我们来看下performSetupOperations()的底层实现:
func performSetupOperations(for request: Request,
convertible: URLRequestConvertible,
shouldCreateTask: @escaping () -> Bool = { true }) {
dispatchPrecondition(condition: .onQueue(requestQueue))//确保请求操作被安全地调度到指定队列上。
let initialRequest: URLRequest
do {
initialRequest = try convertible.asURLRequest()//将 URLRequestConvertible 转换为实际的 URLRequest。
try initialRequest.validate()//验证生成的 URLRequest,检查 URL 是否合法、方法是否合理等。
} catch {
rootQueue.async { request.didFailToCreateURLRequest(with: error.asAFError(or: .createURLRequestFailed(error: error))) }
return
}
rootQueue.async { request.didCreateInitialURLRequest(initialRequest) }//该回调通知请求对象,已经成功创建了初始的 URLRequest,为后续执行做好准备。
guard !request.isCancelled else { return }
guard let adapter = adapter(for: request) else {
guard shouldCreateTask() else { return }
rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) }
return
}
let adapterState = RequestAdapterState(requestID: request.id, session: self)
adapter.adapt(initialRequest, using: adapterState) { result in
do {
let adaptedRequest = try result.get()
try adaptedRequest.validate()
self.rootQueue.async { request.didAdaptInitialRequest(initialRequest, to: adaptedRequest) }
guard shouldCreateTask() else { return }
self.rootQueue.async { self.didCreateURLRequest(adaptedRequest, for: request) }
} catch {
self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: .requestAdaptationFailed(error: error)) }
}
}
}
这个方法的主要作用是设置网络请求的初始配置,包括将传入的 URLRequestConvertible 转换为 URLRequest,进行请求验证,适配器操作(如果有),并最终为网络请求创建 URLSessionTask。该方法对请求生命周期中的准备阶段做了全面的处理。
我们重点看下这一段代码:
guard let adapter = adapter(for: request) else {
guard shouldCreateTask() else { return }
rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) }
return
}
adapter(for: request):检查是否为请求配置了 RequestAdapter(适配器),适配器允许在发出请求前对 URLRequest 进行修改(如添加认证信息、修改 URL 等)。
如果没有的话,则执行request的请求,也就是self.didCreateURLRequest(initialRequest, for: request)
self.didCreateURLRequest(initialRequest, for: request)的代码实现:
func didCreateURLRequest(_ urlRequest: URLRequest, for request: Request) {
dispatchPrecondition(condition: .onQueue(rootQueue))
request.didCreateURLRequest(urlRequest)
guard !request.isCancelled else { return }
let task = request.task(for: urlRequest, using: session)
requestTaskMap[request] = task
request.didCreateTask(task)
updateStatesForTask(task, request: request)
}
func updateStatesForTask(_ task: URLSessionTask, request: Request) {
dispatchPrecondition(condition: .onQueue(rootQueue))
request.withState { state in
switch state {
case .initialized, .finished:
// Do nothing.
break
case .resumed:
task.resume()
rootQueue.async { request.didResumeTask(task) }
case .suspended:
task.suspend()
rootQueue.async { request.didSuspendTask(task) }
case .cancelled:
// Resume to ensure metrics are gathered.
task.resume()
task.cancel()
rootQueue.async { request.didCancelTask(task) }
}
}
}
看到这里是不是就很熟悉了,没错这里就是NSURLSession中的URLSessionTask(NSURLSession的各种任务)。上述方法创建RequestTask
,最后调用task.resume()。其实本质上和
NSURLSession一样,只不过是封装了而已。
小结:
1. Alamofire 概述:
• Alamofire 是对 NSURLSession 的封装,简化了网络请求的处理流程,提供更便捷的 API。
• 主要流程包括创建 URLRequest、发起请求、处理响应等,内部依然使用 URLSession 和 URLSessionTask。
2. 核心步骤:
• AF.request() 方法是入口,参数包括 URL、请求方法(如 GET/POST)、请求参数、编码方式、请求头等。
• 该方法内部会创建一个 RequestConvertible 对象,将所有请求参数封装成 URLRequest。
3. 执行请求:
• DataRequest 是 Alamofire 处理网络请求的核心对象。它继承自 Request,管理请求的生命周期。
• perform(request) 根据不同请求类型(DataRequest、UploadRequest 等)调用相应的处理方法。
4. 请求最终执行:
• 通过 performSetupOperations 设置请求参数并验证生成的 URLRequest,接着通过 URLSessionTask 发起请求,最后调用 .resume() 来执行任务。
总结:Alamofire 的封装本质上是对 NSURLSession 工作流的简化和扩展,主要增强了参数编码、请求头管理、响应处理等功能,便于开发者更方便地进行网络操作。
参考:
GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift
Alamofire 5源码解析一: 执行HTTP Request_alamofire5.0 interceptor-CSDN博客
iOS开发之Alamofire源码解析前奏--NSURLSession全家桶-腾讯云开发者社区-腾讯云
NSURLSession | Apple Developer Documentation