本篇是Alamofire中的请求抽象层的讲解html
在Alamofire中,围绕着Request,设计了不少额外的特性,这也偏偏代表,Request是全部请求的基础部分和发起点。这无疑给咱们一个Request很复杂的想法。但看了Alamofire中Request.swift
中的代码,Request被设计的又是如此的简单,这就是为何这些顶级框架如此让人喜好的缘由。swift
在后续的文章中,我会单独写一篇Swift中协议的使用技巧,在Alamofire源码解读系列(一)之概述和使用这篇的Alamofire高级用法中,我根据Alamofire官方文档作了一些补充,其中涉及到了URLConvertible和URLRequestConvertible的高级用法,在本篇中一样出现了3个协议:安全
有一点须要特别说明的是,在使用Alamofire的高级用法时,须要操做SessionManager这个类。服务器
明白Alamofire中一个请求的过程,是很是有必要的。先看下边的代码:网络
Alamofire.request("https://httpbin.org/get")
上边的代码是最简单的一个请求,咱们看看Alamofire.request中究竟干了什么?session
@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
。post
URLRequestConvertible协议的目的是对URLRequest进行自定义的转换,所以,在得到转换后的URLRequest后,须要用URLRequest生成task,这样才能发起网络请求,在Alamofire中,但凡是request开头的函数,默认的都是DataRequest类型,如今有了URLRequest还不够,还须要检测她可否生成与之相对应的task。学习
在上边的函数中,用到了DataRequest.Requestable,Requestable其实一个结构体,他实现了TaskConvertible协议,所以,它可以用URLRequest生成与之相对应的task。接下来就初始化DataRequest,而后真正的开始发起请求。
咱们总结一下这个过程:
明白了上边的过程,再回过头来看Request.swift
也就是本篇的内容就简单多了,就下边几个目的:
有不少二次封装的网络框架中,通常都有这么一个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协议给了给了咱们转换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其实挺有意思的,他实现了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 } }
上边的代码只扩展了两个默认选项:
能够采用相似的手法,本身扩展更多的选项。看一下下边的例子就明白了:
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) 简书-----博客园