Alamofire 5.x 源码阅读分析(Source Code reading and analysis)
本文最后更新于 2021年4月4日 晚上
iOS端常用的三方框架, 很少有时间去阅读它们的源码, 目前正好有机会可以看看. 这次先来看 Alamofire, 一个基于苹果原生 URLSession 的网络通信框架.
文档
先看文档, 然后再分析源码, 循序渐进.
简介
下面从整体上介绍 Alamofire 的相关内容.
主要功能
- 链式的请求/响应方法
- URL/JSON/PList 参数编码
- 文件/流/MultiPart表单数据上传
- 文件下载/恢复
- 使用 URLCredential 进行认证
- HTTP 响应验证(Validation)
- 提供上传/下载进度回调
- cURL 命令输出
- 动态适应(Adapt)/重试(Retry)请求
- TLS 证书/公钥 Pinning(主动选择信任 CA 的能力)
- 网络可达性检测
- 大量的单元/集成测试覆盖
- 完善的文档支持
组件库
Alamofire 只是核心库, 在其上还开发了若干组件:
AlamofireImage
: 一个图片库, 提供图片响应序列化器, 以及 UIImage/UIImageView 扩展, 自定义图片过滤器, 自动清理图片缓存, 基于优先级的图片下载系统.AlamofireNetworkActivityIndicator
: 通过 Alamofire 来控制 iOS 的网络活动指示器. 并支持独立的 URLSession.
需求
- iOS 10 以上/ macOS 10.12 以上/ tvOS 10 以上/ watchOS 3 以上
- Xcode 10.2 以上
- Swift 5 以上
支持
Alamofire 是基于苹果的 URLSession 的, 即URL Loading System.
在 swift.org 论坛中有专门的版块提供 Alamofire 支持.
在 Stack Overflow 上也有.
安装
支持如下安装方式:
- CocoaPods:
pod 'Alamofire', '~> 5.0.0-rc.3'
- Carthage:
github "Alamofire/Alamofire" "5.0.0-rc.3"
- SPM:
1
2
3dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.0.0-rc.3")
] - 手动集成: 基于 git submodule 方式.
基本使用
基本使用包括 HTTP 请求/响应, 数据的上传和下载, 以及一些实用工具的介绍.
Alamofire 基本实现概述
Alamofire 建立在苹果提供的 URL Loading System 上, 核心是 URLSession
和 URLSessionTask
类族. 并且在 Alamofire 中网络通信都是异步进行的, 不会阻塞主线程.
新版的 Alamofire 仍然提供默认的 Session
(之前的 SessionManager
), 只是把它挪到了 AF 名空间下, AF
是一个 enum.
基本使用的内容大多基于 AF
名空间下的静态方法实现.
进行请求
进行简单的请求:
1 |
|
其中 request
方法声明如下:
1 |
|
方法返回一个 DataRequest
对象. 新版 API 中都推荐使用这个泛型的 request
重载, 而不推荐使用老的那个带 ParameterEncoding
参数的非泛型重载方法.
另外还有一个更简洁的 request
非泛型重载方法:
1 |
|
这个方法中提供了一个全新的 URLRequestConvertible
协议对象参数, 可以把所有请求参数都封装到里面, 极大地方便了开发者, 我们现在就可以利用这个协议来实现自定义请求参数类. 具体的使用方式详见高级功能用法一节.
HTTP 方法
根据 RFC 7231 第 4.3 节定义了如下的 HTTP 方法(使用 struct 而非 enum 定义的):
1 |
|
通过 request
API 传入它们:
1 |
|
由于不同的请求方法可能会需要不同的参数编码方式, 比如 post 可能把参数编码到请求体, 且请求体中可以包含 JSON 或表单数据等, 另外 苹果的 URL Loading System 中 URLSession
不允许在使用 Get
方法时携带请求体(Request Body)参数. 此时如果在 Get 请求中指定请求体参数, 将会报错.
在 Alamofire 的内部, 通过如下代码获取 URLRequest
中的请求方法字符串对应的 HTTPMethod
:
1 |
|
另外, 如果需要使用自定义的 HTTP 方法(很少见, 但也可能), 此时可以像下面这样对 HTTPMethod
进行扩展:
1 |
|
请求参数和参数序列化器
Alamofire 支持对 Encodable
类型参数直接进行序列化. 目前 Alamofire 提供了两个 ParameterEncoder
实现类, 包括 JSONParameterEncoder
和 URLEncodedFormParameterEncoder
. 使用上非常方便.
其中 JSONParameterEncoder
可以将参数对象序列化为 JSON 字符串, 若请求头没有指定 ContentType
, 则默认设置 application/json
.
Post 请求传递 JSON 参数:
1 |
|
可以在请求时对 JSON 序列化器进行配置:
1 |
|
可以设置自定义的 JSON 序列化器:
1 |
|
URLEncodedFormParameterEncoder
则支持将对象序列化为 query 字符串或请求体表单数据. 展开说就是在 Get 请求时, 默认将参数序列化为 query string, 在 Post 请求时, 将参数序列化为请求体中携带的参数. 通过 destination
可设置参数的具体位置:
.methodDependent
: 在 get/head/delete 请求时,设置为 query string, 在其他情况下, 设置为请求体参数..queryString
: 序列化为字符串并设置为 query string 参数.httpBody
: 序列化为字符串并设置为请求体参数
如果没有指定 Content-Type
, 则 Alamofire 自动设置其为 application/x-www-form-urlencoded; charset=utf-8
.
URLEncodedFormParameterEncoder
内部使用 URLEncodedFormEncoder
进行序列化操作.
Get 请求:
1 |
|
Post 请求时:
1 |
|
可以通过 alphabetizeKeyValuePairs
设置是否将参数按照字母顺序排序, 默认情况下会自动按字母顺序排序(由于swift的字典是哈希字典, 如果不排序, 可能对缓存等产生影响, 因为每次的参数序列化字符串都可能不同):
1 |
|
还有 Array/Bool/Data/Date等的序列化选项, 详见文档.
特别是常用的 Date
序列化, 可以指定 .iso8601
/.millisecondsSince1970
/.secondsSince1970
或者 .custom((Date) throws -> String)
等选项.
对于参数中的 Key
风格, 在序列化时也可以进行设置(默认是原样进行序列化):
1 |
|
指定空格的序列化选项(默认是序列化为 %20
):
1 |
|
HTTP Header 设置
Alamofire 提供了 HTTPHeaders
类型表示请求/响应头, 它能够保持头部数据的顺序, 并且忽略大小写.
在请求时使用 HTTPHeaders
:
1 |
|
最佳实践: 针对一直不变的请求头数据, 可以直接在初始化
Session
时设置到URLSessionConfiguration
中.
Alamofire 中会设置如下默认请求头数据:
Accept-Encoding
: 默认值br;q=1.0, gzip;q=0.8, deflate;q=0.6
.Accept-Language
: 默认值为系统中优先级最高的 6 种语言, 格式为:en;q=1.0
User-Agent
: 根据当前 APP 的信息生成, 格式为:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 13.0.0) Alamofire/5.0.0
创建新的 Session
实例后, 如果要自定义统一的请求头信息, 有两条路:
新建
URLSessionConfiguration
, 然后设置.获取
URLSessionConfiguration.af.default
, 然后修改上面的内容并传递到Session
的构造函数(构造函数中默认使用的就是它).
响应验证(Validation)
默认情况下, Alamofire 认为只要接收到响应就表示整个请求过程是成功的. 但有时希望对响应进行验证, 这个时候就可以使用 validate()
相关方法.
自动验证: 链入空的
validate()
方法, 它认为响应状态码在200..<300
范围内, 且如果设置了Accept
头时响应的Content-Type
和其相符的表示响应成功.1
2
3AF.request("https://httpbin.org/get").validate().responseJSON { response in
debugPrint(response)
}手动验证: 手动指定相关验证条件
1
2
3
4
5
6
7
8
9
10
11AF.request("https://httpbin.org/get")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData { response in
switch response.result {
case .success:
print("Validation Successful")
case let .failure(error):
print(error)
}
}
响应处理
Alamofire 中的请求对象 或 DownloadRequest
对应的响应类型都是
Alamofire 中请求对象和响应对象的对应关系:
DataRequest
对应DataResponse<Success, Failure: Error>
DownloadRequest
对应DownloadResponse<Success, Failure: Error>
其中 Error 都是 AFError.
针对公共 API, Alamofire 提供的则是 AFDataResponse<Success>
和 AFDownloadResponse<Success>
. 实际定义如下:
1 |
|
对 DataResponse
处理, 都是在其上调用 response
组方法, 比如 responseJSON
:
1 |
|
对于 responseJSON
内部使用 JSONResponseSerializer
对响应数据进行序列化.
Alamofire 有如下 6 种 response
方法:
1 |
|
response
: 不会对响应数据进行任何处理, 它直接将响应的 Data 返回. 在实际开发中推荐使用其他几个方法.responseData
: 使用DataResponseSerializer
处理并返回服务端响应的 Data 形式.responseString
: 使用StringResponseSerializer
将服务端响应 Data 转换为String
. 如果没有指定 Encoding, Alamofire 会自动使用响应中的编码格式反序列化 Data 为 String, 如果没有, 则默认为.isoLatin1
.responseJSON
: 使用JSONResponseSerializer
将 Data 序列化为Any
, 它内部使用的是Foundation
中的JSONSerialization
API 进行 JSON 反序列化. 这个方法的返回值实际上可能是数组([[String: Any]]
)或字典([String: Any]
). 且内部序列化失败的情况下会抛出jsonSerializationFailed
错误.responseDecodable<T: Decodable>
: 使用DecodableResponseSerializer
将响应数据序列化为传入的类型, 可以将响应数据直接序列化为指定类型的模型. 在内部使用的是JSONDecoder
来进行 JSON 反序列化.
默认情况下, response
处理块的代理是在 .main
线程执行的, 可以在调用时传入其他线程:
1 |
|
对数据的反序列化操作始终都是在后台线程进行的.
缓存响应
响应的缓存是通过 URLCache
在系统框架级别上进行的. 默认情况下, Alamofire 使用 URLCache.shared
对象进行缓存. 可以在 Session
创建时对缓存行为进行配置.
cURL 命令输出
借由这个功能可以非常有效地进行 Debug:
1 |
|
1 |
|
高级功能用法
高级使用包括基于 URLSession 的封装介绍, Routing, 响应序列化, 连接检测等内容的介绍.
在进入高级功能之前, 先要弄懂 iOS 平台上的 URL Loading System.
高级使用里面首先介绍了 Session 和 Session 配置的东西, 然后后续的在日常开发中看了会比较有帮助.
Routing
在 Alamofire 中, 进行请求的入口就是那 2 个 request
方法, 而接收参数的时候传入的要么是 URLConvertible
和若干请求参数, 要么是 URLRequestConvertible
包装的所有参数, 所以在网络栈实现时, 也可以有两条路可选:
URLConvertible
路线:1
2
3
4
5
6
7
8
9
10
11
12
13extension User: URLConvertible {
static let baseURLString = "https://example.com"
func asURL() throws -> URL {
let urlString = User.baseURLString + "/users/\(username)/"
return try urlString.asURL()
}
}
// ...
let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/matttURLRequestConvertible
路线: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
28enum Router: URLRequestConvertible {
case search(query: String, page: Int)
static let baseURLString = "https://example.com"
static let perPage = 50
// MARK: URLRequestConvertible
func asURLRequest() throws -> URLRequest {
let result: (path: String, parameters: Parameters) = {
switch self {
case let .search(query, page) where page > 0:
return ("/search", ["q": query, "offset": Router.perPage * page])
case let .search(query, _):
return ("/search", ["q": query])
}
}()
let url = try Router.baseURLString.asURL()
let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))
return try URLEncoding.default.encode(urlRequest, with: result.parameters)
}
}
// ...
Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50
针对后面一种, 更完整的例子如下:
1 |
|
请求加工和重试
RequestAdapter 解决了一个老大难问题: 如何实现当更新 accessToken 后的认证头更新问题.
可以使用 RequestAdapter
来处理:
1 |
|
另外有 RequestRetrier
可以实现请求错误时重试, 详见文档.
自定义的响应转换
可以在 response 类型上使用 map 和 flatmap, 详见文档, 特别是这样可以帮助实现自定义的响应处理块.
还有很多有用的内容详见文档.
网络可达性观察
1 |
|
源码阅读
基于上述知识准备, 我们就可以开始源码的阅读了. Alamofire 的源码不多, 阅读和实验相对比较容易.
Alamofire 文件夹组织如下所示:
Alamofire.swift
: 提供AF
名空间的定义, 在其中实现 Data/Download/Upload 组的请求方法. 均使用Session.default
进行请求.Core
文件夹: 包含 Session/Request/Response, 以及请求参数序列化等功能实现.Extensions
文件夹: 包含任务调度相关的扩展, URLRequest 的相关扩展, SwiftResult
扩展, 以及 URLSessionConfiguration 的default
实现.Features
文件夹: 包含响应序列化实现, 便捷响应序列化处理等.
从文档中可以知道, Alamofire
的公共 API 入口主要就是 AF
名空间下的便捷方法, 或者是使用自己创建的 Session 上的 request, response, validate 方法. 以及请求的 adaptor 和 retrier. 下面从这几个角度去看源码, 骨架完善后再补充细枝末节.
AF 名空间
在 Alamofire 中, 请求的类型被限定为三大类: Data/Download/Upload
. 在 AF
名空间下, 使用 Session.default
进行上述三类请求.
Session.default
的配置如下:
1 |
|
创建 default
时使用默认配置, 和文档描述一致, 不再赘述.
调用两个主要的 request
方法都会返回 DataRequest
对象.
这样就引出了疑问1: DataRequest
对象的创建过程?
请求的时候, 最终会在 Session
的 request
中创建 DataRequest
. 创建时会传入root队列和序列化队列, 两个队列有关联关系, 所以需要明白 iOS 中的 DispatchQueue 为什么会有 target 参数?
1 |
|