原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容
续文见上篇:IOS源码解析:Alamofire 5 核心
目录
- 一、请求源码
- 1、证书校验 ServerTrustEvaluation
- 2、多表单 MultipartFormData
- 3、网络请求 Request
- 二、响应源码
- 1、请求响应 Response
- 2、序列化响应 ResponseSerialization
- 三、底层源码
- 1、错误处理 AFError
- 2、会话代理 SessionDelegate
- 四、其他源码
- 1、通知处理 Notifications
- 2、网络监控 NetworkReachabilityManager
- Demo
- 参考文献
一、请求源码
1、ServerTrustEvaluation
a、HTTPS请求的过程
步骤一:HTTPS请求以https开头。我们首先向服务器发送一条请求。
步骤二:服务器需要一个证书
这个证书可以从某些机构获得,也可以自己通过工具生成,通过某些合法机构生成的证书客户端不需要进行验证,这样的请求不会触发Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)
代理方法。自己生成的证书则需要客户端进行验证。
由于使用非对称加密,证书中包含公钥和私钥。公钥是公开的,任何人都可以使用该公钥加密数据,只有知道了私钥才能解密数据。私钥是要求高度保密的,只有知道了私钥才能解密用公钥加密的数据。
步骤三:服务器会把公钥发送给客户端
步骤四:客户端此刻就拿到了公钥
这里不是直接就拿公钥加密数据发送了,因为这仅仅能满足客户端给服务器发加密数据,那么服务器怎么给客户端发送加密数据呢?因此需要在客户端和服务器间建立一条通道,通道的密码只有客户端和服务器知道。只能让客户端自己生成一个密码,这个密码就是一个随机数,这个随机数绝对是安全的,因为目前只有客户端自己知道。
步骤五:客户端把这个随机数通过公钥加密后发送给服务器,就算被别人截获了加密后的数据,在没有私钥的情况下,是根本无法解密的
步骤六:服务器用私钥把数据解密后,就获得了这个随机数
步骤七:到这里客户端和服务器的安全连接就已经建立了。最主要的目的是交换随机数,然后服务器就用这个随机数把数据加密后发给客户端,使用的是对称加密技术
步骤八:客户端获得了服务器的加密数据,使用随机数解密,到此,客户端和服务器就能通过随机数发送数据了
b、ServerTrustManager
❶ 策略字典
ServerTrustManager
是对ServerTrustEvaluating
的管理。我们可以暂时把ServerTrustEvaluating
当做是一个安全策略,就是指对一个服务器采取的策略。然而在真实的开发中,一个APP可能会用到很多不同的主机地址(host
),因此就产生了这样的需求,为不同的host
绑定一个特定的安全策略。
open class ServerTrustManager
{
// 映射到特定主机的策略字典
public let evaluators: [String: ServerTrustEvaluating]
}
❷ 根据host读取策略
open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating?
{
let evaluator = evaluators[host]
return evaluator
}
❸ 使用方式
let manager = ServerTrustManager(evaluators: ["httpbin.org": PinnedCertificatesTrustEvaluator()])
let managerSession = Session(serverTrustManager: manager)
managerSession.request("https://httpbin.org/get").responseJSON
{ response in
debugPrint(response)
}
输出结果为:
打印HTTPMethod的值:HTTPMethod(rawValue: "GET")
打印HTTPMethod的原始值:GET
[Request]: GET https://httpbin.org/get
[Headers]: None
[Body]: None
[Response]: None
[Network Duration]: None
[Serialization Duration]: 5.08379889652133e-05s
[Result]: failure(Alamofire.AFError.sessionDeinitialized)
c、ServerTrustEvaluating
描述用于评估服务器信任的API的协议
public protocol ServerTrustEvaluating
{
// 为给定的host计算给定的SecTrust值
func evaluate(_ trust: SecTrust, forHost host: String) throws
}
实现该协议的类
// 默认的策略,只有合法证书才能通过验证
public final class DefaultTrustEvaluator: ServerTrustEvaluating
// 对注销证书做的一种额外设置
public final class RevocationTrustEvaluator: ServerTrustEvaluating
// 如果不验证证书链的话,只要对比指定的证书有没有和服务器信任的证书匹配项,只要有一个能匹配上,就验证通过
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
// 和上边的那个差不多,只是验证对象改为PublicKeys
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating
// 组合验证
public final class CompositeTrustEvaluator: ServerTrustEvaluating
// 无条件信任,验证一直都是通过的
public final class DisabledTrustEvaluator: ServerTrustEvaluating
d、获取证书
在开发中,如果和服务器的安全连接需要对服务器进行验证,最好的办法就是在本地保存一些证书,拿到服务器传过来的证书,然后进行对比,如果有匹配的,就表示可以信任该服务器。从上边的函数中可以看出,Alamofire
会在Bundle
(默认为main
)中查找带有[".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]
后缀的证书。
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
{
private let certificates: [SecCertificate]
public init(certificates: [SecCertificate] = Bundle.main.af.certificates)
{
self.certificates = certificates
}
}
下边函数中的paths
保存的就是这些证书的路径。map
把这些后缀转换成路径,我们以.cer
为例,通过map
后,原来的.cer
就变成了一个数组,也就是说通过map
后,原来的数组变成了二维数组了,然后再通过joined()
函数,把二维数组转换成一维数组。然后要做的就是根据这些路径获取证书数据了。
extension Bundle: AlamofireExtended {}
extension AlamofireExtension where ExtendedType: Bundle
{
public var certificates: [SecCertificate]
{
paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap
{ path in
guard
let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil }
return certificate
}
}
}
e、获取公钥
PublicKeysTrustEvaluator
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating
{
private let keys: [SecKey]
private let performDefaultValidation: Bool
private let validateHost: Bool
public init(keys: [SecKey] = Bundle.main.af.publicKeys,
performDefaultValidation: Bool = true,
validateHost: Bool = true)
{
self.keys = keys
self.performDefaultValidation = performDefaultValidation
self.validateHost = validateHost
}
}
返回包中有效证书的所有公钥,就是在本地证书中取出公钥
public var publicKeys: [SecKey]
{
certificates.af.publicKeys
}
f、核心的方法evaluate:以Pinned证书验证为例
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
{
public func evaluate(_ trust: SecTrust, forHost host: String) throws
{
// 未发现证书
guard !certificates.isEmpty else {
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
// 接受自签名证书
if acceptSelfSignedCertificates
{
try trust.af.setAnchorCertificates(certificates)
}
// 执行默认验证
if performDefaultValidation
{
try trust.af.performDefaultValidation(forHost: host)
}
// 验证主机
if validateHost
{
try trust.af.performValidation(forHost: host)
}
// 服务器证书数据
let serverCertificatesData = Set(trust.af.certificateData)
// pinned证书数据
let pinnedCertificatesData = Set(certificates.af.data)
// pinned证书数据 在 服务器证书数据中
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
// 倘若不在则验证失败
if !pinnedCertificatesInServerData
{
throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host,
trust: trust,
pinnedCertificates: certificates,
serverCertificates: trust.af.certificates))
}
}
}
2、多表单 MultipartFormData
a、多表单格式
我相信应该有不少的开发者不明白多表单是怎么一回事,嗯,我也一样,所以我们一起来学习下。试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf
等等)需要上传给服务器,你打算怎么办?如果你一个一个的上传,那我无话可说,但是如果你想一次性上传,那么就要考虑服务端如何识别这些不同类型的数据呢?服务端对不同类型数据的识别解决方案就是多表单。客户端与服务端共同制定一套规范,彼此使用该规则交换数据就完全ok了。
POST / HTTP/1.1
[[ Less interesting headers ... ]]
// 通过Content-Type来说明当前数据的类型为multipart/form-data,这样服务器就知道客户端将要发送的数据是多表单了
// 多表单说白了就是把各种数据拼接起来,要想区分不同数据,必须添加一个界限标识符boundary
Content-Type: multipart/form-data; boundary=735323031399963166993862150
// 告诉服务端数据的总长度,在后边的代码中会有一个属性来提供这个数据
// 我们最终上传的数据都是二进制流,因此获取到Data就能计算大小
Content-Length: 834
// 如果在boundary前边添加了--就表示是多表单的开始边界
--735323031399963166993862150
// 对内容的进一步说明
Content-Disposition: form-data; name="text1"
text default
735323031399963166993862150
Content-Disposition: form-data; name="text2"
aωb
735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
// 表示对表单内该数据的类型的说明
Content-Type: text/plain
Content of a.txt.
735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html
<!DOCTYPE html><title>Content of a.html.</title>
735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream
aωb
// 在boundary后面添加--就表示是结束边界
735323031399963166993862150--
b、Boundary
// 边界生产者
enum BoundaryGenerator
{
...
}
// 换行回车:对"\r\n"的一个封装
enum EncodingCharacters
{
static let crlf = "\r\n"
}
❶ 设计一个枚举来封装边界类型:开始边界、内部边界、结束边界
enum BoundaryType
{
case initial, encapsulated, final
}
❷ 生成边界字符串:通常该字符串采用随机数生成的方式
static func randomBoundary() -> String
{
let first = UInt32.random(in: UInt32.min...UInt32.max)
let second = UInt32.random(in: UInt32.min...UInt32.max)
return String(format: "alamofire.boundary.%08x%08x", first, second)
}
❸ 转换函数:因为最终上传的数据是Data类型,所以需要将边界转换成Data类型
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data
{
let boundaryText: String
switch boundaryType
{
case .initial:
boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
case .encapsulated:
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
case .final:
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
}
return Data(boundaryText.utf8)
}
c、BodyPart
对每一个body
部分的描述,这个类只能在MultipartFormData
内部访问,外部无法访问。
open class MultipartFormData
{
class BodyPart
{
let headers: HTTPHeaders //对数据的描述
let bodyStream: InputStream //数据来源,Alamofire中使用InputStream统一进行处理
let bodyContentLength: UInt64 //该数据的大小
var hasInitialBoundary = false //是否包含初始边界
var hasFinalBoundary = false //是否包含结束边界
init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64)
{
self.headers = headers
self.bodyStream = bodyStream
self.bodyContentLength = bodyContentLength
}
}
}
d、MultipartFormData
- 提供一些在请求时需要的参数
- 提供各种数据拼接的入口
- 如果数据过大,为了性能,提供把数据写入文件的功能
属性
// 通过Content-Type来说明当前数据的类型为multipart/form-data,这样服务器就知道客户端将要发送的数据是多表单了
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
// 获取数据的大小,该属性是一个计算属性
public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
// 表示边界,在初始化中会使用BoundaryGenerator来生成一个边界字符串
public let boundary: String
// 是一个集合,包含了每一个数据的封装对象BodyPart
private var bodyParts: [BodyPart]
// 设置stream传输的buffer大小
private let streamBufferSize: Int
初始化方法
public init(fileManager: FileManager = .default, boundary: String? = nil)
{
self.fileManager = fileManager
self.boundary = boundary ?? BoundaryGenerator.randomBoundary()
bodyParts = []
streamBufferSize = 1024
}
e、Body Parts:将多种不同类型的文件拼接到bodyParts数组中
❶ 3种输入源
Data //直接提供Data类型的数据,比如把一张图片编码成Data,然后拼接进来
fileURL //通过一个文件的本地URL来获取数据,然后拼接进来
Stream //直接通过stream导入数据
❷ 提供参数来描述输入源传入的数据
name //与数据相关的名字
mimeType //表示数据的类型
fileName //表示数据的文件名称
length //表示数据大小
stream //表示输入流
headers //数据的headers
❸ 设计函数来实现把每一条数据封装成BodyPart对象,然后拼接到bodyParts数组中
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders)
{
// 给出headers,stream和length就能生成BodyPart对象
let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
// 然后把它拼接到数组中就行了
bodyParts.append(bodyPart)
}
如果每次都是用上边的函数拼接数据,我们估计会疯掉,因为必须要对它的3个参数非常了解才行。因此,这就说明上边的函数是最底层的函数方案。之所以称为最底层,因为他可定义的灵活性很高,使用起来也很麻烦。我们接下来要考虑的就是如何减少开发过程中的使用障碍。那么现在要设计一个包含最多参数的函数,这个函数会成为其他函数的内部实现基础。我们把headers这个参数去掉,这个参数可以根据name
、mimeType
、fileName
计算出来,因此有了下边的函数。
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String)
{
// 根据name,mimeType,fileName计算出headers
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
append(stream, withLength: bodyContentLength, headers: headers)
}
如果我传入的数据是个Data
类型呢?能对Data
进行描述的有3个参数:name
,mimeType
,fileName
。根据data
生成InputStream
和length
是关键。
public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil)
{
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
let stream = InputStream(data: data)
let length = UInt64(data.count)
append(stream, withLength: length, headers: headers)
}
❹ 当需要把文件写入fileURL中或者从fileURL中读取数据时对错误的处理
判断fileURL
是不是一个file
的URL
guard fileURL.isFileURL else
{
setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
return
}
判断该fileURL
是不是可达的
do
{
let isReachable = try fileURL.checkPromisedItemIsReachable()
guard isReachable else
{
setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
return
}
}
catch
{
setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
return
}
判断fileURL
是不是一个文件夹,而不是具体的数据
var isDirectory: ObjCBool = false
let path = fileURL.path
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else
{
setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
return
}
判断fileURL
指定的文件能不能被读取
let bodyContentLength: UInt64
do
{
guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else
{
setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
return
}
bodyContentLength = fileSize.uint64Value
}
catch
{
setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
return
}
判断能不能通过fileURL
创建InputStream
guard let stream = InputStream(url: fileURL) else
{
setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
return
}
f、将bodyParts数组中的模型拼接成一个完整的Data
❶ 编码整个请求体(在内存中读取并编码)
public func encode() throws -> Data
{
// 如果发生异常, 则直接抛出
if let bodyPartError = bodyPartError
{
throw bodyPartError
}
// 最终的请求体
var encoded = Data()
// 给数组中第一个数据设置开始边界,最后一个数据设置结束边界
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
// 拼接 body
for bodyPart in bodyParts
{
// 编码内容块
let encodedData = try encode(bodyPart)
// 把bodyPart对象转换成Data类型,然后拼接到encoded中
encoded.append(encodedData)
}
return encoded
}
❷ 编码内容块为data (在内存中读取并编码)
private func encode(_ bodyPart: BodyPart) throws -> Data
{
// 最终数据
var encoded = Data()
// 如果是第一个数据, 要使用起始字段, 否则用正常分割字段
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
encoded.append(initialData)
// 添加头字段
let headerData = encodeHeaders(for: bodyPart)
encoded.append(headerData)
// 添加内容字段
let bodyStreamData = try encodeBodyStream(for: bodyPart)
encoded.append(bodyStreamData)
/// 如果是最后一个数据, 要加上尾字段
if bodyPart.hasFinalBoundary
{
encoded.append(finalBoundaryData())
}
return encoded
}
g、写入编码好的数据到指定文件(处理大尺寸数据)
在Alamofire
中,如果编码后的数据超过了某个值,就会把该数据写入到fileURL
中,在发送请求的时候,在fileURL
中读取数据上传。Alamofire
并没有使用上边的encode
函数来生成一个Data
,然后再写入fileURL
。这是因为大文件往往我们是通过append(fileURL)
方式拼接进来的,并没有把数据加载到内存。
public func writeEncodedData(to fileURL: URL) throws
{
if let bodyPartError = bodyPartError
{
throw bodyPartError
}
// 判断写入文件是否已存在
if fileManager.fileExists(atPath: fileURL.path)
{
throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
}
// 判断是否是文件 url
else if !fileURL.isFileURL
{
throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
}
// 生成流
guard let outputStream = OutputStream(url: fileURL, append: false) else
{
throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
}
// 开启流
outputStream.open()
// defer可以定义代码块结束后执行的语句
defer { outputStream.close() }
// 设置头尾
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
for bodyPart in bodyParts
{
// 写入
try write(bodyPart, to: outputStream)
}
}
3、网络请求 Request
a、request方法
❶ 外界调用的request方法
// 外界调用网络请求方法
AF.request("https://httpbin.org/get")
// 提供给外界调用的网络请求方法
open func request(...) -> DataRequest
{
let convertible = RequestConvertible(...)
// 内部实现
return request(convertible, interceptor: interceptor)
}
❷ 内部实现的request方法
open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest
{
// 创建DataRequest
let request = DataRequest(convertible: convertible,
underlyingQueue: rootQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: self)
// 执行提供的Request
perform(request)
return request
}
b、DataRequest 数据请求类
UploadRequest
DataRequest
DownloadRequest
DataStreamRequest
❶ 属性
public class DataRequest: Request
{
// URLRequestConvertible值,用于为此实例创建URLRequest
public let convertible: URLRequestConvertible
// 目前为止从服务器读取的数据
public var data: Data? { mutableData }
// 可变数据类型,将每次从服务器读取的数据添加到其后
@Protected
private var mutableData: Data? = nil
}
❷ 重置网络请求的方法
override func reset()
{
super.reset()
mutableData = nil
}
❸ 当此实例接收到Data时调用以下方法,在更新下载进度的时候也会被调用
func didReceive(data: Data)
{
if self.data == nil
{
// 第一次拿到数据
mutableData = data
}
else
{
// 拼接之后请求到的数据
$mutableData.write { $0?.append(data) }
}
// 更新下载进度
updateDownloadProgress()
}
❹ 根据request获取dataTask
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask
{
let copiedRequest = request
return session.dataTask(with: copiedRequest)
}
❺ 更新下载进度
func updateDownloadProgress()
{
let totalBytesReceived = Int64(data?.count ?? 0)
let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
downloadProgress.totalUnitCount = totalBytesExpected
downloadProgress.completedUnitCount = totalBytesReceived
downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) }
}
❻ 使用指定的闭包验证请求
public func validate(_ validation: @escaping Validation) -> Self
{
let validator: () -> Void =
{ [unowned self] in
guard self.error == nil, let response = self.response else { return }
let result = validation(self.request, response, self.data)
if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) }
self.eventMonitor?.request(self,
didValidateRequest: self.request,
response: response,
data: self.data,
withResult: result)
}
$validators.write { $0.append(validator) }
return self
}
c、开始执行提供的网络请求
func perform(_ request: Request)
{
rootQueue.async
{
// 如果网络请求取消掉了则直接返回
guard !request.isCancelled else { return }
// 将该请求添加到当前活动的网络请求集合
self.activeRequests.insert(request)
// 在指定队列异步请求网络
self.requestQueue.async
{
// 判断request类型来执行具体请求任务
switch request
{
// 由于子类型关系,UploadRequest必须位于DataRequest之前
case let r as UploadRequest: self.performUploadRequest(r)
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))")
}
}
}
}
二、响应源码
1、请求响应 Response
public func responseString(completionHandler: @escaping (AFDataResponse<String>) -> Void)
public func responseDecodable<T: Decodable>(completionHandler: @escaping (AFDownloadResponse<T>) -> Void)
public typealias AFDataResponse<Success> = DataResponse<Success, AFError>
public typealias AFDownloadResponse<Success> = DownloadResponse<Success, AFError>
共有4中不同的Request
类型,DataStreamRequest
我们先不提,对于UploadRequest
来说,服务器响应的数据比较简单,就响应一个结果就行,因此不需要对它的Response
专门进行封装。因此,Alamofire
设计了2种与Request
类型相对应的Response
类型,他们分别是:DataResponse
和DownloadResponse
。
a、链式访问原理:response函数的返回值都是Request
在下边的代码中,虽然两个闭包里的response
名字都一样,但并不是同一类型。
AF.request("https://httpbin.org/get")
.responseString
{ response in
print("Response String: \(String(describing: response.value))")
}
.responseJSON
{ response in
print("Response JSON: \(String(describing: response.value))")
}
输出结果为:
Response String: Optional("{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"br;q=1.0, gzip;q=0.9, deflate;q=0.8\", \n \"Accept-Language\": \"en;q=1.0\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"AlamofireSourceCodeAnalysis/1.0 (com.xiejiapei.framework.AlamofireSourceCodeAnalysis; build:1; iOS 14.4.0) Alamofire/5.4.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-60129046-79f8d1b13ef72f727faa8e36\"\n }, \n \"origin\": \"222.76.251.163\", \n \"url\": \"https://httpbin.org/get\"\n}\n")
Response JSON: Optional({
args = {
};
headers = {
Accept = "*/*";
"Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
"Accept-Language" = "en;q=1.0";
Host = "httpbin.org";
"User-Agent" = "AlamofireSourceCodeAnalysis/1.0 (com.xiejiapei.framework.AlamofireSourceCodeAnalysis; build:1; iOS 14.4.0) Alamofire/5.4.1";
"X-Amzn-Trace-Id" = "Root=1-60129046-79f8d1b13ef72f727faa8e36";
};
origin = "222.76.251.163";
url = "https://httpbin.org/get";
})
能实现链式访问的原理就是每个response
函数的返回值都是Self
即Request
。
extension DataRequest
{
public func responseString(...) -> Self
}
extension DownloadRequest
{
public func responseDecodable<T: Decodable>(...) -> Self
}
b、DataResponse:用于存储与DataRequest或UploadRequest的序列化响应关联的所有值的类型
❶ 定义需要保存的数据属性
- 在swift中,如果只是为了保存数据,那么应该把这个类设计成
struct
。struct
是值传递,因此对数据的操作更安全
public struct DataResponse<Success, Failure: Error>
{
// 表示该响应来源于哪个请求
public let request: URLRequest?
// 服务器返回的响应
public let response: HTTPURLResponse?
// 响应数据
public let data: Data?
// 包含了请求和响应的统计信息
public let metrics: URLSessionTaskMetrics?
// 序列化响应所用的时间
public let serializationDuration: TimeInterval
// 响应序列化的结果
public let result: Result<Success, Failure>
// 如果结果成功,则返回结果的关联值,否则返回nil
public var value: Success? { result.success }
// 在请求中可能发生的错误
public var error: Failure? { result.failure }
}
❷ 设计一个符合要求的构造函数
public init(request: URLRequest?,
response: HTTPURLResponse?,
data: Data?,
metrics: URLSessionTaskMetrics?,
serializationDuration: TimeInterval,
result: Result<Success, Failure>)
{
self.request = request
self.response = response
self.data = data
self.metrics = metrics
self.serializationDuration = serializationDuration
self.result = result
}
c、DownloadResponse
// 用于存储与下载请求的序列化响应相关联的所有数据
public struct DownloadResponse<Success, Failure: Error>
{
// 表示该响应来源于哪个请求
public let request: URLRequest?
// 服务器返回的响应
public let response: HTTPURLResponse?
// 从服务器返回的数据移动后的最终位置的URL
public let fileURL: URL?
// 表示可恢复的数据,对于下载任务,如果因为某种原因下载中断或失败了,可以使用该数据恢复之前的下载
public let resumeData: Data?
// 包含了请求和响应的统计信息
public let metrics: URLSessionTaskMetrics?
// 序列化响应所用的时间
public let serializationDuration: TimeInterval
// 响应序列化的结果
public let result: Result<Success, Failure>
// 如果结果成功,则返回结果的关联值,否则返回nil
public var value: Success? { result.success }
// 在请求中可能发生的错误
public var error: Failure? { result.failure }
}
2、序列化响应 ResponseSerialization
a、序列化的设计思路
❶ 最基本的请求
我们先从最简单的事情着手。如果我发起了一个请求,我肯定希望知道请求的结果,那么就会有下边这样的伪代码。伪代码中的response
函数是请求的回调函数,ResponseObj
是对服务器返回的数据的一个抽象。
dataRequest().response{ ResponseObj in }
downloadRequest().response{ ResponseObj in }
❷ 回调函数增加对多线程的支持
默认情况下我们可能希望回调函数会在主线程调用,但是对于某些特定的功能,还是应该增加对多线程的支持,因此我们把上边的代码做一下扩展,给response
函数增加一个参数,这个参数用来决定回调函数会在哪个线程被调用。这里的回调函数会给我们一个未序列化的结果,此时序列化用时serializationDuration
= 0。在Alamofire
中,DataRequest
对应的结果是DataResponse
,DownloadRequest
对应的结果是DownloadResponse
,他们都是struct
类型,是纯正的存储设计类型。我们之所以把data
和download
的请求每次都分开来设计,原因是因为这两个不同的请求得到的响应不一样。download
可以从一个URL
中获取数据,而data
不行。
dataRequest().response(queue 回调函数)
downloadRequest().response(queue 回调函数)
❸ 把上边的伪代码还原成Alamfire中的函数:使用未序列化的结果
扩展 DataRequest
,添加一个在请求结束后会调用获取返回结果的函数。
extension DataRequest
{
@discardableResult
public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse<Data?>) -> Void) -> Self
{
appendResponseSerializer
{
let result = AFResult<Data?>(value: self.data, error: self.error)
self.underlyingQueue.async
{
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
}
return self
}
}
扩展 DownloadRequest
,添加一个在请求结束后会调用获取返回结果的函数。
extension DownloadRequest
{
@discardableResult
public func response(queue: DispatchQueue = .main,
completionHandler: @escaping (AFDownloadResponse<URL?>) -> Void)
-> Self
{
appendResponseSerializer
{
let result = AFResult<URL?>(value: self.fileURL, error: self.error)
self.underlyingQueue.async
{
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
}
return self
}
}
❹ 序列化者遵守的序列化协议
只要在上边的response
方法中添加一个参数就行,这个参数的任务就是完成数据的序列化。此时我们说的系列化就是指可以把响应数据生成Result
的功能。伪代码如下:
dataRequest().response(queue 序列化者 回调函数)
downloadRequest().response(queue 序列化者 回调函数)
序列化者的任务是把数据转换成Result
。因此我们可以把这个序列化者设计成一个类或者结构体,里边提供一个转换的方法就行了,这也是再正常不过的思想。但是swift跟oc不一样,在swift中我们应该转变思维。我们不应该把序列化者用一个固定的对象封死。这个时候就是协议大显身手的时刻了。既然序列化者需要一个函数,那么我们就设计一个包含该函数的协议。SerializedObject
定义了要序列化后的对象类型,这么写的原因也是因为后边会序列成Data
,JOSN
,String
等不同类型的需求。
// 定义数据序列化协议
public protocol DataResponseSerializerProtocol
{
// 序列化后的结果类型
associatedtype SerializedObject
// 序列化方法
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject
}
// 定义下载序列化协议
public protocol DownloadResponseSerializerProtocol
{
associatedtype SerializedObject
func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject
}
❺ 使用指定序列化器序列化的结果
序列化用时serializationDuration = end - start
public func response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void)
-> Self
{
appendResponseSerializer
{
// 应在序列化队列上的开始工作
let start = ProcessInfo.processInfo.systemUptime
// 生成序列化结果
let result: AFResult<Serializer.SerializedObject> = Result
{
// 调用序列化器来序列化结果
try responseSerializer.serialize(request: self.request,
response: self.response,
data: self.data,
error: self.error)
}
.mapError
{ error in
error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
}
// 应在序列化队列上的结束工作
let end = ProcessInfo.processInfo.systemUptime
...
}
return self
}
生成响应,并调用完成回调
self.underlyingQueue.async
{
// 生成响应
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
// 埋点日志
self.eventMonitor?.request(self, didParseResponse: response)
// 序列化过程出错
guard let serializerError = result.failure, let delegate = self.delegate else
{
// 完成回调
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
return
}
...
}
异步询问委托是否将重试Request
delegate.retryResult(for: self, dueTo: serializerError)
{ retryResult in
var didComplete: (() -> Void)?
defer
{
if let didComplete = didComplete
{
// 完成回调
self.responseSerializerDidComplete { queue.async { didComplete() } }
}
}
// 重试结果
switch retryResult
{
case .doNotRetry:
// 完成回调
didComplete = { completionHandler(response) }
case let .doNotRetryWithError(retryError):
let result: AFResult<Serializer.SerializedObject> = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError"))
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
// 完成回调
didComplete = { completionHandler(response) }
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
b、responseString
如果要把data
序列成string
,只需要创建一个data
序列者就好了,但是这样的设计用起来很麻烦,因为还要书写序列成Result
的函数,这些函数往往都是一样的,那么可以把这些函数封装起来,通过封装后的函数来获取序列化后的response
。
extension DataRequest
{
@discardableResult
public func responseString(...completionHandler: @escaping (AFDataResponse<String>) -> Void) -> Self
{
response(queue: queue,
responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}
c、responseData
extension DataRequest
{
public func responseData(completionHandler: @escaping (AFDataResponse<Data>) -> Void) -> Self
{
response(queue: queue,
responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}
d、responseJSON
extension DownloadRequest
{
public func responseJSON(...completionHandler: @escaping (AFDownloadResponse<Any>) -> Void) -> Self
{
response(queue: queue,
responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods,
options: options),
completionHandler: completionHandler)
}
}
e、序列化的核心方法:以字符串为例
// 将data序列化为字符串
public final class StringResponseSerializer: ResponseSerializer
{
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String
{
data = try dataPreprocessor.preprocess(data)
var convertedEncoding = encoding
if let encodingName = response?.textEncodingName, convertedEncoding == nil
{
convertedEncoding = String.Encoding(ianaCharsetName: encodingName)
}
let actualEncoding = convertedEncoding ?? .isoLatin1
return string
}
}
三、底层源码
1、错误处理 AFError
Alamofire
的错误封装很经典,是使用swift中enum
的一个典型案例。只要结果可能是有限的集合的情况下,我们就尽量考虑使用枚举。 其实枚举本身还是数据的一种载体,swift中,枚举有着很丰富的使用方法。
a、ParameterEncodingFailureReason:参数编码错误
❶ 嵌套枚举
ParameterEncodingFailureReason
本身是一个enum
,同时,它又被包含在AFError
之中,这说明枚举之中可以有另一个枚举。
public enum AFError: Error
{
public enum ParameterEncodingFailureReason
{
// 给定的urlRequest.url为nil的情况抛出该错误
case missingURL
// 当选择把参数编码成JSON格式的情况下,参数JSON化出错时抛出的错误
case jsonEncodingFailed(error: Error)
// 自定义编码出错时抛出的错误
case customEncodingFailed(error: Error)
}
}
那么像这种情况我们怎么使用呢?枚举的访问是一级一级进行的。
let parameterErrorReason = AFError.ParameterEncodingFailureReason.missingURL
print(parameterErrorReason)
输出结果为:
missingURL
❷ 关联值
public enum ParameterEncodingFailureReason
{
// 当选择把参数编码成JSON格式的情况下,参数JSON化出错时抛出的错误
case jsonEncodingFailed(error: Error)
}
jsonEncodingFailed(error: Error)
并不是函数,只是枚举的一个普通的子选项,(error: Error)
是它的一个关联值。对于任何一个子选项,我们都可以关联任何值,把这些值与子选项进行绑定的意义在于可以在需要的时候进行调用。
b、MultipartEncodingFailureReason:多部分编码错误
多部分编码错误一般发生在上传或下载请求中对数据的处理过程中。
public enum MultipartEncodingFailureReason
{
// 上传数据时,可以通过fileURL的方式,读取本地文件数据,如果fileURL不可用,就会抛出这个错误
case bodyPartURLInvalid(url: URL)
// 如果使用fileURL的lastPathComponent或者pathExtension获取filename为空则抛出该错误
case bodyPartFilenameInvalid(in: URL)
// 如果通过fileURL不能访问数据,那就是不可达的
case bodyPartFileNotReachable(at: URL)
// 尝试检测fileURL时发现不是可达的则抛出该错误
case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
// 当fileURL是一个文件夹时抛出该错误
case bodyPartFileIsDirectory(at: URL)
// 当使用系统Api获取fileURL指定文件的size出错时抛出该错误
case bodyPartFileSizeNotAvailable(at: URL)
// 查询fileURL指定文件的size出错时抛出该错误
case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
// 通过fileURL创建inputStream出错时抛出该错误
case bodyPartInputStreamCreationFailed(for: URL)
// 当尝试把编码后的数据写入到硬盘时,创建outputStream出错时抛出该错误
case outputStreamCreationFailed(for: URL)
// 数据不能被写入,因为指定的fileURL已经存在
case outputStreamFileAlreadyExists(at: URL)
// fileURL不是一个file URL
case outputStreamURLInvalid(url: URL)
// 数据流写入错误
case outputStreamWriteFailed(error: Error)
// 数据流读入错误
case inputStreamReadFailed(error: Error)
}
c、ResponseValidationFailureReason:响应验证失错误
Alamofire
不管请求是否成功,都会返回response
。Alamofire
提供了验证ContentType
和StatusCode
的功能。
public enum ResponseValidationFailureReason
{
// 保存数据的URL不存在,这种情况一般出现在下载任务中,指的是下载代理中的fileURL缺失
case dataFileNil
// 保存数据的URL无法读取数据,同上
case dataFileReadFailed(at: URL)
// 服务器返回的response不包含ContentType且提供的acceptableContentTypes不包含通配符(通配符表示可以接受任何类型)
case missingContentType(acceptableContentTypes: [String])
// ContentTypes不匹配
case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
// StatusCode不匹配
case unacceptableStatusCode(code: Int)
// 自定义验证失败
case customValidationFailed(error: Error)
}
d、ResponseSerializationFailureReason:响应序列化错误
Alamofire
支持把服务器的response
序列成几种数据格式。
response //直接返回HTTPResponse,未序列化
responseData //序列化为Data
responseJSON //序列化为Json
responseString //序列化为字符串
在序列化的过程中,很可能会发生下边的错误
public enum ResponseSerializationFailureReason
{
// 服务器返回的response没有数据或者数据的长度是0
case inputDataNilOrZeroLength
// 指向数据的URL不存在
case inputFileNil
// 指向数据的URL无法读取数据
case inputFileReadFailed(at: URL)
// 当使用指定的String.Encoding序列化数据为字符串出错时抛出的错误
case stringSerializationFailed(encoding: String.Encoding)
// JSON序列化错误
case jsonSerializationFailed(error: Error)
// 数据解码失败
case decodingFailed(error: Error)
// 自定义序列化失败
case customSerializationFailed(error: Error)
// 无效响应
case invalidEmptyResponse(type: String)
}
e、把上边4个独立的枚举进行串联
上边介绍的4个枚举是被包含在AFError
中彼此独立的枚举,使用的时候需要通过AFError.ParameterEncodingFailureReason
这种方式进行操作。如何把上边4个独立的枚举进行串联呢?由于4个独立的枚举分别代表这个网络框架的4大错误模块,那么我们只需要给AFError
设计4个子选项关联上这4个独立枚举的值就行了。这个设计真的很巧妙,试想,如果把所有的错误都放到AFError
中,就显得非常冗余。
// 无效的URL
case invalidURL(url: URLConvertible)
// 多部分编码失败
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
// 请求参数编码失败
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
// 响应序列化失败
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
f、判断当前的错误是不是某个指定的错误类型
假如我们只想知道某个error
是不是参数编码错误,应该怎么办?AFError
为此提供了多个布尔类型的属性,专门用来获取当前的错误是不是某个指定的错误类型。
extension AFError
{
// 无效的URL
public var isInvalidURLError: Bool
{
if case .invalidURL = self { return true }
return false
}
// 请求参数编码错误
public var isParameterEncodingError: Bool
{
if case .parameterEncodingFailed = self { return true }
return false
}
// 多部分编码错误
public var isMultipartEncodingError: Bool
{
if case .multipartEncodingFailed = self { return true }
return false
}
// 响应验证错误
public var isResponseValidationError: Bool
{
if case .responseValidationFailed = self { return true }
return false
}
// 响应序列化错误
public var isResponseSerializationError: Bool
{
if case .responseSerializationFailed = self { return true }
return false
}
...
}
g、便利属性
extension AFError
{
// 获取某个属性,这个属性实现了URLConvertible协议,在AFError中只有case invalidURL(url: URLConvertible)这个选项符合要求
public var urlConvertible: URLConvertible?
{
guard case let .invalidURL(url) = self else { return nil }
return url
}
// 获取AFError中的URL
public var url: URL?
{
guard case let .multipartEncodingFailed(reason) = self else { return nil }
return reason.url
}
// 可接受的ContentType
public var acceptableContentTypes: [String]?
{
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.acceptableContentTypes
}
// 响应码
public var responseCode: Int?
{
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.responseCode
}
// 错误的字符串编码
public var failedStringEncoding: String.Encoding?
{
guard case let .responseSerializationFailed(reason) = self else { return nil }
return reason.failedStringEncoding
}
}
h、错误描述
在开发中,如果程序遇到错误,我们往往会给用户展示更加直观的信息,这就要求我们把错误信息转换成易于理解的内容。因此我们只要实现LocalizedError
协议就好了。
extension AFError: LocalizedError
{
public var errorDescription: String?
{
switch self
{
case let .invalidURL(url):
return "URL is not valid: \(url)"
case let .parameterEncodingFailed(reason):
return reason.localizedDescription
case let .multipartEncodingFailed(reason):
return reason.localizedDescription
case let .responseValidationFailed(reason):
return reason.localizedDescription
case let .responseSerializationFailed(reason):
return reason.localizedDescription
}
}
}
2、会话代理 SessionDelegate
a、概念
网络请求过程
一条最普通的网络请求,究竟是怎样的一个过程?首先我们根据一个URL
和若干个参数生成Request
,然后根据Request
生成一个会话Session
,再根据这个Session
生成Task
,我们开启Task
就完成了这个请求。当然这一过程之中还会包含重定向,数据上传,挑战,证书等等一系列的配置信息。
代理
我们再聊聊代理的问题,不管是在网络请求中,还是再其他的地方,代理都类似于一个管理员的身份。这在业务架构中是一个很好的主意。假如我把代理想象成一个人,那么这个人的工作是什么呢?一是通过代理提供我所需要的数据。二是我知道这个业务你比较精通,当有和你相关的事情的时候,我会通知你来解决。
b、初始化
❶ 初始化Session时传入SessionDelegate
public init(session: URLSession,
delegate: SessionDelegate,
eventMonitors: [EventMonitor] = [])
{
self.session = session
self.delegate = delegate
eventMonitor = CompositeEventMonitor(monitors: defaultEventMonitors + eventMonitors)
delegate.eventMonitor = eventMonitor //事件监听器
delegate.stateProvider = self //状态监听器
}
❷ 负责处理所有与内部 session 关联的代理回调
open class SessionDelegate: NSObject
{
// 状态监听器
weak var stateProvider: SessionStateProvider?
// 事件监听器
var eventMonitor: EventMonitor?
}
c、URLSessionDataDelegate
extension SessionDelegate: URLSessionDataDelegate
{
.....
}
❶ 接受到数据
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
{
eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: data)
if let request = request(for: dataTask, as: DataRequest.self)
{
request.didReceive(data: data)
}
}
❷ 是否需要缓存响应
该函数用于处理是否需要缓存响应,Alamofire
默认是缓存这些response
的。
open func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse,
completionHandler: @escaping (CachedURLResponse?) -> Void)
{
eventMonitor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse)
if let handler = stateProvider?.request(for: dataTask)?.cachedResponseHandler ?? stateProvider?.cachedResponseHandler
{
handler.dataTask(dataTask, willCacheResponse: proposedResponse, completion: completionHandler)
}
else
{
completionHandler(proposedResponse)
}
}
d、URLSessionDownloadDelegate
❶ 数据下载完成
当数据下载完成后,该函数被触发。系统会把数据下载到一个临时的locationURL
的地方,我们就是通过这个URL
拿到数据的。上边函数内的代码主要是把数据复制到目标路径中。
open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
{
eventMonitor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
let (destination, options): (URL, DownloadRequest.Options)
if let response = request.response
{
(destination, options) = request.destination(location, response)
}
eventMonitor?.request(request, didCreateDestinationURL: destination)
if options.contains(.removePreviousFile), fileManager.fileExists(atPath: destination.path)
{
try fileManager.removeItem(at: destination)
}
if options.contains(.createIntermediateDirectories)
{
let directory = destination.deletingLastPathComponent()
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
try fileManager.moveItem(at: location, to: destination)
request.didFinishDownloading(using: downloadTask, with: .success(destination))
}
❷ 提供下载进度
该代理方法在数据下载过程中被触发,主要的作用就是提供下载进度。
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
❸ 断点续传
如果一个下载的task
是可以恢复的,那么当下载被取消或者失败后,系统会返回一个resumeData
对象,这个对象包含了一些跟这个下载task
相关的一些信息,有了它就能重新创建下载task
,创建方法有两个:downloadTask(withResumeData:)
和downloadTask(withResumeData:completionHandler:)
,当task
开始后,上边的代理方法就会被触发。
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64)
{
downloadRequest.updateDownloadProgress(bytesWritten: fileOffset,
totalBytesExpectedToWrite: expectedTotalBytes)
}
e、URLSessionTaskDelegate
提供上传的进度
open func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64)
{
stateProvider?.request(for: task)?.updateUploadProgress(totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend)
}
四、其他源码
1、通知处理 Notifications
通知作为传递事件和数据的载体,在使用中是不受限制的,但是如果忘记移除某个通知的监听,就会造成很多潜在的问题,这些问题在测试中是很难被发现的。有的团队为了管理通知,创建了一个类似于NotificationManager
的类,所有通知的添加移除都通过这个类进行管理,通过打印通知数组就能很清楚的看到添加了哪些通知,以及这些通知被绑定在了哪些对象之上,这是一个很好地思路。
a、通知的名称
swift中发通知的函数原型如下,除了name
之外,其他参数跟OC没什么区别。name
的主要作用就是作为通知的唯一标识,但由于在OC中这个name
是一个最普通的字符串,就导致了开发中乱用的问题。很多人为了管理这些通知字符串想出了很多办法,比如把这些字符串放到一个或几个文件中,或者写一个类根据不同功能提供不同的字符串(这个是比较推荐的写法,也和本篇中讲解的用法很像)。
open func post(name aName: NSNotification.Name, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil)
b、创建命名空间
通知名
- 用于指示通知名,在这里作用类似于一个命名空间
- 把跟
request
相关的通知都绑定在这个request
上
extension Request
{
// 在 Request 继续运行的时候会发送通知,通知中含有此 Request
public static let didResumeNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume")
// 在 Request 暂停的时候会发送通知,通知中含有此 Request
public static let didSuspendNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend")
// 在 Request 取消的时候会发送通知,通知中含有此 Request
public static let didCancelNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel")
// 在 Request 完成的时候会发送通知,通知中含有此 Request
public static let didFinishNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didFinish")
// 在 URLSessionTask 继续运行的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public static let didResumeTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResumeTask")
// 在 URLSessionTask 暂停的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public static let didSuspendTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspendTask")
// 在 URLSessionTask 取消的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public static let didCancelTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancelTask")
// 在 URLSessionTask 完成的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public static let didCompleteTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCompleteTask")
}
用户信息字典键值
- 这里也是起到一个命名空间的作用,用于标记指定键值
extension String
{
// 表示与通知关联的Request的用户信息字典键值
fileprivate static let requestKey = "org.alamofire.notification.key.request"
}
c、使用命名空间
❶ 通过字典键值从通知的用户信息中获取到关联的Request
extension Notification
{
// 通过字典键值从通知的用户信息中获取到关联的Request
public var request: Request?
{
userInfo?[String.requestKey] as? Request
}
}
❷ 传入通知名称和当前Request初始化通知
extension Notification
{
// 传入通知名称和当前Request初始化通知
init(name: Notification.Name, request: Request)
{
self.init(name: name, object: nil, userInfo: [String.requestKey: request])
}
}
❸ 传入通知名称和当前Request发送通知
extension NotificationCenter
{
// 传入通知名称和当前Request发送通知
func postNotification(named name: Notification.Name, with request: Request)
{
let notification = Notification(name: name, request: request)
post(notification)
}
}
❹ EventMonitor提供Alamofire通知发送的时机
public final class AlamofireNotifications: EventMonitor
{
// 当Request收到resume调用时调用的事件
public func requestDidResume(_ request: Request)
{
NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request)
}
// 当Request收到suspend调用时调用的事件
public func requestDidSuspend(_ request: Request) {
NotificationCenter.default.postNotification(named: Request.didSuspendNotification, with: request)
}
// 当Request收到cancel调用时调用的事件
public func requestDidCancel(_ request: Request)
{
NotificationCenter.default.postNotification(named: Request.didCancelNotification, with: request)
}
// 当Request收到finish调用时调用的事件
public func requestDidFinish(_ request: Request)
{
NotificationCenter.default.postNotification(named: Request.didFinishNotification, with: request)
}
}
public final class AlamofireNotifications: EventMonitor
{
// 在 URLSessionTask 继续运行的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public func request(_ request: Request, didResumeTask task: URLSessionTask)
{
NotificationCenter.default.postNotification(named: Request.didResumeTaskNotification, with: request)
}
// 在 URLSessionTask 暂停的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public func request(_ request: Request, didSuspendTask task: URLSessionTask)
{
NotificationCenter.default.postNotification(named: Request.didSuspendTaskNotification, with: request)
}
// 在 URLSessionTask 取消的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public func request(_ request: Request, didCancelTask task: URLSessionTask)
{
NotificationCenter.default.postNotification(named: Request.didCancelTaskNotification, with: request)
}
// 在 URLSessionTask 完成的时候会发送通知,通知中含有此 URLSessionTask 关联的 Request
public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?)
{
NotificationCenter.default.postNotification(named: Request.didCompleteTaskNotification, with: request)
}
}
2、网络监控 NetworkReachabilityManager
a、用途
使用场景
- 聊天列表需要实时监控当前的网络是不是可达的,如果不可达则出现联网失败的提示
- 网络视频播放需要判断当前的网络状态,如果不是WiFi应该给出消耗流量来播放的提示
- 可以把请求放进缓存,当监听到网络连接成功后发送。举个例子,每次进app都要把位置信息发给服务器,如果发送失败后,发现是网络不可达造成的失败,那么可以把这个请求放入到一个队列中,在网络可达的时候,开启队列任务
- 当网络状态变化时,实时的给用户提示信息
获取网络状态
- 所以在开发中有时候我们需要获取这些信息:手机是否联网,当前网络是WiFi还是蜂窝
- 但是极其不建议在发请求前,先检测当前的网络是不是可达,因为手机的网络状态是经常变化的
b、SCNetworkReachabilityFlags
功能
- 能够判断某个指定的网络节点名称或者地址是不是可达的
- 也能判断该节点或地址是不是需要先建立连接
- 也可以判断是不是需要用户手动去建立连接
属性
// 表明当前指定的节点或地址是可达的
public static var reachable: SCNetworkReachabilityFlags { get }
// 要想和指定的节点或地址通信,需要先建立连接,比如在很多地方需要输入手机号获取验证码后才能联网
public static var connectionRequired: SCNetworkReachabilityFlags { get }
// 需要用户手动提供一些数据,比如密码或者token
public static var interventionRequired: SCNetworkReachabilityFlags { get }
// 表明是不是本地地址
public static var isLocalAddress: SCNetworkReachabilityFlags { get }
// 表明是不是通过蜂窝网络连接
public static var isWWAN: SCNetworkReachabilityFlags { get }
// 表面是不是自动连接
public static var connectionAutomatic: SCNetworkReachabilityFlags { get }
c、枚举与属性
通过枚举获取当前的网络连接类型
// 对于手机而言,我们需要的连接类型就两种
public enum ConnectionType
{
// 一种是WiFi网络
case ethernetOrWiFi
// 一种是蜂窝网络
case cellular
}
网络连接状态
// 网络连接状态明显要比网络类型范围更大,因此又增加了两个选项
public enum NetworkReachabilityStatus
{
// 表示当前的网络是未知的
case unknown
// 表示当前的网路不可达
case notReachable
// 在关联的ConnectionType上可以访问网络
case reachable(ConnectionType)
}
监听器类型
- 监听器类型实质是一个闭包
- 当网络状态改变时,闭包会被调用
- 闭包只有一个参数,为网络可达性状态
public typealias Listener = (NetworkReachabilityStatus) -> Void
属性
// 当前网络是可达的,要么是蜂窝网络,要么是WiFi连接
open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi }
// 表明当前网络是通过蜂窝网络连接
open var isReachableOnCellular: Bool { status == .reachable(.cellular) }
// 表明当前网络是通过WiFi连接
open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) }
// 返回当前的网络状态
open var status: NetworkReachabilityStatus
{
flags.map(NetworkReachabilityStatus.init) ?? .unknown
}
// 监听器中代码执行所在的队列
public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue")
// 网络状态就是根据flags判断出来的
open var flags: SCNetworkReachabilityFlags?
{
// 有了它才能获取flags
var flags = SCNetworkReachabilityFlags()
return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil
}
// 可达性
private let reachability: SCNetworkReachability
d、初始化方法
便利构造函数:通过传入一个主机地址验证可达性
public convenience init?(host: String)
{
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
self.init(reachability: reachability)
}
直接调用init方法会默认的设置为指向0.0.0.0
- 可达性将0.0.0.0视为一个特殊的地址,因为它会监听设备的路由信息,在 ipv4 和 ipv6 下都可以使用
public convenience init?()
{
var zero = sockaddr()
zero.sa_len = UInt8(MemoryLayout<sockaddr>.size)
zero.sa_family = sa_family_t(AF_INET)
guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil }
self.init(reachability: reachability)
}
通过指定 SCNetworkReachability 来初始化
private init(reachability: SCNetworkReachability)
{
self.reachability = reachability
}
在析构函数中会停止监听网络变化
deinit
{
stopListening()
}
e、监听器
开始监控网络状态
@discardableResult //表明可以忽略返回值
open func startListening(onQueue queue: DispatchQueue = .main,
onUpdatePerforming listener: @escaping Listener) -> Bool
{
stopListening()
// 创建一个监听器
$mutableState.write
{ state in
state.listenerQueue = queue
state.listener = listener
}
// 设置上下文
var context = SCNetworkReachabilityContext(version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil)
// 创建回调函数
let callback: SCNetworkReachabilityCallBack =
{ _, flags, info in
guard let info = info else { return }
// 获取 NetworkReachabilityManager 的实例对象
let instance = Unmanaged<NetworkReachabilityManager>.fromOpaque(info).takeUnretainedValue()
// 通知监听者,也就是触发回调函数
instance.notifyListener(flags)
}
// 注册队列
let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue)
// 注册回调函数
let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context)
// 通知网络状态发送改变
if let currentFlags = flags
{
reachabilityQueue.async
{
self.notifyListener(currentFlags)
}
}
return callbackAdded && queueAdded
}
停止监控网络状态
open func stopListening()
{
// 取消回调
SCNetworkReachabilitySetCallback(reachability, nil, nil)
// 取消队列
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
// 销毁监听器
$mutableState.write
{ state in
state.listener = nil
state.listenerQueue = nil
state.previousStatus = nil
}
}
Demo
Demo在我的Github上,欢迎下载。
SourceCodeAnalysisDemo