原創:知識點總結性文章
創作不易,請珍惜,之后會持續更新,不斷完善
個人比較喜歡做筆記和寫總結,畢竟好記性不如爛筆頭哈哈,這些文章記錄了我的IOS成長歷程,希望能與大家一起進步
溫馨提示:由于簡書不支持目錄跳轉,大家可通過command + F 輸入目錄標題后迅速尋找到你所需要的內容
續文見上篇:IOS源碼解析:Alamofire 5 核心
目錄
- 一、請求源碼
- 1、證書校驗 ServerTrustEvaluation
- 2、多表單 MultipartFormData
- 3、網絡請求 Request
- 二、響應源碼
- 1、請求響應 Response
- 2、序列化響應 ResponseSerialization
- 三、底層源碼
- 1、錯誤處理 AFError
- 2、會話代理 SessionDelegate
- 四、其他源碼
- 1、通知處理 Notifications
- 2、網絡監控 NetworkReachabilityManager
- Demo
- 參考文獻
一、請求源碼
1、ServerTrustEvaluation
a、HTTPS請求的過程
步驟一:HTTPS請求以https開頭。我們首先向服務器發送一條請求。
步驟二:服務器需要一個證書
這個證書可以從某些機構獲得,也可以自己通過工具生成,通過某些合法機構生成的證書客戶端不需要進行驗證,這樣的請求不會觸發Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)
代理方法。自己生成的證書則需要客戶端進行驗證。
由于使用非對稱加密,證書中包含公鑰和私鑰。公鑰是公開的,任何人都可以使用該公鑰加密數據,只有知道了私鑰才能解密數據。私鑰是要求高度保密的,只有知道了私鑰才能解密用公鑰加密的數據。
步驟三:服務器會把公鑰發送給客戶端
步驟四:客戶端此刻就拿到了公鑰
這里不是直接就拿公鑰加密數據發送了,因為這僅僅能滿足客戶端給服務器發加密數據,那么服務器怎么給客戶端發送加密數據呢?因此需要在客戶端和服務器間建立一條通道,通道的密碼只有客戶端和服務器知道。只能讓客戶端自己生成一個密碼,這個密碼就是一個隨機數,這個隨機數絕對是安全的,因為目前只有客戶端自己知道。
步驟五:客戶端把這個隨機數通過公鑰加密后發送給服務器,就算被別人截獲了加密后的數據,在沒有私鑰的情況下,是根本無法解密的
步驟六:服務器用私鑰把數據解密后,就獲得了這個隨機數
步驟七:到這里客戶端和服務器的安全連接就已經建立了。最主要的目的是交換隨機數,然后服務器就用這個隨機數把數據加密后發給客戶端,使用的是對稱加密技術
步驟八:客戶端獲得了服務器的加密數據,使用隨機數解密,到此,客戶端和服務器就能通過隨機數發送數據了
b、ServerTrustManager
? 策略字典
ServerTrustManager
是對ServerTrustEvaluating
的管理。我們可以暫時把ServerTrustEvaluating
當做是一個安全策略,就是指對一個服務器采取的策略。然而在真實的開發中,一個APP可能會用到很多不同的主機地址(host
),因此就產生了這樣的需求,為不同的host
綁定一個特定的安全策略。
open class ServerTrustManager
{
// 映射到特定主機的策略字典
public let evaluators: [String: ServerTrustEvaluating]
}
? 根據host讀取策略
open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating?
{
let evaluator = evaluators[host]
return evaluator
}
? 使用方式
let manager = ServerTrustManager(evaluators: ["httpbin.org": PinnedCertificatesTrustEvaluator()])
let managerSession = Session(serverTrustManager: manager)
managerSession.request("https://httpbin.org/get").responseJSON
{ response in
debugPrint(response)
}
輸出結果為:
打印HTTPMethod的值:HTTPMethod(rawValue: "GET")
打印HTTPMethod的原始值:GET
[Request]: GET https://httpbin.org/get
[Headers]: None
[Body]: None
[Response]: None
[Network Duration]: None
[Serialization Duration]: 5.08379889652133e-05s
[Result]: failure(Alamofire.AFError.sessionDeinitialized)
c、ServerTrustEvaluating
描述用于評估服務器信任的API的協議
public protocol ServerTrustEvaluating
{
// 為給定的host計算給定的SecTrust值
func evaluate(_ trust: SecTrust, forHost host: String) throws
}
實現該協議的類
// 默認的策略,只有合法證書才能通過驗證
public final class DefaultTrustEvaluator: ServerTrustEvaluating
// 對注銷證書做的一種額外設置
public final class RevocationTrustEvaluator: ServerTrustEvaluating
// 如果不驗證證書鏈的話,只要對比指定的證書有沒有和服務器信任的證書匹配項,只要有一個能匹配上,就驗證通過
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
// 和上邊的那個差不多,只是驗證對象改為PublicKeys
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating
// 組合驗證
public final class CompositeTrustEvaluator: ServerTrustEvaluating
// 無條件信任,驗證一直都是通過的
public final class DisabledTrustEvaluator: ServerTrustEvaluating
d、獲取證書
在開發中,如果和服務器的安全連接需要對服務器進行驗證,最好的辦法就是在本地保存一些證書,拿到服務器傳過來的證書,然后進行對比,如果有匹配的,就表示可以信任該服務器。從上邊的函數中可以看出,Alamofire
會在Bundle
(默認為main
)中查找帶有[".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]
后綴的證書。
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
{
private let certificates: [SecCertificate]
public init(certificates: [SecCertificate] = Bundle.main.af.certificates)
{
self.certificates = certificates
}
}
下邊函數中的paths
保存的就是這些證書的路徑。map
把這些后綴轉換成路徑,我們以.cer
為例,通過map
后,原來的.cer
就變成了一個數組,也就是說通過map
后,原來的數組變成了二維數組了,然后再通過joined()
函數,把二維數組轉換成一維數組。然后要做的就是根據這些路徑獲取證書數據了。
extension Bundle: AlamofireExtended {}
extension AlamofireExtension where ExtendedType: Bundle
{
public var certificates: [SecCertificate]
{
paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap
{ path in
guard
let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil }
return certificate
}
}
}
e、獲取公鑰
PublicKeysTrustEvaluator
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating
{
private let keys: [SecKey]
private let performDefaultValidation: Bool
private let validateHost: Bool
public init(keys: [SecKey] = Bundle.main.af.publicKeys,
performDefaultValidation: Bool = true,
validateHost: Bool = true)
{
self.keys = keys
self.performDefaultValidation = performDefaultValidation
self.validateHost = validateHost
}
}
返回包中有效證書的所有公鑰,就是在本地證書中取出公鑰
public var publicKeys: [SecKey]
{
certificates.af.publicKeys
}
f、核心的方法evaluate:以Pinned證書驗證為例
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating
{
public func evaluate(_ trust: SecTrust, forHost host: String) throws
{
// 未發現證書
guard !certificates.isEmpty else {
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
// 接受自簽名證書
if acceptSelfSignedCertificates
{
try trust.af.setAnchorCertificates(certificates)
}
// 執行默認驗證
if performDefaultValidation
{
try trust.af.performDefaultValidation(forHost: host)
}
// 驗證主機
if validateHost
{
try trust.af.performValidation(forHost: host)
}
// 服務器證書數據
let serverCertificatesData = Set(trust.af.certificateData)
// pinned證書數據
let pinnedCertificatesData = Set(certificates.af.data)
// pinned證書數據 在 服務器證書數據中
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
// 倘若不在則驗證失敗
if !pinnedCertificatesInServerData
{
throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host,
trust: trust,
pinnedCertificates: certificates,
serverCertificates: trust.af.certificates))
}
}
}
2、多表單 MultipartFormData
a、多表單格式
我相信應該有不少的開發者不明白多表單是怎么一回事,嗯,我也一樣,所以我們一起來學習下。試想一下,如果有多個不同類型的文件(png/txt/mp3/pdf
等等)需要上傳給服務器,你打算怎么辦?如果你一個一個的上傳,那我無話可說,但是如果你想一次性上傳,那么就要考慮服務端如何識別這些不同類型的數據呢?服務端對不同類型數據的識別解決方案就是多表單。客戶端與服務端共同制定一套規范,彼此使用該規則交換數據就完全ok了。
POST / HTTP/1.1
[[ Less interesting headers ... ]]
// 通過Content-Type來說明當前數據的類型為multipart/form-data,這樣服務器就知道客戶端將要發送的數據是多表單了
// 多表單說白了就是把各種數據拼接起來,要想區分不同數據,必須添加一個界限標識符boundary
Content-Type: multipart/form-data; boundary=735323031399963166993862150
// 告訴服務端數據的總長度,在后邊的代碼中會有一個屬性來提供這個數據
// 我們最終上傳的數據都是二進制流,因此獲取到Data就能計算大小
Content-Length: 834
// 如果在boundary前邊添加了--就表示是多表單的開始邊界
--735323031399963166993862150
// 對內容的進一步說明
Content-Disposition: form-data; name="text1"
text default
735323031399963166993862150
Content-Disposition: form-data; name="text2"
aωb
735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
// 表示對表單內該數據的類型的說明
Content-Type: text/plain
Content of a.txt.
735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html
<!DOCTYPE html><title>Content of a.html.</title>
735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream
aωb
// 在boundary后面添加--就表示是結束邊界
735323031399963166993862150--
b、Boundary
// 邊界生產者
enum BoundaryGenerator
{
...
}
// 換行回車:對"\r\n"的一個封裝
enum EncodingCharacters
{
static let crlf = "\r\n"
}
? 設計一個枚舉來封裝邊界類型:開始邊界、內部邊界、結束邊界
enum BoundaryType
{
case initial, encapsulated, final
}
? 生成邊界字符串:通常該字符串采用隨機數生成的方式
static func randomBoundary() -> String
{
let first = UInt32.random(in: UInt32.min...UInt32.max)
let second = UInt32.random(in: UInt32.min...UInt32.max)
return String(format: "alamofire.boundary.%08x%08x", first, second)
}
? 轉換函數:因為最終上傳的數據是Data類型,所以需要將邊界轉換成Data類型
static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data
{
let boundaryText: String
switch boundaryType
{
case .initial:
boundaryText = "--\(boundary)\(EncodingCharacters.crlf)"
case .encapsulated:
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)\(EncodingCharacters.crlf)"
case .final:
boundaryText = "\(EncodingCharacters.crlf)--\(boundary)--\(EncodingCharacters.crlf)"
}
return Data(boundaryText.utf8)
}
c、BodyPart
對每一個body
部分的描述,這個類只能在MultipartFormData
內部訪問,外部無法訪問。
open class MultipartFormData
{
class BodyPart
{
let headers: HTTPHeaders //對數據的描述
let bodyStream: InputStream //數據來源,Alamofire中使用InputStream統一進行處理
let bodyContentLength: UInt64 //該數據的大小
var hasInitialBoundary = false //是否包含初始邊界
var hasFinalBoundary = false //是否包含結束邊界
init(headers: HTTPHeaders, bodyStream: InputStream, bodyContentLength: UInt64)
{
self.headers = headers
self.bodyStream = bodyStream
self.bodyContentLength = bodyContentLength
}
}
}
d、MultipartFormData
- 提供一些在請求時需要的參數
- 提供各種數據拼接的入口
- 如果數據過大,為了性能,提供把數據寫入文件的功能
屬性
// 通過Content-Type來說明當前數據的類型為multipart/form-data,這樣服務器就知道客戶端將要發送的數據是多表單了
open lazy var contentType: String = "multipart/form-data; boundary=\(self.boundary)"
// 獲取數據的大小,該屬性是一個計算屬性
public var contentLength: UInt64 { bodyParts.reduce(0) { $0 + $1.bodyContentLength } }
// 表示邊界,在初始化中會使用BoundaryGenerator來生成一個邊界字符串
public let boundary: String
// 是一個集合,包含了每一個數據的封裝對象BodyPart
private var bodyParts: [BodyPart]
// 設置stream傳輸的buffer大小
private let streamBufferSize: Int
初始化方法
public init(fileManager: FileManager = .default, boundary: String? = nil)
{
self.fileManager = fileManager
self.boundary = boundary ?? BoundaryGenerator.randomBoundary()
bodyParts = []
streamBufferSize = 1024
}
e、Body Parts:將多種不同類型的文件拼接到bodyParts數組中
? 3種輸入源
Data //直接提供Data類型的數據,比如把一張圖片編碼成Data,然后拼接進來
fileURL //通過一個文件的本地URL來獲取數據,然后拼接進來
Stream //直接通過stream導入數據
? 提供參數來描述輸入源傳入的數據
name //與數據相關的名字
mimeType //表示數據的類型
fileName //表示數據的文件名稱
length //表示數據大小
stream //表示輸入流
headers //數據的headers
? 設計函數來實現把每一條數據封裝成BodyPart對象,然后拼接到bodyParts數組中
public func append(_ stream: InputStream, withLength length: UInt64, headers: HTTPHeaders)
{
// 給出headers,stream和length就能生成BodyPart對象
let bodyPart = BodyPart(headers: headers, bodyStream: stream, bodyContentLength: length)
// 然后把它拼接到數組中就行了
bodyParts.append(bodyPart)
}
如果每次都是用上邊的函數拼接數據,我們估計會瘋掉,因為必須要對它的3個參數非常了解才行。因此,這就說明上邊的函數是最底層的函數方案。之所以稱為最底層,因為他可定義的靈活性很高,使用起來也很麻煩。我們接下來要考慮的就是如何減少開發過程中的使用障礙。那么現在要設計一個包含最多參數的函數,這個函數會成為其他函數的內部實現基礎。我們把headers這個參數去掉,這個參數可以根據name
、mimeType
、fileName
計算出來,因此有了下邊的函數。
public func append(_ fileURL: URL, withName name: String, fileName: String, mimeType: String)
{
// 根據name,mimeType,fileName計算出headers
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
append(stream, withLength: bodyContentLength, headers: headers)
}
如果我傳入的數據是個Data
類型呢?能對Data
進行描述的有3個參數:name
,mimeType
,fileName
。根據data
生成InputStream
和length
是關鍵。
public func append(_ data: Data, withName name: String, fileName: String? = nil, mimeType: String? = nil)
{
let headers = contentHeaders(withName: name, fileName: fileName, mimeType: mimeType)
let stream = InputStream(data: data)
let length = UInt64(data.count)
append(stream, withLength: length, headers: headers)
}
? 當需要把文件寫入fileURL中或者從fileURL中讀取數據時對錯誤的處理
判斷fileURL
是不是一個file
的URL
guard fileURL.isFileURL else
{
setBodyPartError(withReason: .bodyPartURLInvalid(url: fileURL))
return
}
判斷該fileURL
是不是可達的
do
{
let isReachable = try fileURL.checkPromisedItemIsReachable()
guard isReachable else
{
setBodyPartError(withReason: .bodyPartFileNotReachable(at: fileURL))
return
}
}
catch
{
setBodyPartError(withReason: .bodyPartFileNotReachableWithError(atURL: fileURL, error: error))
return
}
判斷fileURL
是不是一個文件夾,而不是具體的數據
var isDirectory: ObjCBool = false
let path = fileURL.path
guard fileManager.fileExists(atPath: path, isDirectory: &isDirectory) && !isDirectory.boolValue else
{
setBodyPartError(withReason: .bodyPartFileIsDirectory(at: fileURL))
return
}
判斷fileURL
指定的文件能不能被讀取
let bodyContentLength: UInt64
do
{
guard let fileSize = try fileManager.attributesOfItem(atPath: path)[.size] as? NSNumber else
{
setBodyPartError(withReason: .bodyPartFileSizeNotAvailable(at: fileURL))
return
}
bodyContentLength = fileSize.uint64Value
}
catch
{
setBodyPartError(withReason: .bodyPartFileSizeQueryFailedWithError(forURL: fileURL, error: error))
return
}
判斷能不能通過fileURL
創建InputStream
guard let stream = InputStream(url: fileURL) else
{
setBodyPartError(withReason: .bodyPartInputStreamCreationFailed(for: fileURL))
return
}
f、將bodyParts數組中的模型拼接成一個完整的Data
? 編碼整個請求體(在內存中讀取并編碼)
public func encode() throws -> Data
{
// 如果發生異常, 則直接拋出
if let bodyPartError = bodyPartError
{
throw bodyPartError
}
// 最終的請求體
var encoded = Data()
// 給數組中第一個數據設置開始邊界,最后一個數據設置結束邊界
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
// 拼接 body
for bodyPart in bodyParts
{
// 編碼內容塊
let encodedData = try encode(bodyPart)
// 把bodyPart對象轉換成Data類型,然后拼接到encoded中
encoded.append(encodedData)
}
return encoded
}
? 編碼內容塊為data (在內存中讀取并編碼)
private func encode(_ bodyPart: BodyPart) throws -> Data
{
// 最終數據
var encoded = Data()
// 如果是第一個數據, 要使用起始字段, 否則用正常分割字段
let initialData = bodyPart.hasInitialBoundary ? initialBoundaryData() : encapsulatedBoundaryData()
encoded.append(initialData)
// 添加頭字段
let headerData = encodeHeaders(for: bodyPart)
encoded.append(headerData)
// 添加內容字段
let bodyStreamData = try encodeBodyStream(for: bodyPart)
encoded.append(bodyStreamData)
/// 如果是最后一個數據, 要加上尾字段
if bodyPart.hasFinalBoundary
{
encoded.append(finalBoundaryData())
}
return encoded
}
g、寫入編碼好的數據到指定文件(處理大尺寸數據)
在Alamofire
中,如果編碼后的數據超過了某個值,就會把該數據寫入到fileURL
中,在發送請求的時候,在fileURL
中讀取數據上傳。Alamofire
并沒有使用上邊的encode
函數來生成一個Data
,然后再寫入fileURL
。這是因為大文件往往我們是通過append(fileURL)
方式拼接進來的,并沒有把數據加載到內存。
public func writeEncodedData(to fileURL: URL) throws
{
if let bodyPartError = bodyPartError
{
throw bodyPartError
}
// 判斷寫入文件是否已存在
if fileManager.fileExists(atPath: fileURL.path)
{
throw AFError.multipartEncodingFailed(reason: .outputStreamFileAlreadyExists(at: fileURL))
}
// 判斷是否是文件 url
else if !fileURL.isFileURL
{
throw AFError.multipartEncodingFailed(reason: .outputStreamURLInvalid(url: fileURL))
}
// 生成流
guard let outputStream = OutputStream(url: fileURL, append: false) else
{
throw AFError.multipartEncodingFailed(reason: .outputStreamCreationFailed(for: fileURL))
}
// 開啟流
outputStream.open()
// defer可以定義代碼塊結束后執行的語句
defer { outputStream.close() }
// 設置頭尾
bodyParts.first?.hasInitialBoundary = true
bodyParts.last?.hasFinalBoundary = true
for bodyPart in bodyParts
{
// 寫入
try write(bodyPart, to: outputStream)
}
}
3、網絡請求 Request
a、request方法
? 外界調用的request方法
// 外界調用網絡請求方法
AF.request("https://httpbin.org/get")
// 提供給外界調用的網絡請求方法
open func request(...) -> DataRequest
{
let convertible = RequestConvertible(...)
// 內部實現
return request(convertible, interceptor: interceptor)
}
? 內部實現的request方法
open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest
{
// 創建DataRequest
let request = DataRequest(convertible: convertible,
underlyingQueue: rootQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: self)
// 執行提供的Request
perform(request)
return request
}
b、DataRequest 數據請求類
UploadRequest
DataRequest
DownloadRequest
DataStreamRequest
? 屬性
public class DataRequest: Request
{
// URLRequestConvertible值,用于為此實例創建URLRequest
public let convertible: URLRequestConvertible
// 目前為止從服務器讀取的數據
public var data: Data? { mutableData }
// 可變數據類型,將每次從服務器讀取的數據添加到其后
@Protected
private var mutableData: Data? = nil
}
? 重置網絡請求的方法
override func reset()
{
super.reset()
mutableData = nil
}
? 當此實例接收到Data時調用以下方法,在更新下載進度的時候也會被調用
func didReceive(data: Data)
{
if self.data == nil
{
// 第一次拿到數據
mutableData = data
}
else
{
// 拼接之后請求到的數據
$mutableData.write { $0?.append(data) }
}
// 更新下載進度
updateDownloadProgress()
}
? 根據request獲取dataTask
override func task(for request: URLRequest, using session: URLSession) -> URLSessionTask
{
let copiedRequest = request
return session.dataTask(with: copiedRequest)
}
? 更新下載進度
func updateDownloadProgress()
{
let totalBytesReceived = Int64(data?.count ?? 0)
let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
downloadProgress.totalUnitCount = totalBytesExpected
downloadProgress.completedUnitCount = totalBytesReceived
downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) }
}
? 使用指定的閉包驗證請求
public func validate(_ validation: @escaping Validation) -> Self
{
let validator: () -> Void =
{ [unowned self] in
guard self.error == nil, let response = self.response else { return }
let result = validation(self.request, response, self.data)
if case let .failure(error) = result { self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error))) }
self.eventMonitor?.request(self,
didValidateRequest: self.request,
response: response,
data: self.data,
withResult: result)
}
$validators.write { $0.append(validator) }
return self
}
c、開始執行提供的網絡請求
func perform(_ request: Request)
{
rootQueue.async
{
// 如果網絡請求取消掉了則直接返回
guard !request.isCancelled else { return }
// 將該請求添加到當前活動的網絡請求集合
self.activeRequests.insert(request)
// 在指定隊列異步請求網絡
self.requestQueue.async
{
// 判斷request類型來執行具體請求任務
switch request
{
// 由于子類型關系,UploadRequest必須位于DataRequest之前
case let r as UploadRequest: self.performUploadRequest(r)
case let r as DataRequest: self.performDataRequest(r)
case let r as DownloadRequest: self.performDownloadRequest(r)
case let r as DataStreamRequest: self.performDataStreamRequest(r)
default: fatalError("Attempted to perform unsupported Request subclass: \(type(of: request))")
}
}
}
}
二、響應源碼
1、請求響應 Response
public func responseString(completionHandler: @escaping (AFDataResponse<String>) -> Void)
public func responseDecodable<T: Decodable>(completionHandler: @escaping (AFDownloadResponse<T>) -> Void)
public typealias AFDataResponse<Success> = DataResponse<Success, AFError>
public typealias AFDownloadResponse<Success> = DownloadResponse<Success, AFError>
共有4中不同的Request
類型,DataStreamRequest
我們先不提,對于UploadRequest
來說,服務器響應的數據比較簡單,就響應一個結果就行,因此不需要對它的Response
專門進行封裝。因此,Alamofire
設計了2種與Request
類型相對應的Response
類型,他們分別是:DataResponse
和DownloadResponse
。
a、鏈式訪問原理:response函數的返回值都是Request
在下邊的代碼中,雖然兩個閉包里的response
名字都一樣,但并不是同一類型。
AF.request("https://httpbin.org/get")
.responseString
{ response in
print("Response String: \(String(describing: response.value))")
}
.responseJSON
{ response in
print("Response JSON: \(String(describing: response.value))")
}
輸出結果為:
Response String: Optional("{\n \"args\": {}, \n \"headers\": {\n \"Accept\": \"*/*\", \n \"Accept-Encoding\": \"br;q=1.0, gzip;q=0.9, deflate;q=0.8\", \n \"Accept-Language\": \"en;q=1.0\", \n \"Host\": \"httpbin.org\", \n \"User-Agent\": \"AlamofireSourceCodeAnalysis/1.0 (com.xiejiapei.framework.AlamofireSourceCodeAnalysis; build:1; iOS 14.4.0) Alamofire/5.4.1\", \n \"X-Amzn-Trace-Id\": \"Root=1-60129046-79f8d1b13ef72f727faa8e36\"\n }, \n \"origin\": \"222.76.251.163\", \n \"url\": \"https://httpbin.org/get\"\n}\n")
Response JSON: Optional({
args = {
};
headers = {
Accept = "*/*";
"Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
"Accept-Language" = "en;q=1.0";
Host = "httpbin.org";
"User-Agent" = "AlamofireSourceCodeAnalysis/1.0 (com.xiejiapei.framework.AlamofireSourceCodeAnalysis; build:1; iOS 14.4.0) Alamofire/5.4.1";
"X-Amzn-Trace-Id" = "Root=1-60129046-79f8d1b13ef72f727faa8e36";
};
origin = "222.76.251.163";
url = "https://httpbin.org/get";
})
能實現鏈式訪問的原理就是每個response
函數的返回值都是Self
即Request
。
extension DataRequest
{
public func responseString(...) -> Self
}
extension DownloadRequest
{
public func responseDecodable<T: Decodable>(...) -> Self
}
b、DataResponse:用于存儲與DataRequest或UploadRequest的序列化響應關聯的所有值的類型
? 定義需要保存的數據屬性
- 在swift中,如果只是為了保存數據,那么應該把這個類設計成
struct
。struct
是值傳遞,因此對數據的操作更安全
public struct DataResponse<Success, Failure: Error>
{
// 表示該響應來源于哪個請求
public let request: URLRequest?
// 服務器返回的響應
public let response: HTTPURLResponse?
// 響應數據
public let data: Data?
// 包含了請求和響應的統計信息
public let metrics: URLSessionTaskMetrics?
// 序列化響應所用的時間
public let serializationDuration: TimeInterval
// 響應序列化的結果
public let result: Result<Success, Failure>
// 如果結果成功,則返回結果的關聯值,否則返回nil
public var value: Success? { result.success }
// 在請求中可能發生的錯誤
public var error: Failure? { result.failure }
}
? 設計一個符合要求的構造函數
public init(request: URLRequest?,
response: HTTPURLResponse?,
data: Data?,
metrics: URLSessionTaskMetrics?,
serializationDuration: TimeInterval,
result: Result<Success, Failure>)
{
self.request = request
self.response = response
self.data = data
self.metrics = metrics
self.serializationDuration = serializationDuration
self.result = result
}
c、DownloadResponse
// 用于存儲與下載請求的序列化響應相關聯的所有數據
public struct DownloadResponse<Success, Failure: Error>
{
// 表示該響應來源于哪個請求
public let request: URLRequest?
// 服務器返回的響應
public let response: HTTPURLResponse?
// 從服務器返回的數據移動后的最終位置的URL
public let fileURL: URL?
// 表示可恢復的數據,對于下載任務,如果因為某種原因下載中斷或失敗了,可以使用該數據恢復之前的下載
public let resumeData: Data?
// 包含了請求和響應的統計信息
public let metrics: URLSessionTaskMetrics?
// 序列化響應所用的時間
public let serializationDuration: TimeInterval
// 響應序列化的結果
public let result: Result<Success, Failure>
// 如果結果成功,則返回結果的關聯值,否則返回nil
public var value: Success? { result.success }
// 在請求中可能發生的錯誤
public var error: Failure? { result.failure }
}
2、序列化響應 ResponseSerialization
a、序列化的設計思路
? 最基本的請求
我們先從最簡單的事情著手。如果我發起了一個請求,我肯定希望知道請求的結果,那么就會有下邊這樣的偽代碼。偽代碼中的response
函數是請求的回調函數,ResponseObj
是對服務器返回的數據的一個抽象。
dataRequest().response{ ResponseObj in }
downloadRequest().response{ ResponseObj in }
? 回調函數增加對多線程的支持
默認情況下我們可能希望回調函數會在主線程調用,但是對于某些特定的功能,還是應該增加對多線程的支持,因此我們把上邊的代碼做一下擴展,給response
函數增加一個參數,這個參數用來決定回調函數會在哪個線程被調用。這里的回調函數會給我們一個未序列化的結果,此時序列化用時serializationDuration
= 0。在Alamofire
中,DataRequest
對應的結果是DataResponse
,DownloadRequest
對應的結果是DownloadResponse
,他們都是struct
類型,是純正的存儲設計類型。我們之所以把data
和download
的請求每次都分開來設計,原因是因為這兩個不同的請求得到的響應不一樣。download
可以從一個URL
中獲取數據,而data
不行。
dataRequest().response(queue 回調函數)
downloadRequest().response(queue 回調函數)
? 把上邊的偽代碼還原成Alamfire中的函數:使用未序列化的結果
擴展 DataRequest
,添加一個在請求結束后會調用獲取返回結果的函數。
extension DataRequest
{
@discardableResult
public func response(queue: DispatchQueue = .main, completionHandler: @escaping (AFDataResponse<Data?>) -> Void) -> Self
{
appendResponseSerializer
{
let result = AFResult<Data?>(value: self.data, error: self.error)
self.underlyingQueue.async
{
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
}
return self
}
}
擴展 DownloadRequest
,添加一個在請求結束后會調用獲取返回結果的函數。
extension DownloadRequest
{
@discardableResult
public func response(queue: DispatchQueue = .main,
completionHandler: @escaping (AFDownloadResponse<URL?>) -> Void)
-> Self
{
appendResponseSerializer
{
let result = AFResult<URL?>(value: self.fileURL, error: self.error)
self.underlyingQueue.async
{
let response = DownloadResponse(request: self.request,
response: self.response,
fileURL: self.fileURL,
resumeData: self.resumeData,
metrics: self.metrics,
serializationDuration: 0,
result: result)
self.eventMonitor?.request(self, didParseResponse: response)
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
}
}
return self
}
}
? 序列化者遵守的序列化協議
只要在上邊的response
方法中添加一個參數就行,這個參數的任務就是完成數據的序列化。此時我們說的系列化就是指可以把響應數據生成Result
的功能。偽代碼如下:
dataRequest().response(queue 序列化者 回調函數)
downloadRequest().response(queue 序列化者 回調函數)
序列化者的任務是把數據轉換成Result
。因此我們可以把這個序列化者設計成一個類或者結構體,里邊提供一個轉換的方法就行了,這也是再正常不過的思想。但是swift跟oc不一樣,在swift中我們應該轉變思維。我們不應該把序列化者用一個固定的對象封死。這個時候就是協議大顯身手的時刻了。既然序列化者需要一個函數,那么我們就設計一個包含該函數的協議。SerializedObject
定義了要序列化后的對象類型,這么寫的原因也是因為后邊會序列成Data
,JOSN
,String
等不同類型的需求。
// 定義數據序列化協議
public protocol DataResponseSerializerProtocol
{
// 序列化后的結果類型
associatedtype SerializedObject
// 序列化方法
func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> SerializedObject
}
// 定義下載序列化協議
public protocol DownloadResponseSerializerProtocol
{
associatedtype SerializedObject
func serializeDownload(request: URLRequest?, response: HTTPURLResponse?, fileURL: URL?, error: Error?) throws -> SerializedObject
}
? 使用指定序列化器序列化的結果
序列化用時serializationDuration = end - start
public func response<Serializer: DataResponseSerializerProtocol>(queue: DispatchQueue = .main,
responseSerializer: Serializer,
completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void)
-> Self
{
appendResponseSerializer
{
// 應在序列化隊列上的開始工作
let start = ProcessInfo.processInfo.systemUptime
// 生成序列化結果
let result: AFResult<Serializer.SerializedObject> = Result
{
// 調用序列化器來序列化結果
try responseSerializer.serialize(request: self.request,
response: self.response,
data: self.data,
error: self.error)
}
.mapError
{ error in
error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
}
// 應在序列化隊列上的結束工作
let end = ProcessInfo.processInfo.systemUptime
...
}
return self
}
生成響應,并調用完成回調
self.underlyingQueue.async
{
// 生成響應
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
// 埋點日志
self.eventMonitor?.request(self, didParseResponse: response)
// 序列化過程出錯
guard let serializerError = result.failure, let delegate = self.delegate else
{
// 完成回調
self.responseSerializerDidComplete { queue.async { completionHandler(response) } }
return
}
...
}
異步詢問委托是否將重試Request
delegate.retryResult(for: self, dueTo: serializerError)
{ retryResult in
var didComplete: (() -> Void)?
defer
{
if let didComplete = didComplete
{
// 完成回調
self.responseSerializerDidComplete { queue.async { didComplete() } }
}
}
// 重試結果
switch retryResult
{
case .doNotRetry:
// 完成回調
didComplete = { completionHandler(response) }
case let .doNotRetryWithError(retryError):
let result: AFResult<Serializer.SerializedObject> = .failure(retryError.asAFError(orFailWith: "Received retryError was not already AFError"))
let response = DataResponse(request: self.request,
response: self.response,
data: self.data,
metrics: self.metrics,
serializationDuration: end - start,
result: result)
// 完成回調
didComplete = { completionHandler(response) }
case .retry, .retryWithDelay:
delegate.retryRequest(self, withDelay: retryResult.delay)
}
}
b、responseString
如果要把data
序列成string
,只需要創建一個data
序列者就好了,但是這樣的設計用起來很麻煩,因為還要書寫序列成Result
的函數,這些函數往往都是一樣的,那么可以把這些函數封裝起來,通過封裝后的函數來獲取序列化后的response
。
extension DataRequest
{
@discardableResult
public func responseString(...completionHandler: @escaping (AFDataResponse<String>) -> Void) -> Self
{
response(queue: queue,
responseSerializer: StringResponseSerializer(dataPreprocessor: dataPreprocessor,
encoding: encoding,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}
c、responseData
extension DataRequest
{
public func responseData(completionHandler: @escaping (AFDataResponse<Data>) -> Void) -> Self
{
response(queue: queue,
responseSerializer: DataResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods),
completionHandler: completionHandler)
}
}
d、responseJSON
extension DownloadRequest
{
public func responseJSON(...completionHandler: @escaping (AFDownloadResponse<Any>) -> Void) -> Self
{
response(queue: queue,
responseSerializer: JSONResponseSerializer(dataPreprocessor: dataPreprocessor,
emptyResponseCodes: emptyResponseCodes,
emptyRequestMethods: emptyRequestMethods,
options: options),
completionHandler: completionHandler)
}
}
e、序列化的核心方法:以字符串為例
// 將data序列化為字符串
public final class StringResponseSerializer: ResponseSerializer
{
public func serialize(request: URLRequest?, response: HTTPURLResponse?, data: Data?, error: Error?) throws -> String
{
data = try dataPreprocessor.preprocess(data)
var convertedEncoding = encoding
if let encodingName = response?.textEncodingName, convertedEncoding == nil
{
convertedEncoding = String.Encoding(ianaCharsetName: encodingName)
}
let actualEncoding = convertedEncoding ?? .isoLatin1
return string
}
}
三、底層源碼
1、錯誤處理 AFError
Alamofire
的錯誤封裝很經典,是使用swift中enum
的一個典型案例。只要結果可能是有限的集合的情況下,我們就盡量考慮使用枚舉。 其實枚舉本身還是數據的一種載體,swift中,枚舉有著很豐富的使用方法。
a、ParameterEncodingFailureReason:參數編碼錯誤
? 嵌套枚舉
ParameterEncodingFailureReason
本身是一個enum
,同時,它又被包含在AFError
之中,這說明枚舉之中可以有另一個枚舉。
public enum AFError: Error
{
public enum ParameterEncodingFailureReason
{
// 給定的urlRequest.url為nil的情況拋出該錯誤
case missingURL
// 當選擇把參數編碼成JSON格式的情況下,參數JSON化出錯時拋出的錯誤
case jsonEncodingFailed(error: Error)
// 自定義編碼出錯時拋出的錯誤
case customEncodingFailed(error: Error)
}
}
那么像這種情況我們怎么使用呢?枚舉的訪問是一級一級進行的。
let parameterErrorReason = AFError.ParameterEncodingFailureReason.missingURL
print(parameterErrorReason)
輸出結果為:
missingURL
? 關聯值
public enum ParameterEncodingFailureReason
{
// 當選擇把參數編碼成JSON格式的情況下,參數JSON化出錯時拋出的錯誤
case jsonEncodingFailed(error: Error)
}
jsonEncodingFailed(error: Error)
并不是函數,只是枚舉的一個普通的子選項,(error: Error)
是它的一個關聯值。對于任何一個子選項,我們都可以關聯任何值,把這些值與子選項進行綁定的意義在于可以在需要的時候進行調用。
b、MultipartEncodingFailureReason:多部分編碼錯誤
多部分編碼錯誤一般發生在上傳或下載請求中對數據的處理過程中。
public enum MultipartEncodingFailureReason
{
// 上傳數據時,可以通過fileURL的方式,讀取本地文件數據,如果fileURL不可用,就會拋出這個錯誤
case bodyPartURLInvalid(url: URL)
// 如果使用fileURL的lastPathComponent或者pathExtension獲取filename為空則拋出該錯誤
case bodyPartFilenameInvalid(in: URL)
// 如果通過fileURL不能訪問數據,那就是不可達的
case bodyPartFileNotReachable(at: URL)
// 嘗試檢測fileURL時發現不是可達的則拋出該錯誤
case bodyPartFileNotReachableWithError(atURL: URL, error: Error)
// 當fileURL是一個文件夾時拋出該錯誤
case bodyPartFileIsDirectory(at: URL)
// 當使用系統Api獲取fileURL指定文件的size出錯時拋出該錯誤
case bodyPartFileSizeNotAvailable(at: URL)
// 查詢fileURL指定文件的size出錯時拋出該錯誤
case bodyPartFileSizeQueryFailedWithError(forURL: URL, error: Error)
// 通過fileURL創建inputStream出錯時拋出該錯誤
case bodyPartInputStreamCreationFailed(for: URL)
// 當嘗試把編碼后的數據寫入到硬盤時,創建outputStream出錯時拋出該錯誤
case outputStreamCreationFailed(for: URL)
// 數據不能被寫入,因為指定的fileURL已經存在
case outputStreamFileAlreadyExists(at: URL)
// fileURL不是一個file URL
case outputStreamURLInvalid(url: URL)
// 數據流寫入錯誤
case outputStreamWriteFailed(error: Error)
// 數據流讀入錯誤
case inputStreamReadFailed(error: Error)
}
c、ResponseValidationFailureReason:響應驗證失錯誤
Alamofire
不管請求是否成功,都會返回response
。Alamofire
提供了驗證ContentType
和StatusCode
的功能。
public enum ResponseValidationFailureReason
{
// 保存數據的URL不存在,這種情況一般出現在下載任務中,指的是下載代理中的fileURL缺失
case dataFileNil
// 保存數據的URL無法讀取數據,同上
case dataFileReadFailed(at: URL)
// 服務器返回的response不包含ContentType且提供的acceptableContentTypes不包含通配符(通配符表示可以接受任何類型)
case missingContentType(acceptableContentTypes: [String])
// ContentTypes不匹配
case unacceptableContentType(acceptableContentTypes: [String], responseContentType: String)
// StatusCode不匹配
case unacceptableStatusCode(code: Int)
// 自定義驗證失敗
case customValidationFailed(error: Error)
}
d、ResponseSerializationFailureReason:響應序列化錯誤
Alamofire
支持把服務器的response
序列成幾種數據格式。
response //直接返回HTTPResponse,未序列化
responseData //序列化為Data
responseJSON //序列化為Json
responseString //序列化為字符串
在序列化的過程中,很可能會發生下邊的錯誤
public enum ResponseSerializationFailureReason
{
// 服務器返回的response沒有數據或者數據的長度是0
case inputDataNilOrZeroLength
// 指向數據的URL不存在
case inputFileNil
// 指向數據的URL無法讀取數據
case inputFileReadFailed(at: URL)
// 當使用指定的String.Encoding序列化數據為字符串出錯時拋出的錯誤
case stringSerializationFailed(encoding: String.Encoding)
// JSON序列化錯誤
case jsonSerializationFailed(error: Error)
// 數據解碼失敗
case decodingFailed(error: Error)
// 自定義序列化失敗
case customSerializationFailed(error: Error)
// 無效響應
case invalidEmptyResponse(type: String)
}
e、把上邊4個獨立的枚舉進行串聯
上邊介紹的4個枚舉是被包含在AFError
中彼此獨立的枚舉,使用的時候需要通過AFError.ParameterEncodingFailureReason
這種方式進行操作。如何把上邊4個獨立的枚舉進行串聯呢?由于4個獨立的枚舉分別代表這個網絡框架的4大錯誤模塊,那么我們只需要給AFError
設計4個子選項關聯上這4個獨立枚舉的值就行了。這個設計真的很巧妙,試想,如果把所有的錯誤都放到AFError
中,就顯得非常冗余。
// 無效的URL
case invalidURL(url: URLConvertible)
// 多部分編碼失敗
case multipartEncodingFailed(reason: MultipartEncodingFailureReason)
// 請求參數編碼失敗
case parameterEncodingFailed(reason: ParameterEncodingFailureReason)
// 響應序列化失敗
case responseSerializationFailed(reason: ResponseSerializationFailureReason)
f、判斷當前的錯誤是不是某個指定的錯誤類型
假如我們只想知道某個error
是不是參數編碼錯誤,應該怎么辦?AFError
為此提供了多個布爾類型的屬性,專門用來獲取當前的錯誤是不是某個指定的錯誤類型。
extension AFError
{
// 無效的URL
public var isInvalidURLError: Bool
{
if case .invalidURL = self { return true }
return false
}
// 請求參數編碼錯誤
public var isParameterEncodingError: Bool
{
if case .parameterEncodingFailed = self { return true }
return false
}
// 多部分編碼錯誤
public var isMultipartEncodingError: Bool
{
if case .multipartEncodingFailed = self { return true }
return false
}
// 響應驗證錯誤
public var isResponseValidationError: Bool
{
if case .responseValidationFailed = self { return true }
return false
}
// 響應序列化錯誤
public var isResponseSerializationError: Bool
{
if case .responseSerializationFailed = self { return true }
return false
}
...
}
g、便利屬性
extension AFError
{
// 獲取某個屬性,這個屬性實現了URLConvertible協議,在AFError中只有case invalidURL(url: URLConvertible)這個選項符合要求
public var urlConvertible: URLConvertible?
{
guard case let .invalidURL(url) = self else { return nil }
return url
}
// 獲取AFError中的URL
public var url: URL?
{
guard case let .multipartEncodingFailed(reason) = self else { return nil }
return reason.url
}
// 可接受的ContentType
public var acceptableContentTypes: [String]?
{
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.acceptableContentTypes
}
// 響應碼
public var responseCode: Int?
{
guard case let .responseValidationFailed(reason) = self else { return nil }
return reason.responseCode
}
// 錯誤的字符串編碼
public var failedStringEncoding: String.Encoding?
{
guard case let .responseSerializationFailed(reason) = self else { return nil }
return reason.failedStringEncoding
}
}
h、錯誤描述
在開發中,如果程序遇到錯誤,我們往往會給用戶展示更加直觀的信息,這就要求我們把錯誤信息轉換成易于理解的內容。因此我們只要實現LocalizedError
協議就好了。
extension AFError: LocalizedError
{
public var errorDescription: String?
{
switch self
{
case let .invalidURL(url):
return "URL is not valid: \(url)"
case let .parameterEncodingFailed(reason):
return reason.localizedDescription
case let .multipartEncodingFailed(reason):
return reason.localizedDescription
case let .responseValidationFailed(reason):
return reason.localizedDescription
case let .responseSerializationFailed(reason):
return reason.localizedDescription
}
}
}
2、會話代理 SessionDelegate
a、概念
網絡請求過程
一條最普通的網絡請求,究竟是怎樣的一個過程?首先我們根據一個URL
和若干個參數生成Request
,然后根據Request
生成一個會話Session
,再根據這個Session
生成Task
,我們開啟Task
就完成了這個請求。當然這一過程之中還會包含重定向,數據上傳,挑戰,證書等等一系列的配置信息。
代理
我們再聊聊代理的問題,不管是在網絡請求中,還是再其他的地方,代理都類似于一個管理員的身份。這在業務架構中是一個很好的主意。假如我把代理想象成一個人,那么這個人的工作是什么呢?一是通過代理提供我所需要的數據。二是我知道這個業務你比較精通,當有和你相關的事情的時候,我會通知你來解決。
b、初始化
? 初始化Session時傳入SessionDelegate
public init(session: URLSession,
delegate: SessionDelegate,
eventMonitors: [EventMonitor] = [])
{
self.session = session
self.delegate = delegate
eventMonitor = CompositeEventMonitor(monitors: defaultEventMonitors + eventMonitors)
delegate.eventMonitor = eventMonitor //事件監聽器
delegate.stateProvider = self //狀態監聽器
}
? 負責處理所有與內部 session 關聯的代理回調
open class SessionDelegate: NSObject
{
// 狀態監聽器
weak var stateProvider: SessionStateProvider?
// 事件監聽器
var eventMonitor: EventMonitor?
}
c、URLSessionDataDelegate
extension SessionDelegate: URLSessionDataDelegate
{
.....
}
? 接受到數據
open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
{
eventMonitor?.urlSession(session, dataTask: dataTask, didReceive: data)
if let request = request(for: dataTask, as: DataRequest.self)
{
request.didReceive(data: data)
}
}
? 是否需要緩存響應
該函數用于處理是否需要緩存響應,Alamofire
默認是緩存這些response
的。
open func urlSession(_ session: URLSession,
dataTask: URLSessionDataTask,
willCacheResponse proposedResponse: CachedURLResponse,
completionHandler: @escaping (CachedURLResponse?) -> Void)
{
eventMonitor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse)
if let handler = stateProvider?.request(for: dataTask)?.cachedResponseHandler ?? stateProvider?.cachedResponseHandler
{
handler.dataTask(dataTask, willCacheResponse: proposedResponse, completion: completionHandler)
}
else
{
completionHandler(proposedResponse)
}
}
d、URLSessionDownloadDelegate
? 數據下載完成
當數據下載完成后,該函數被觸發。系統會把數據下載到一個臨時的locationURL
的地方,我們就是通過這個URL
拿到數據的。上邊函數內的代碼主要是把數據復制到目標路徑中。
open func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
{
eventMonitor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
let (destination, options): (URL, DownloadRequest.Options)
if let response = request.response
{
(destination, options) = request.destination(location, response)
}
eventMonitor?.request(request, didCreateDestinationURL: destination)
if options.contains(.removePreviousFile), fileManager.fileExists(atPath: destination.path)
{
try fileManager.removeItem(at: destination)
}
if options.contains(.createIntermediateDirectories)
{
let directory = destination.deletingLastPathComponent()
try fileManager.createDirectory(at: directory, withIntermediateDirectories: true)
}
try fileManager.moveItem(at: location, to: destination)
request.didFinishDownloading(using: downloadTask, with: .success(destination))
}
? 提供下載進度
該代理方法在數據下載過程中被觸發,主要的作用就是提供下載進度。
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didWriteData bytesWritten: Int64,
totalBytesWritten: Int64,
totalBytesExpectedToWrite: Int64)
{
downloadRequest.updateDownloadProgress(bytesWritten: bytesWritten,
totalBytesExpectedToWrite: totalBytesExpectedToWrite)
}
? 斷點續傳
如果一個下載的task
是可以恢復的,那么當下載被取消或者失敗后,系統會返回一個resumeData
對象,這個對象包含了一些跟這個下載task
相關的一些信息,有了它就能重新創建下載task
,創建方法有兩個:downloadTask(withResumeData:)
和downloadTask(withResumeData:completionHandler:)
,當task
開始后,上邊的代理方法就會被觸發。
open func urlSession(_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didResumeAtOffset fileOffset: Int64,
expectedTotalBytes: Int64)
{
downloadRequest.updateDownloadProgress(bytesWritten: fileOffset,
totalBytesExpectedToWrite: expectedTotalBytes)
}
e、URLSessionTaskDelegate
提供上傳的進度
open func urlSession(_ session: URLSession,
task: URLSessionTask,
didSendBodyData bytesSent: Int64,
totalBytesSent: Int64,
totalBytesExpectedToSend: Int64)
{
stateProvider?.request(for: task)?.updateUploadProgress(totalBytesSent: totalBytesSent,
totalBytesExpectedToSend: totalBytesExpectedToSend)
}
四、其他源碼
1、通知處理 Notifications
通知作為傳遞事件和數據的載體,在使用中是不受限制的,但是如果忘記移除某個通知的監聽,就會造成很多潛在的問題,這些問題在測試中是很難被發現的。有的團隊為了管理通知,創建了一個類似于NotificationManager
的類,所有通知的添加移除都通過這個類進行管理,通過打印通知數組就能很清楚的看到添加了哪些通知,以及這些通知被綁定在了哪些對象之上,這是一個很好地思路。
a、通知的名稱
swift中發通知的函數原型如下,除了name
之外,其他參數跟OC沒什么區別。name
的主要作用就是作為通知的唯一標識,但由于在OC中這個name
是一個最普通的字符串,就導致了開發中亂用的問題。很多人為了管理這些通知字符串想出了很多辦法,比如把這些字符串放到一個或幾個文件中,或者寫一個類根據不同功能提供不同的字符串(這個是比較推薦的寫法,也和本篇中講解的用法很像)。
open func post(name aName: NSNotification.Name, object anObject: Any?, userInfo aUserInfo: [AnyHashable : Any]? = nil)
b、創建命名空間
通知名
- 用于指示通知名,在這里作用類似于一個命名空間
- 把跟
request
相關的通知都綁定在這個request
上
extension Request
{
// 在 Request 繼續運行的時候會發送通知,通知中含有此 Request
public static let didResumeNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResume")
// 在 Request 暫停的時候會發送通知,通知中含有此 Request
public static let didSuspendNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspend")
// 在 Request 取消的時候會發送通知,通知中含有此 Request
public static let didCancelNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancel")
// 在 Request 完成的時候會發送通知,通知中含有此 Request
public static let didFinishNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didFinish")
// 在 URLSessionTask 繼續運行的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public static let didResumeTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didResumeTask")
// 在 URLSessionTask 暫停的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public static let didSuspendTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didSuspendTask")
// 在 URLSessionTask 取消的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public static let didCancelTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCancelTask")
// 在 URLSessionTask 完成的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public static let didCompleteTaskNotification = Notification.Name(rawValue: "org.alamofire.notification.name.request.didCompleteTask")
}
用戶信息字典鍵值
- 這里也是起到一個命名空間的作用,用于標記指定鍵值
extension String
{
// 表示與通知關聯的Request的用戶信息字典鍵值
fileprivate static let requestKey = "org.alamofire.notification.key.request"
}
c、使用命名空間
? 通過字典鍵值從通知的用戶信息中獲取到關聯的Request
extension Notification
{
// 通過字典鍵值從通知的用戶信息中獲取到關聯的Request
public var request: Request?
{
userInfo?[String.requestKey] as? Request
}
}
? 傳入通知名稱和當前Request初始化通知
extension Notification
{
// 傳入通知名稱和當前Request初始化通知
init(name: Notification.Name, request: Request)
{
self.init(name: name, object: nil, userInfo: [String.requestKey: request])
}
}
? 傳入通知名稱和當前Request發送通知
extension NotificationCenter
{
// 傳入通知名稱和當前Request發送通知
func postNotification(named name: Notification.Name, with request: Request)
{
let notification = Notification(name: name, request: request)
post(notification)
}
}
? EventMonitor提供Alamofire通知發送的時機
public final class AlamofireNotifications: EventMonitor
{
// 當Request收到resume調用時調用的事件
public func requestDidResume(_ request: Request)
{
NotificationCenter.default.postNotification(named: Request.didResumeNotification, with: request)
}
// 當Request收到suspend調用時調用的事件
public func requestDidSuspend(_ request: Request) {
NotificationCenter.default.postNotification(named: Request.didSuspendNotification, with: request)
}
// 當Request收到cancel調用時調用的事件
public func requestDidCancel(_ request: Request)
{
NotificationCenter.default.postNotification(named: Request.didCancelNotification, with: request)
}
// 當Request收到finish調用時調用的事件
public func requestDidFinish(_ request: Request)
{
NotificationCenter.default.postNotification(named: Request.didFinishNotification, with: request)
}
}
public final class AlamofireNotifications: EventMonitor
{
// 在 URLSessionTask 繼續運行的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public func request(_ request: Request, didResumeTask task: URLSessionTask)
{
NotificationCenter.default.postNotification(named: Request.didResumeTaskNotification, with: request)
}
// 在 URLSessionTask 暫停的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public func request(_ request: Request, didSuspendTask task: URLSessionTask)
{
NotificationCenter.default.postNotification(named: Request.didSuspendTaskNotification, with: request)
}
// 在 URLSessionTask 取消的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public func request(_ request: Request, didCancelTask task: URLSessionTask)
{
NotificationCenter.default.postNotification(named: Request.didCancelTaskNotification, with: request)
}
// 在 URLSessionTask 完成的時候會發送通知,通知中含有此 URLSessionTask 關聯的 Request
public func request(_ request: Request, didCompleteTask task: URLSessionTask, with error: AFError?)
{
NotificationCenter.default.postNotification(named: Request.didCompleteTaskNotification, with: request)
}
}
2、網絡監控 NetworkReachabilityManager
a、用途
使用場景
- 聊天列表需要實時監控當前的網絡是不是可達的,如果不可達則出現聯網失敗的提示
- 網絡視頻播放需要判斷當前的網絡狀態,如果不是WiFi應該給出消耗流量來播放的提示
- 可以把請求放進緩存,當監聽到網絡連接成功后發送。舉個例子,每次進app都要把位置信息發給服務器,如果發送失敗后,發現是網絡不可達造成的失敗,那么可以把這個請求放入到一個隊列中,在網絡可達的時候,開啟隊列任務
- 當網絡狀態變化時,實時的給用戶提示信息
獲取網絡狀態
- 所以在開發中有時候我們需要獲取這些信息:手機是否聯網,當前網絡是WiFi還是蜂窩
- 但是極其不建議在發請求前,先檢測當前的網絡是不是可達,因為手機的網絡狀態是經常變化的
b、SCNetworkReachabilityFlags
功能
- 能夠判斷某個指定的網絡節點名稱或者地址是不是可達的
- 也能判斷該節點或地址是不是需要先建立連接
- 也可以判斷是不是需要用戶手動去建立連接
屬性
// 表明當前指定的節點或地址是可達的
public static var reachable: SCNetworkReachabilityFlags { get }
// 要想和指定的節點或地址通信,需要先建立連接,比如在很多地方需要輸入手機號獲取驗證碼后才能聯網
public static var connectionRequired: SCNetworkReachabilityFlags { get }
// 需要用戶手動提供一些數據,比如密碼或者token
public static var interventionRequired: SCNetworkReachabilityFlags { get }
// 表明是不是本地地址
public static var isLocalAddress: SCNetworkReachabilityFlags { get }
// 表明是不是通過蜂窩網絡連接
public static var isWWAN: SCNetworkReachabilityFlags { get }
// 表面是不是自動連接
public static var connectionAutomatic: SCNetworkReachabilityFlags { get }
c、枚舉與屬性
通過枚舉獲取當前的網絡連接類型
// 對于手機而言,我們需要的連接類型就兩種
public enum ConnectionType
{
// 一種是WiFi網絡
case ethernetOrWiFi
// 一種是蜂窩網絡
case cellular
}
網絡連接狀態
// 網絡連接狀態明顯要比網絡類型范圍更大,因此又增加了兩個選項
public enum NetworkReachabilityStatus
{
// 表示當前的網絡是未知的
case unknown
// 表示當前的網路不可達
case notReachable
// 在關聯的ConnectionType上可以訪問網絡
case reachable(ConnectionType)
}
監聽器類型
- 監聽器類型實質是一個閉包
- 當網絡狀態改變時,閉包會被調用
- 閉包只有一個參數,為網絡可達性狀態
public typealias Listener = (NetworkReachabilityStatus) -> Void
屬性
// 當前網絡是可達的,要么是蜂窩網絡,要么是WiFi連接
open var isReachable: Bool { isReachableOnCellular || isReachableOnEthernetOrWiFi }
// 表明當前網絡是通過蜂窩網絡連接
open var isReachableOnCellular: Bool { status == .reachable(.cellular) }
// 表明當前網絡是通過WiFi連接
open var isReachableOnEthernetOrWiFi: Bool { status == .reachable(.ethernetOrWiFi) }
// 返回當前的網絡狀態
open var status: NetworkReachabilityStatus
{
flags.map(NetworkReachabilityStatus.init) ?? .unknown
}
// 監聽器中代碼執行所在的隊列
public let reachabilityQueue = DispatchQueue(label: "org.alamofire.reachabilityQueue")
// 網絡狀態就是根據flags判斷出來的
open var flags: SCNetworkReachabilityFlags?
{
// 有了它才能獲取flags
var flags = SCNetworkReachabilityFlags()
return (SCNetworkReachabilityGetFlags(reachability, &flags)) ? flags : nil
}
// 可達性
private let reachability: SCNetworkReachability
d、初始化方法
便利構造函數:通過傳入一個主機地址驗證可達性
public convenience init?(host: String)
{
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else { return nil }
self.init(reachability: reachability)
}
直接調用init方法會默認的設置為指向0.0.0.0
- 可達性將0.0.0.0視為一個特殊的地址,因為它會監聽設備的路由信息,在 ipv4 和 ipv6 下都可以使用
public convenience init?()
{
var zero = sockaddr()
zero.sa_len = UInt8(MemoryLayout<sockaddr>.size)
zero.sa_family = sa_family_t(AF_INET)
guard let reachability = SCNetworkReachabilityCreateWithAddress(nil, &zero) else { return nil }
self.init(reachability: reachability)
}
通過指定 SCNetworkReachability 來初始化
private init(reachability: SCNetworkReachability)
{
self.reachability = reachability
}
在析構函數中會停止監聽網絡變化
deinit
{
stopListening()
}
e、監聽器
開始監控網絡狀態
@discardableResult //表明可以忽略返回值
open func startListening(onQueue queue: DispatchQueue = .main,
onUpdatePerforming listener: @escaping Listener) -> Bool
{
stopListening()
// 創建一個監聽器
$mutableState.write
{ state in
state.listenerQueue = queue
state.listener = listener
}
// 設置上下文
var context = SCNetworkReachabilityContext(version: 0,
info: Unmanaged.passUnretained(self).toOpaque(),
retain: nil,
release: nil,
copyDescription: nil)
// 創建回調函數
let callback: SCNetworkReachabilityCallBack =
{ _, flags, info in
guard let info = info else { return }
// 獲取 NetworkReachabilityManager 的實例對象
let instance = Unmanaged<NetworkReachabilityManager>.fromOpaque(info).takeUnretainedValue()
// 通知監聽者,也就是觸發回調函數
instance.notifyListener(flags)
}
// 注冊隊列
let queueAdded = SCNetworkReachabilitySetDispatchQueue(reachability, reachabilityQueue)
// 注冊回調函數
let callbackAdded = SCNetworkReachabilitySetCallback(reachability, callback, &context)
// 通知網絡狀態發送改變
if let currentFlags = flags
{
reachabilityQueue.async
{
self.notifyListener(currentFlags)
}
}
return callbackAdded && queueAdded
}
停止監控網絡狀態
open func stopListening()
{
// 取消回調
SCNetworkReachabilitySetCallback(reachability, nil, nil)
// 取消隊列
SCNetworkReachabilitySetDispatchQueue(reachability, nil)
// 銷毀監聽器
$mutableState.write
{ state in
state.listener = nil
state.listenerQueue = nil
state.previousStatus = nil
}
}
Demo
Demo在我的Github上,歡迎下載。
SourceCodeAnalysisDemo