IOS源码解析:Alamofire 5 功能模块

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的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这个参数去掉,这个参数可以根据namemimeTypefileName计算出来,因此有了下边的函数。

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个参数:namemimeTypefileName。根据data生成InputStreamlength是关键。

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是不是一个fileURL

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类型,他们分别是:DataResponseDownloadResponse

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函数的返回值都是SelfRequest

extension DataRequest
{
    public func responseString(...) -> Self
}

extension DownloadRequest
{
    public func responseDecodable<T: Decodable>(...) -> Self
}

b、DataResponse:用于存储与DataRequest或UploadRequest的序列化响应关联的所有值的类型
❶ 定义需要保存的数据属性
  • 在swift中,如果只是为了保存数据,那么应该把这个类设计成structstruct是值传递,因此对数据的操作更安全
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对应的结果是DataResponseDownloadRequest对应的结果是DownloadResponse,他们都是struct类型,是纯正的存储设计类型。我们之所以把datadownload的请求每次都分开来设计,原因是因为这两个不同的请求得到的响应不一样。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定义了要序列化后的对象类型,这么写的原因也是因为后边会序列成DataJOSNString等不同类型的需求。

// 定义数据序列化协议
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不管请求是否成功,都会返回responseAlamofire提供了验证ContentTypeStatusCode的功能。

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

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,179评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,229评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,032评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,533评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,531评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,539评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,916评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,813评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,568评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,654评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,354评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,937评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,918评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,152评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,852评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,378评论 2 342

推荐阅读更多精彩内容