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

參考文獻

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。

推薦閱讀更多精彩內容