本篇是Alamofire中的請求抽象層的講解
前言
在Alamofire中,圍繞著Request,設計了很多額外的特性,這也恰恰表明,Request是所有請求的基礎部分和發起點。這無疑給我們一個Request很復雜的想法。但看了Alamofire中Request.swift
中的代碼,Request被設計的又是如此的簡單,這就是為什么這些頂級框架如此讓人喜愛的原因。
在后續的文章中,我會單獨寫一篇Swift中協議的使用技巧,在Alamofire源碼解讀系列(一)之概述和使用這篇的Alamofire高級用法中,我根據Alamofire官方文檔做了一些補充,其中涉及到了URLConvertible和URLRequestConvertible的高級用法,在本篇中同樣出現了3個協議:
- RequestAdapter 請求適配器,目的是自定義修改請求,一個典型的例子是為每一個請求調價Token請求頭
- RequestRetrier 請求重試器, 目的是控制請求的重試機制,一個典型的例子是當某個特殊的請求失敗后,是否重試。
- TaskConvertible task轉換器,目的是把task裝換成特定的類型,在Alamofire中有4中task:Data/Download/Upload/Stream
有一點需要特別說明的是,在使用Alamofire的高級用法時,需要操作SessionManager這個類。
請求過程
明白Alamofire中一個請求的過程,是非常有必要的。先看下邊的代碼:
Alamofire.request("https://httpbin.org/get")
上邊的代碼是最簡單的一個請求,我們看看Alamofire.request中究竟干了什么?
@discardableResult
public func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest
{
return SessionManager.default.request(
url,
method: method,
parameters: parameters,
encoding: encoding,
headers: headers
)
}
該函數內部調用了SessionManager的request方法,這說明請求的第一個發起點來自SessionManager,Alamofire.swift
該文件是最上層的封裝,緊鄰其下的就是SessionManager.swift
。接下來我們再看看SessionManager.default.request
做了什么?
@discardableResult
open func request(
_ url: URLConvertible,
method: HTTPMethod = .get,
parameters: Parameters? = nil,
encoding: ParameterEncoding = URLEncoding.default,
headers: HTTPHeaders? = nil)
-> DataRequest
{
var originalRequest: URLRequest?
/// 在這里計算出可能出現的額錯誤的類型
/// 1.url 如果不能被轉成URL被拋出一個error
/// 2.originalRequest不能轉換為URLRequest會拋出error
do {
originalRequest = try URLRequest(url: url, method: method, headers: headers)
let encodedURLRequest = try encoding.encode(originalRequest!, with: parameters)
return request(encodedURLRequest)
} catch {
return request(originalRequest, failedWith: error)
}
}
上邊的函數內部創建了一個Request對象,然后把參數編碼進這個Request中,之后又調用了內部的一個request函數,函數的參數就是上邊的Request對象。我們就緒看看這個request函數做了什么?
open func request(_ urlRequest: URLRequestConvertible) -> DataRequest {
var originalRequest: URLRequest?
do {
originalRequest = try urlRequest.asURLRequest()
/// 這里需要注意的是Requestable并不是DataRequest的一個屬性,前邊是沒有加let/var的,所以可以通過DataRequest.Requestable來操作
let originalTask = DataRequest.Requestable(urlRequest: originalRequest!)
let task = try originalTask.task(session: session, adapter: adapter, queue: queue)
let request = DataRequest(session: session, requestTask: .data(originalTask, task))
delegate[task] = request
if startRequestsImmediately { request.resume() }
return request
} catch {
return request(originalRequest, failedWith: error)
}
}
注意,上邊的函數是一個open函數,因此可以使用SessionManager.request來發起請求,不過參數是_ urlRequest: URLRequestConvertible
。
URLRequestConvertible協議的目的是對URLRequest進行自定義的轉換,因此,在獲得轉換后的URLRequest后,需要用URLRequest生成task,這樣才能發起網絡請求,在Alamofire中,但凡是request開頭的函數,默認的都是DataRequest類型,現在有了URLRequest還不夠,還需要檢測她能否生成與之相對應的task。
在上邊的函數中,用到了DataRequest.Requestable,Requestable其實一個結構體,他實現了TaskConvertible協議,因此,它能夠用URLRequest生成與之相對應的task。接下來就初始化DataRequest,然后真正的開始發起請求。
我們總結一下這個過程:
明白了上邊的過程,再回過頭來看Request.swift
也就是本篇的內容就簡單多了,就下邊幾個目的:
- 創建DataRequest/DownloadRequest/UploadRequest/StreamRequest
- 發起請求
Request
有很多二次封裝的網絡框架中,一般都有這么一個Request類,用于發送網絡請求,接受response,關聯服務器返回的數據并且管理task。Alamofire中的Request同樣主要實現上邊的任務。
Request作為DataRequest、DownloadRequest、UploadRequest、StreamRequest的基類,我們一起來看看它有哪些屬性:
/// The delegate for the underlying task.
/// 由于某個屬性是通過另一個屬性來setter和getter的,因此建議加一個鎖
open internal(set) var delegate: TaskDelegate {
get {
taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
return taskDelegate
}
set {
taskDelegateLock.lock() ; defer { taskDelegateLock.unlock() }
taskDelegate = newValue
}
}
/// The underlying task.
open var task: URLSessionTask? { return delegate.task }
/// The session belonging to the underlying task.
open let session: URLSession
/// The request sent or to be sent to the server.
open var request: URLRequest? { return task?.originalRequest }
/// The response received from the server, if any.
open var response: HTTPURLResponse? { return task?.response as? HTTPURLResponse }
/// The number of times the request has been retried.
open internal(set) var retryCount: UInt = 0
let originalTask: TaskConvertible?
var startTime: CFAbsoluteTime?
var endTime: CFAbsoluteTime?
var validations: [() -> Void] = []
private var taskDelegate: TaskDelegate
private var taskDelegateLock = NSLock()
這些屬性沒什么好說的,我們就略過這些內容,Request的初始化方法,有點意思,我們先看看代碼:
init(session: URLSession, requestTask: RequestTask, error: Error? = nil) {
self.session = session
switch requestTask {
case .data(let originalTask, let task):
taskDelegate = DataTaskDelegate(task: task)
self.originalTask = originalTask
case .download(let originalTask, let task):
taskDelegate = DownloadTaskDelegate(task: task)
self.originalTask = originalTask
case .upload(let originalTask, let task):
taskDelegate = UploadTaskDelegate(task: task)
self.originalTask = originalTask
case .stream(let originalTask, let task):
taskDelegate = TaskDelegate(task: task)
self.originalTask = originalTask
}
delegate.error = error
delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
}
要想發起一個請求,有一個task就足夠了,在上邊的方法中傳遞過來的session主要用于CustomStringConvertible
和CustomDebugStringConvertible
這兩個協議的實現方法中獲取特定的數據。
這里有一點小提示,在創建自定義的類的時候,實現上邊這兩個協議,通過打印,能夠進行快速的調試。
上邊方法中第二個參數是requestTask,它是一個枚舉類型,我們看一下:
enum RequestTask {
case data(TaskConvertible?, URLSessionTask?)
case download(TaskConvertible?, URLSessionTask?)
case upload(TaskConvertible?, URLSessionTask?)
case stream(TaskConvertible?, URLSessionTask?)
}
在swift中枚舉不僅僅用來區分不同的選項,更強大的是為每個選項綁定的數據。大家仔細想一下,在初始化Request的時候,只需要傳遞requestTask這個枚舉值,我們就得到了兩個重要的數據:Request的類型和相對應的task。這一變成手法的運用,大大提高了代碼的質量。
RequestTask枚舉中和選項綁定的數據有兩個,TaskConvertible表示原始的對象,該對象實現了TaskConvertible協議,能夠轉換成task。URLSessionTask是原始對象轉換后的task。因此衍生出一種高級使用方法的可能性,可以自定義一個類,實現TaskConvertible協議,就能夠操縱task的轉換過程,很靈活。
delegate.queue.addOperation { self.endTime = CFAbsoluteTimeGetCurrent() }
上邊的這一行代碼。給代理的queue添加了一個操作,隊列是先進先出原則,但是可以通過isSuspended暫停隊列內部的操作,下邊是一個例子演示:
let queue = { () -> OperationQueue in
let operationQueue = OperationQueue()
operationQueue.maxConcurrentOperationCount = 1
operationQueue.isSuspended = true
operationQueue.qualityOfService = .utility
return operationQueue
}()
queue.addOperation {
print("1")
}
queue.addOperation {
print("2")
}
queue.addOperation {
print("3")
}
queue.isSuspended = false
打印結果:
1
2
3
隊列提供了強大的功能,了解隊列的知識點非常有必要,有很大的一種可能性,也許某個問題卡住了,用隊列能夠很輕松的解決。有興趣可以看看我模仿SDWebImage寫的下載器MCDownloader(iOS下載器)說明書。
處理網絡請求,就必須要面對安全的問題,為了解決數據傳輸安全問題,到目前為止,已經出現了很多種解決方式。想了解這方面的知識,可以去看<<HTTP權威指南>>。
在Alamofire源碼解讀系列(一)之概述和使用中的Alamofire高級使用技巧部分。
/// Associates an HTTP Basic credential with the request.
///
/// - parameter user: The user.
/// - parameter password: The password.
/// - parameter persistence: The URL credential persistence. `.ForSession` by default.
///
/// - returns: The request.
/// 這里需要注意一點,persistence表示持久性,可以點擊去查看詳細說明
@discardableResult
open func authenticate(
user: String,
password: String,
persistence: URLCredential.Persistence = .forSession)
-> Self
{
let credential = URLCredential(user: user, password: password, persistence: persistence)
return authenticate(usingCredential: credential)
}
/// Associates a specified credential with the request.
///
/// - parameter credential: The credential.
///
/// - returns: The request.
@discardableResult
open func authenticate(usingCredential credential: URLCredential) -> Self {
delegate.credential = credential
return self
}
上邊的這兩個函數能夠處理請求中的驗證問題,可以用來應對用戶密碼和證書驗證。
/// Returns a base64 encoded basic authentication credential as an authorization header tuple.
///
/// - parameter user: The user.
/// - parameter password: The password.
///
/// - returns: A tuple with Authorization header and credential value if encoding succeeds, `nil` otherwise.
open static func authorizationHeader(user: String, password: String) -> (key: String, value: String)? {
guard let data = "\(user):\(password)".data(using: .utf8) else { return nil }
let credential = data.base64EncodedString(options: [])
return (key: "Authorization", value: "Basic \(credential)")
}
這個方法是一個輔助函數,某些服務器可能需要把用戶名和密碼拼接到請求頭中,那么可以使用這個函數來實現。
我們對一個請求的操作有下邊3中可能:
-
resume 喚醒該請求,這個非常簡單,函數中做了3件事:記錄開始時間,喚醒task,發通知。
/// Resumes the request. open func resume() { guard let task = task else { delegate.queue.isSuspended = false ; return } if startTime == nil { startTime = CFAbsoluteTimeGetCurrent() } task.resume() NotificationCenter.default.post( name: Notification.Name.Task.DidResume, object: self, userInfo: [Notification.Key.Task: task] ) }
-
suspend 暫停
/// Suspends the request. open func suspend() { guard let task = task else { return } task.suspend() NotificationCenter.default.post( name: Notification.Name.Task.DidSuspend, object: self, userInfo: [Notification.Key.Task: task] ) }
-
cancel 取消
/// Cancels the request. open func cancel() { guard let task = task else { return } task.cancel() NotificationCenter.default.post( name: Notification.Name.Task.DidCancel, object: self, userInfo: [Notification.Key.Task: task] ) }
Request中對CustomDebugStringConvertible和CustomStringConvertible的實現,我們就不做太多介紹了,有兩點需要注意:
類似像
urlCredentialStorage
,httpCookieStorage
這種帶有Storage
字段的對象,需要仔細研究一下這種代碼設計的規律。-
下邊這一小段代碼正好提現了swift的優雅之處,需要記住:
for (field, value) in headerFields where field != "Cookie" { headers[field] = value }
TaskConvertible
TaskConvertible協議給了給了我們轉換task的能力,任何實現了該協議的對象,都表示能夠轉換成一個task。我們都知道DataRequest,DownloadRequest,UploadRequest,StreamRequest都繼承自Request,最終應該是通過TaskConvertible協議來把一個URLRequest轉換成對應的task。
而Alamofire的Request的設計中,采用struct或者enum來實現這個協議,我們來看看這些實現;
DataRequest:
struct Requestable: TaskConvertible {
let urlRequest: URLRequest
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
do {
let urlRequest = try self.urlRequest.adapt(using: adapter)
return queue.sync { session.dataTask(with: urlRequest) }
} catch {
throw AdaptError(error: error)
}
}
}
DownloadRequest:
enum Downloadable: TaskConvertible {
case request(URLRequest)
case resumeData(Data)
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
do {
let task: URLSessionTask
switch self {
case let .request(urlRequest):
let urlRequest = try urlRequest.adapt(using: adapter)
task = queue.sync { session.downloadTask(with: urlRequest) }
case let .resumeData(resumeData):
task = queue.sync { session.downloadTask(withResumeData: resumeData) }
}
return task
} catch {
throw AdaptError(error: error)
}
}
}
如果task的類型是下載,會出現兩種情況,一種是直接通過URLRequest生成downloadTask,另一種是根據已有的數據恢復成downloadTask。我們之前已經講過了,下載失敗后會有resumeData。里邊保存了下載信息,這里就不提了。總之,上邊這個enum給我們提供了兩種不同的方式來生成downloadTask。
這種代碼的設計值得學習。
UploadRequest:
enum Uploadable: TaskConvertible {
case data(Data, URLRequest)
case file(URL, URLRequest)
case stream(InputStream, URLRequest)
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
do {
let task: URLSessionTask
switch self {
case let .data(data, urlRequest):
let urlRequest = try urlRequest.adapt(using: adapter)
task = queue.sync { session.uploadTask(with: urlRequest, from: data) }
case let .file(url, urlRequest):
let urlRequest = try urlRequest.adapt(using: adapter)
task = queue.sync { session.uploadTask(with: urlRequest, fromFile: url) }
case let .stream(_, urlRequest):
let urlRequest = try urlRequest.adapt(using: adapter)
task = queue.sync { session.uploadTask(withStreamedRequest: urlRequest) }
}
return task
} catch {
throw AdaptError(error: error)
}
}
}
雖然內容與上邊的DownloadRequest不同,但是套路卻相同。從代碼中,也能看出,上傳數據有3種介質,分別是:data,file,stream。
StreamRequest:
enum Streamable: TaskConvertible {
case stream(hostName: String, port: Int)
case netService(NetService)
func task(session: URLSession, adapter: RequestAdapter?, queue: DispatchQueue) throws -> URLSessionTask {
let task: URLSessionTask
switch self {
case let .stream(hostName, port):
task = queue.sync { session.streamTask(withHostName: hostName, port: port) }
case let .netService(netService):
task = queue.sync { session.streamTask(with: netService) }
}
return task
}
}
netService超出了本文的范圍,就不做介紹了,平時用的也少。
我們對上邊這些struct,enum做一個總結:由于struct,enum是值拷貝,因此他們比較適合作為數據的載體。一個方案的邏輯中,如果可能出現多個可能性,就考慮使用enum。還有最重要的一點,盡量把一個單一的功能的作用域限制的越小越好。功能越單一,結構越簡單的函數越安全。
忽略的內容
在Request.swift的源碼中,還有一個給任務添加進度的方法,在這里就不做介紹了,原理就是自定義一個函數,傳遞給task的代理。在DownloadRequest中對取消下載任務做了一些額外的處理。還有設置下載后的目錄等等。
DownloadOptions
這個DownloadOptions其實挺有意思的,他實現了OptionSet協議,因此它就有了集合的一些特性。
在OC中,我們往往通過掩碼來實現多個選項共存這一功能,但DownloadOptions用另一種方式實現了這一功能:
/// A collection of options to be executed prior to moving a downloaded file from the temporary URL to the
/// destination URL.
public struct DownloadOptions: OptionSet {
/// Returns the raw bitmask value of the option and satisfies the `RawRepresentable` protocol.
public let rawValue: UInt
/// A `DownloadOptions` flag that creates intermediate directories for the destination URL if specified.
public static let createIntermediateDirectories = DownloadOptions(rawValue: 1 << 0)
/// A `DownloadOptions` flag that removes a previous file from the destination URL if specified.
public static let removePreviousFile = DownloadOptions(rawValue: 1 << 1)
/// Creates a `DownloadFileDestinationOptions` instance with the specified raw value.
///
/// - parameter rawValue: The raw bitmask value for the option.
///
/// - returns: A new log level instance.
public init(rawValue: UInt) {
self.rawValue = rawValue
}
}
上邊的代碼只擴展了兩個默認選項:
- createIntermediateDirectories
- removePreviousFile
可以采用類似的手法,自己擴展更多的選項??匆幌孪逻叺睦泳兔靼琢耍?/p>
var op = DownloadRequest.DownloadOptions(rawValue: 1)
op.insert(DownloadRequest.DownloadOptions(rawValue: 2))
if op.contains(.createIntermediateDirectories) {
print("createIntermediateDirectories")
}
if op.contains(.removePreviousFile) {
print("removePreviousFile")
}
上邊代碼中,if語句內的打印都會調用。
總結
這一篇文章與上一篇間隔了很長時間,原因是公司做了一個項目。這個中小型項目結束后,也有一些需要總結的地方,我會把這些感觸寫下來,和大家討論一些項目開發的內容。
讀的越多,越發覺得Alamofire中的函數的設計很厲害。不是一時半會能夠全部串聯的。
由于知識水平有限,如有錯誤,還望指出
鏈接
Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園
Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園
Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園
Alamofire源碼解讀系列(四)之參數編碼(ParameterEncoding) 簡書-----博客園
Alamofire源碼解讀系列(五)之結果封裝(Result) 簡書-----博客園
Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園
Alamofire源碼解讀系列(七)之網絡監控(NetworkReachabilityManager) 簡書-----博客園
Alamofire源碼解讀系列(八)之安全策略(ServerTrustPolicy) 簡書-----博客園
Alamofire源碼解讀系列(九)之響應封裝(Response) 簡書-----博客園
Alamofire源碼解讀系列(十)之序列化(ResponseSerialization) 簡書-----博客園