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

Alamofire源码解读系列(十二)之请求(Request)的更多相关文章

  1. Alamofire源码解读系列(十二)之时间轴(Timeline)

    本篇带来Alamofire中关于Timeline的一些思路 前言 Timeline翻译后的意思是时间轴,可以表示一个事件从开始到结束的时间节点.时间轴的概念能够应用在很多地方,比如说微博的主页就是一个 ...

  2. Alamofire源码解读系列(十)之序列化(ResponseSerialization)

    本篇主要讲解Alamofire中如何把服务器返回的数据序列化 前言 和前边的文章不同, 在这一篇中,我想从程序的设计层次上解读ResponseSerialization这个文件.更直观的去探讨该功能是 ...

  3. Alamofire源码解读系列(十一)之多表单(MultipartFormData)

    本篇讲解跟上传数据相关的多表单 前言 我相信应该有不少的开发者不明白多表单是怎么一回事,然而事实上,多表单确实很简单.试想一下,如果有多个不同类型的文件(png/txt/mp3/pdf等等)需要上传给 ...

  4. Alamofire源码解读系列(二)之错误处理(AFError)

    本篇主要讲解Alamofire中错误的处理机制 前言 在开发中,往往最容易被忽略的内容就是对错误的处理.有经验的开发者,能够对自己写的每行代码负责,而且非常清楚自己写的代码在什么时候会出现异常,这样就 ...

  5. Alamofire源码解读系列(四)之参数编码(ParameterEncoding)

    本篇讲解参数编码的内容 前言 我们在开发中发的每一个请求都是通过URLRequest来进行封装的,可以通过一个URL生成URLRequest.那么如果我有一个参数字典,这个参数字典又是如何从客户端传递 ...

  6. Alamofire源码解读系列(三)之通知处理(Notification)

    本篇讲解swift中通知的用法 前言 通知作为传递事件和数据的载体,在使用中是不受限制的.由于忘记移除某个通知的监听,会造成很多潜在的问题,这些问题在测试中是很难被发现的.但这不是我们这篇文章探讨的主 ...

  7. Alamofire源码解读系列(五)之结果封装(Result)

    本篇讲解Result的封装 前言 有时候,我们会根据现实中的事物来对程序中的某个业务关系进行抽象,这句话很难理解.在Alamofire中,使用Response来描述请求后的结果.我们都知道Alamof ...

  8. Alamofire源码解读系列(六)之Task代理(TaskDelegate)

    本篇介绍Task代理(TaskDelegate.swift) 前言 我相信可能有80%的同学使用AFNetworking或者Alamofire处理网络事件,并且这两个框架都提供了丰富的功能,我也相信很 ...

  9. Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager)

    Alamofire源码解读系列(七)之网络监控(NetworkReachabilityManager) 本篇主要讲解iOS开发中的网络监控 前言 在开发中,有时候我们需要获取这些信息: 手机是否联网 ...

随机推荐

  1. The superclass “javax.servlet.http.HttpServlet" was not found on the Java Build Path错误

    1.异常信息 创建maven web项目时,出现 The superclass "javax.servlet.http.HttpServlet" was not found on ...

  2. VMware workstation转到vsphere解决办法

    一.前因 上一篇http://www.cnblogs.com/cuncunjun/p/6611837.html 中提到,我想把本地的vmware workstation的虚拟机拷贝到服务器上,因为鄙人 ...

  3. [SinGuLaRiTy] 组合数学

    [SinGuLaRiTy-1005] Copyright (c) SinGuLaRiTy 2017. All Rights Reserved . 加法原理 设事件A有m种产生方式,事件B有n种产生方式 ...

  4. Java集合常见面试题集锦

    1.介绍Collection框架的结构 集合是Java中的一个非常重要的一个知识点,主要分为List.Set.Map.Queue三大数据结构.它们在Java中的结构关系如下: Collection接口 ...

  5. display与visibility的使用(区别)

    display:none;隐藏元素,且此元素无物理位置: visibility:hidden;隐藏元素,但元素的物理位置依然存在: 因为display:none导致页面上无此元素的空间,js就获取不到 ...

  6. 设置ARC有效或者无效

    在编译单位上,可以设置ARC有效或者无效.比如对每个文件可以选择使用或者不使用ARC,一个应用程序中可以混合ARC有效或者无效的二进制形式. 设置ARC有效的编译方法如下所示:(Xcode4.2开始默 ...

  7. jQuery的发展史

    jQuery的发展史,你知道吗? 每天多学一点知识,就少写一行代码2006年1月,jQuery的第一个版本面世,至今已经有6年多了(注:这个时间点是截止至出书时间).虽然过了这么久,但它依然以其简洁. ...

  8. sizeof 与 strlen

    一.sizeof     sizeof(...)是运算符,其值在编译时即计算好了,参数可以是数组.指针.类型.对象.函数等.    它的功能是:获得保证能容纳实现所建立的最大对象的字节大小.    由 ...

  9. css过渡模块和2d转换模块

    今天,我们一起来研究一下css3中的过渡模块.2d转换模块和3d转换模块 一.过渡模块transition (一)过度模块的三要素: 1.必须要有属性发生变化 2.必须告诉系统哪个属性需要执行过渡效果 ...

  10. node-ejs-mongodb结合的项目案例-----引用mongoose和未引用mongoose模块

    本项目个人尝试了2种方法,一个是直接用mongod,一个是引用mongod里的mongoose. nodejs-ejs-mogondb- nodej+ejs模板,通过mogondb数据查询数据实现简单 ...