本篇是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主要用于CustomStringConvertibleCustomDebugStringConvertible这两个协议的实现方法中获取特定的数据。

这里有一点小提示,在创建自定义的类的时候,实现上边这两个协议,通过打印,能够进行快速的调试。

上边方法中第二个参数是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的实现,我们就不做太多介绍了,有两点需要注意:

  1. 类似像urlCredentialStorage, httpCookieStorage这种带有Storage字段的对象,需要仔细研究一下这种代码设计的规律。

  2. 下边这一小段代码正好提现了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

可以采用类似的手法,自己扩展更多的选项。看一下下边的例子就明白了:

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) 简书-----博客园

Alamofire源码解读系列(十一)之多表单(MultipartFormData) 简书-----博客园

Alamofire源码解读系列(十二)之时间轴(Timeline) 简书-----博客园

最新文章

  1. Android中Retrifit使用总结
  2. 记 Mac Pro 系统升级后,编译安装 PHP-5.6.28 / PHP-7.0 报错修复过程
  3. wf(七)(手把手包会)
  4. 关于synchronized 影响可见性的问题
  5. Crontab的格式
  6. FPGA学习之基本结构
  7. db2官方SQLSTATE代码提示
  8. php数组(array)输出三种形式
  9. Copy xml 文件
  10. 纯CSS3打造七巧板
  11. docker安装与学习
  12. mybatis mysql 批量插入
  13. python使用xlrd, xlwt读取excel文件和 写入excel文件
  14. qt quick-初始学习概念
  15. 从面向服务架构(SOA)学习:微服务时代应该借鉴的5条经验教训
  16. Luogu P5168 xtq玩魔塔
  17. java json注解
  18. Python学习注脚
  19. test20180921 手机信号
  20. 【python】正则表达式-常用函数

热门文章

  1. 深入React组件生命周期
  2. 【原】Linux设备网络硬件管理
  3. Jenkins集成Docker
  4. oracle查询每个表所占的空间
  5. 老李分享知识:性能测试之TPS和吞吐率
  6. 手机自动化测试:appium源码分析之bootstrap十五
  7. 使用纯CSS方案,解决垂直居中
  8. 简单的利用JS来判断页面是在手机端还是在PC端打开的方法
  9. 如何解决chrome 等浏览器不支持本地ajax请求的问题
  10. Linux防火墙配置—访问外网WEB