Alamofire(四)怎么合理使用Alamofire

  • (一) Alamofire框架功能简介
  • (二)Alamofire api使用
    • 1. 发请求
    • 2. 响应处理
      • 2.1 五种不同的响应handler
        • 2.1.1 响应 Handler
        • 2.1.2 响应数据 Handler
        • 2.1.3 响应字符串 Handler
        • 2.1.4 响应 JSON Handler
        • 2.1.5 链式响应handler
      • 2.2 响应handler队列
      • 2.3 响应验证
      • 2.4 响应缓存
      • 2.5 HTTP协议相关
        • 2.5.1 HTTP方法
        • 2.5.2 HTTP请求头
        • 2.5.3 认证
          • 2.5.3.1 HTTP Basic认证
          • 2.5.3.2 使用URLCredential认证
      • 2.6 编码相关
        • 2.6.1 参数编码
          • 2.6.1.1 URL编码
            • 设置Bool类型参数的编码
            • 设置Array类型参数编码
          • 2.6.1.2 JSON编码
          • 2.6.1.3 属性列表编码
          • 2.6.1.4 自定义编码
          • 2.6.1.5 手动URL请求参数编码
      • 2.7 下载
        • 2.7.1 将数据下载到文件
        • 2.7.2 下载文件存储位置
        • 2.7.3 下载进度
        • 2.7.4 恢复下载
      • 2.8 上传
        • 2.8.1 上传数据到服务器
        • 2.8.2 上传数据
        • 2.8.3 上传文件
        • 2.8.4 上传多部分表单数据
        • 2.8.5 上传进度
      • 2.9 统计指标
        • 2.9.1 时间表
        • 2.9.2 URL会话任务指标
        • 2.9.3 cURL命令输出
  • (三)Alamofire不同场景api使用
    • 1. Session Manager
      • 1.1 会话代理 SessionDelegate
    • 2. 请求 Request
      • 2.1 DataRequest
      • 2.2 DownloadRequest
      • 2.3 UploadRequest
      • 2.4 StreamRequest
      • 2.5 URLConvertible
      • 2.6 URLRequestConvertible
      • 2.7 CRUD和授权
      • 2.8 适配和重试请求
        • 2.8.1 RequestAdapter
        • 2.8.2 RequestRetrier
      • 2.9
    • 3. 序列化
      • 3.1 自定义响应序列化
        • 3.1.1 响应映射
        • 3.1.2 处理错误
        • 3.1.3 创建一个自定义的响应序列化器
        • 3.1.4 泛型响应对象序列化
    • 4. 安全
      • 4. 1 ServerTrustPolicy
      • 4. 2 ServerTrustPolicyManager(服务器信任策略管理者 )
      • 4. 3 子类化服务器信任策略管理者
      • 4. 4 验证主机
      • 4. 5 验证证书链
      • 4. 6 ATS 应用传输安全 (App Transport Security)
    • 5. 网络可达性
      • 5. 1 Network Reachability

(一) Alamofire框架功能简介

  • 前面写了三篇关于Alamofire框架的博客,基本是循序渐进的方式,先讲解了必须了解的网络原理,协议等知识,然后讲解了苹果自带框架api UISession的一些知识。而Alamofire就是基于这些实现的网络框架,专注于网络相关的api。
  • 为了更好的理解Alamofire框架的实现原理,很有必要先了解Alamofire框架是什么?它提供了一些什么功能?如果正确高效的使用它?
  • 带着这三个问题,本篇博客将围绕这三个方向来阐述。
  • (1) Alamofire框架是什么?

Alamofire就是牛逼的框架AFNetwork开发者的母公司开发的一套基于swift实现的网络框架。Alamofire专注于核心网络的实现,Alamofire生态系统还有另外两个库:AlamofireImage,AlamofireNetworkActivityIndicator

  1. AlamofireImage:
    一个图片库,包括图像响应序列化器、UIImage和UIImageView的扩展、自定义图像滤镜、内存中自动清除和基于优先级的图像下载系统。
  1. AlamofireNetworkActivityIndicator:
    控制iOS应用的网络活动指示器。包含可配置的延迟计时器来帮助减少闪光,并且支持不受Alamofire管理的URLSession实例。
  • (2) Alamofire框架提供了什么功能?
  1. 链式请求 / 响应方法
  2. URL / JSON / plist参数编码
  3. 上传文件 / 数据 / 流 / 多表单数据
  4. 使用请求或者断点下载来下载文件
  5. 使用URL凭据进行身份认证
  6. HTTP响应验证
  7. 包含进度的上传和下载闭包
  8. cURL命令的输出
  9. 动态适配和重试请求
  10. TLS证书和Public Key Pinning
  11. 网络可达性
  12. 全面的单元和集成测试覆盖率
  • (3) Alamofire框架如何使用?
  1. 要求的使用环境:

iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+
Xcode 10.2+
Swift 5+

  1. 安装方法:
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!target '项目名称' dopod 'Alamofire', '~> 5.0.0-beta.5'
end
  1. api使用,下面将详细讲述

(二)Alamofire api使用

1. 发请求

Alamofire.request("http://qq.com/")

2. 响应处理

  • 直接在请求后面用点语法链接响应处理:

实例1:

Alamofire.request("https://httpbin.org/get").responseJSON { response inprint(response.request)  // 原始的URL请求print(response.response) // HTTP URL响应print(response.data)     // 服务器返回的数据print(response.result)   // 响应序列化结果,在这个闭包里,存储的是JSON数据if let JSON = response.result.value {print("JSON: \(JSON)")}
}

在上面的例子中,responseJSON handler直接拼接到请求后面,当请求完成后被调用。这个闭包一旦收到响应后,就会处理这个响应,并不会因为等待服务器的响应而造成阻塞执行。请求的结果仅在响应闭包的范围内可用。其他任何与服务器返回的响应或者数据相关的操作,都必须在这个闭包内执行。

2.1 五种不同的响应handler

实例2:

  • 所有的响应handler都不会对响应进行验证。也就是说响应状态码在400…<500和500…<600范围内,都不会触发错误。
// 响应 Handler - 未序列化的响应
func response(queue: DispatchQueue?,completionHandler: @escaping (DefaultDataResponse) -> Void)-> Self// 响应数据 Handler - 序列化成数据类型
func responseData(queue: DispatchQueue?,completionHandler: @escaping (DataResponse<Data>) -> Void)-> Self// 响应字符串 Handler - 序列化成字符串类型
func responseString(queue: DispatchQueue?,encoding: String.Encoding?,completionHandler: @escaping (DataResponse<String>) -> Void)-> Self// 响应 JSON Handler - 序列化成Any类型
func responseJSON(queue: DispatchQueue?,completionHandler: @escaping (DataResponse<Any>) -> Void)-> Self// 响应 PropertyList (plist) Handler - 序列化成Any类型
func responsePropertyList(queue: DispatchQueue?,completionHandler: @escaping (DataResponse<Any>) -> Void))-> Self

2.1.1 响应 Handler

  • response handler不处理任何响应数据。它仅仅是从URL session delegate中转发信息。
Alamofire.request("https://httpbin.org/get").response { response inprint("Request: \(response.request)")print("Response: \(response.response)")print("Error: \(response.error)")if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {print("Data: \(utf8Text)")}
}
  • 一般情况下不建议使用这种没有响应序列化器的handler,而应该使用下面有特定序列化器的handler。

2.1.2 响应数据 Handler

  • responseData handler使用responseDataSerializer(这个对象把服务器的数据序列化成其他类型)来提取服务器返回的数据。如果没有返回错误并且有数据返回,那么响应Result将会是.successvalueData类型。
Alamofire.request("https://httpbin.org/get").responseData { response indebugPrint("All Response Info: \(response)")if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) {print("Data: \(utf8Text)")}
}

2.1.3 响应字符串 Handler

  • responseString handler使用responseStringSerializer对象根据指定的编码格式把服务器返回的数据转换成String。如果没有返回错误并且服务器的数据成功地转换为String,那么响应Result将会是.success,value是String类型。
Alamofire.request("https://httpbin.org/get").responseString { response inprint("Success: \(response.result.isSuccess)")print("Response String: \(response.result.value)")
}
  • 如果没有指定编码格式,将会使用服务器的HTTPURLResponse指定的格式。如果服务器无法确定编码格式,那么默认使用.isoLatin1

2.1.4 响应 JSON Handler

  • responseJSON handler使用responseJSONSerializer根据指定的JSONSerialization.ReadingOptions把服务器返回的数据转换成Any类型。如果没有返回错误并且服务器的数据成功地转换为JSON对象,那么响应Result将会是.success,value是Any类型。
Alamofire.request("https://httpbin.org/get").responseJSON { response indebugPrint(response)if let json = response.result.value {print("JSON: \(json)")}
}
  • 所有JSON的序列化,都是使用JSONSerialization完成的。

2.1.5 链式响应handler

  • 响应handler可以链接在一起:
Alamofire.request("https://httpbin.org/get").responseString { response inprint("Response String: \(response.result.value)")}.responseJSON { response inprint("Response JSON: \(response.result.value)")}
  • 在同一个请求中使用多个响应handler,要求服务器的数据会被序列化多次,每次对应一个handler。

2.2 响应handler队列

  • 默认情况下,响应handler是在主队列执行的。但是我们也可以自定义队列:
let utilityQueue = DispatchQueue.global(qos: .utility)Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response inprint("Executing response handler on utility queue")
}

2.3 响应验证

  • 默认情况下,Alamofire把所有完成的请求当做是成功的请求,无论响应的内容是什么。如果响应有一个不能被接受的状态码或者MIME类型,在响应handler之前调用validate将会产生错误。

  • 手动验证:

Alamofire.request("https://httpbin.org/get").validate(statusCode: 200..<300).validate(contentType: ["application/json"]).responseData { response inswitch response.result {case .success:print("Validation Successful")case .failure(let error):print(error)}
}
  • 自动验证:自动验证在200…299范围内的状态码;如果请求头中有指定Accept,那么也会验证响应头的与请求头Accept一样的Content-Type。
Alamofire.request("https://httpbin.org/get").validate().responseJSON { response inswitch response.result {case .success:print("Validation Successful")case .failure(let error):print(error)}
}

2.4 响应缓存

  • 响应缓存是使用系统的框架URLCache来处理的。它提供了内存和磁盘上的缓存,并允许我们控制内存和磁盘的大小。
  • 默认情况下,Alamofire利用共享的URLCache。

2.5 HTTP协议相关

2.5.1 HTTP方法

  • HTTPMethod列举了下面的这些方法:
public enum HTTPMethod: String {case options = "OPTIONS"case get     = "GET"case head    = "HEAD"case post    = "POST"case put     = "PUT"case patch   = "PATCH"case delete  = "DELETE"case trace   = "TRACE"case connect = "CONNECT"
}
  • 在使用Alamofire.request时,可以传入方法参数:
Alamofire.request("https://httpbin.org/get") // 默认是get请求Alamofire.request("https://httpbin.org/post", method: .post)
Alamofire.request("https://httpbin.org/put", method: .put)
Alamofire.request("https://httpbin.org/delete", method: .delete)

2.5.2 HTTP请求头

  • 可以直接在请求方法添加自定义HTTP请求头,这有利于我们在请求中添加请求头。
let headers: HTTPHeaders = ["Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==","Accept": "application/json"
]Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response indebugPrint(response)
}
  • 对于那些不变的请求头,建议在URLSessionConfiguration设置,这样就可以自动被用于任何URLSession创建的URLSessionTask
  • 默认的Alamofire SessionManager为每一个请求提供了一个默认的请求头集合,包括:

Accept-Encoding,默认是gzip;q=1.0, compress;q=0.5。

Accept-Language,默认是系统的前6个偏好语言,格式类似于en;q=1.0。

User-Agent,包含当前应用程序的版本信息。例如iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0。

  • 如果要自定义这些请求头集合,我们必须创建一个自定义的URLSessionConfigurationdefaultHTTPHeaders属性将会被更新,并且自定义的会话配置也会应用到新的SessionManager实例。

2.5.3 认证

  • 认证是使用系统框架URLCredentialURLAuthenticationChallenge实现的。
  • 支持的认证方案:

HTTP Basic
HTTP Digest
Kerberos
NTLM

2.5.3.1 HTTP Basic认证
  • 在合适的时候,在一个请求的authenticate方法会自动提供一个URLCredential给URLAuthenticationChallenge:
let user = "user"
let password = "password"Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)").authenticate(user: user, password: password).responseJSON { response indebugPrint(response)
}
  • 根据服务器实现,Authorization header也可能是适合的:
let user = "user"
let password = "password"var headers: HTTPHeaders = [:]if let authorizationHeader = Request.authorizationHeader(user: user, password: password) {headers[authorizationHeader.key] = authorizationHeader.value
}Alamofire.request("https://httpbin.org/basic-auth/user/password", headers: headers).responseJSON { response indebugPrint(response)
}
2.5.3.2 使用URLCredential认证
  • 使用URLCredential来做认证,如果服务器发出一个challenge,底层的URLSession实际上最终会发两次请求。第一次请求不会包含credential,并且可能会触发服务器发出一个challenge。这个challenge会被Alamofire接收,credential会被添加,然后URLSessin会重试请求。
let user = "user"
let password = "password"let credential = URLCredential(user: user, password: password, persistence: .forSession)Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)").authenticate(usingCredential: credential).responseJSON { response indebugPrint(response)
}

2.6 编码相关

2.6.1 参数编码

  • Alamofire支持三种参数编码:URLJSONPropertyList。还支持遵循了ParameterEncoding协议的自定义编码。
2.6.1.1 URL编码
  • URLEncoding类型创建了一个URL编码的查询字符串来设置或者添加到一个现有的URL查询字符串,或者设置URL请求的请求体。查询字符串是否被设置或者添加到现有的URL查询字符串,或者被作为HTTP请求体,决定于编码的Destination。编码的Destination有三个case:

    • .methodDependent:为GET、HEAD和DELETE请求使用编码查询字符串来设置或者添加到现有查询字符串,并且使用其他HTTP方法来设置请求体。

    • .queryString:设置或者添加编码查询字符串到现有查询字符串

    • .httpBody:把编码查询字符串作为URL请求的请求体

  • 一个编码请求的请求体的Content-Type字段被设置为application/x-www-form-urlencoded; charset=utf-8。因为没有公开的标准说明如何编码集合类型,所以按照惯例在key后面添加[]来表示数组的值(foo[]=1&foo[]=2),在key外面包一个中括号来表示字典的值(foo[bar]=baz)。

  • 使用URL编码参数的GET请求

let parameters: Parameters = ["foo": "bar"]// 下面这三种写法是等价的
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding 默认是`URLEncoding.default`
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/get", parameters: parameters, encoding: URLEncoding(destination: .methodDependent))// https://httpbin.org/get?foo=bar
  • 使用URL编码参数的POST请求
let parameters: Parameters = ["foo": "bar","baz": ["a", 1],"qux": ["x": 1,"y": 2,"z": 3]
]// 下面这三种写法是等价的
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: URLEncoding.httpBody)// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3
设置Bool类型参数的编码
  • URLEncoding.BoolEncoding提供了两种编码方式:

.numeric:把true编码为1,false编码为0
.literal:把true编码为true,false编码为false

  • 默认情况下:Alamofire使用.numeric。
  • 可以使用下面的初始化函数来创建URLEncoding,指定Bool编码的类型:
let encoding = URLEncoding(boolEncoding: .literal)
设置Array类型参数编码
  • URLEncoding.ArrayEncoding提供了两种编码方式:

.brackets: 在每个元素值的key后面加上一个[],如foo=[1,2]编码成foo[]=1&foo[]=2
.noBrackets:不添加[],例如foo=[1,2]编码成``foo=1&foo=2`

  • 默认情况下,Alamofire使用.brackets。
  • 可以使用下面的初始化函数来创建URLEncoding,指定Array编码的类型:
let encoding = URLEncoding(arrayEncoding: .noBrackets)
2.6.1.2 JSON编码
  • JSONEncoding类型创建了一个JOSN对象,并作为请求体。编码请求的请求头的Content-Type请求字段被设置为application/json
  • 使用JSON编码参数的POST请求
let parameters: Parameters = ["foo": [1,2,3],"bar": ["baz": "qux"]
]// 下面这两种写法是等价的
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding(options: []))// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}
2.6.1.3 属性列表编码
  • PropertyListEncoding根据关联格式和写选项值,使用PropertyListSerialization来创建一个属性列表对象,并作为请求体。编码请求的请求头的Content-Type请求字段被设置为application/x-plist
2.6.1.4 自定义编码
  • 如果提供的ParameterEncoding类型不能满足我们的要求,可以创建自定义编码。下面演示如何快速自定义一个JSONStringArrayEncoding类型把JSON字符串数组编码到请求中。
struct JSONStringArrayEncoding: ParameterEncoding {private let array: [String]init(array: [String]) {self.array = array}func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {var urlRequest = urlRequest.urlRequestlet data = try JSONSerialization.data(withJSONObject: array, options: [])if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")}urlRequest.httpBody = datareturn urlRequest}
}
2.6.1.5 手动URL请求参数编码
  • ParameterEncodingAPI可以在创建网络请求外面使用。
let url = URL(string: "https://httpbin.org/get")!
var urlRequest = URLRequest(url: url)let parameters: Parameters = ["foo": "bar"]
let encodedURLRequest = try URLEncoding.queryString.encode(urlRequest, with: parameters)

2.7 下载

2.7.1 将数据下载到文件

  • Alamofire可以把服务器的数据下载到内存(in-memory)或者硬盘(on-disk)中。所有Alamofire.requestAPI下载的数据都是存储在内存中。这比较适合小文件,更高效;但是不适合大文件,因为大文件会把内存耗尽。我们要使用Alamofire.downloadAPI把服务器的数据下载到硬盘中。

  • 下面这个方法只适用于macOS。因为在其他平台不允许在应用沙盒外访问文件系统。下面会讲到如何在其他平台下载文件。

Alamofire.download("https://httpbin.org/image/png").responseData { response inif let data = response.result.value {let image = UIImage(data: data)}
}

2.7.2 下载文件存储位置

  • 我们可以提供一个DownloadFileDestination闭包把临时文件夹的文件移动到一个目标文件夹。在临时文件真正移动到destinationURL之前,闭包内部指定的DownloadOptions将会被执行。目前支持的DownloadOptions有下面两个:

    • .createIntermediateDirectories:如果指定了目标URL,将会创建中间目录。
    • .removePreviousFile:如果指定了目标URL,将会移除之前的文件
let destination: DownloadRequest.DownloadFileDestination = { _, _ inlet documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]let fileURL = documentsURL.appendPathComponent("pig.png")return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}Alamofire.download(urlString, to: destination).response { response inprint(response)if response.error == nil, let imagePath = response.destinationURL?.path {let image = UIImage(contentsOfFile: imagePath)}
}
  • 也可以直接使用建议的下载目标API:
let destination = DownloadRequest.suggestedDownloadDestination(directory: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)

2.7.3 下载进度

  • 所有的DownloadRequest都可以使用downloadProgressAPI来反馈下载进度。
Alamofire.download("https://httpbin.org/image/png").downloadProgress { progress inprint("Download Progress: \(progress.fractionCompleted)")}.responseData { response inif let data = response.result.value {let image = UIImage(data: data)}
}
  • downloadProgressAPI还可以接受一个queue参数来指定下载进度闭包在哪个DispatchQueue中执行。
let utilityQueue = DispatchQueue.global(qos: .utility)Alamofire.download("https://httpbin.org/image/png").downloadProgress(queue: utilityQueue) { progress inprint("Download Progress: \(progress.fractionCompleted)")}.responseData { response inif let data = response.result.value {let image = UIImage(data: data)}
}

2.7.4 恢复下载

  • 如果一个DownloadRequest被取消或中断,底层的URL会话会生成一个恢复数据。恢复数据可以被重新利用并在中断的位置继续下载。恢复数据可以通过下载响应访问,然后在重新开始请求的时候被利用。
  • 在iOS 10 - 10.2, macOS 10.12 - 10.12.2, tvOS 10 - 10.1, watchOS 3 - 3.1.1中,resumeData会被后台URL会话配置破坏。因为在resumeData的生成逻辑有一个底层的bug,不能恢复下载。具体情况可以到Stack Overflow看看。
class ImageRequestor {private var resumeData: Data?private var image: UIImage?func fetchImage(completion: (UIImage?) -> Void) {guard image == nil else { completion(image) ; return }let destination: DownloadRequest.DownloadFileDestination = { _, _ inlet documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]let fileURL = documentsURL.appendPathComponent("pig.png")return (fileURL, [.removePreviousFile, .createIntermediateDirectories])}let request: DownloadRequestif let resumeData = resumeData {request = Alamofire.download(resumingWith: resumeData)} else {request = Alamofire.download("https://httpbin.org/image/png")}request.responseData { response inswitch response.result {case .success(let data):self.image = UIImage(data: data)case .failure:self.resumeData = response.resumeData}}}
}

2.8 上传

2.8.1 上传数据到服务器

  • 使用JOSN或者URL编码参数上传一些小数据到服务器,使用Alamofire.request API就已经足够了。如果需要发送很大的数据,需要使用Alamofire.upload API。当我们需要在后台上传数据时,也可以使用Alamofire.upload。

2.8.2 上传数据

let imageData = UIPNGRepresentation(image)!Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response indebugPrint(response)
}

2.8.3 上传文件

let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response indebugPrint(response)
}

2.8.4 上传多部分表单数据

Alamofire.upload(multipartFormData: { multipartFormData inmultipartFormData.append(unicornImageURL, withName: "unicorn")multipartFormData.append(rainbowImageURL, withName: "rainbow")},to: "https://httpbin.org/post",encodingCompletion: { encodingResult inswitch encodingResult {case .success(let upload, _, _):upload.responseJSON { response indebugPrint(response)}case .failure(let encodingError):print(encodingError)}}
)

2.8.5 上传进度

  • 所有的UploadRequest都可以使用uploadProgress和downloadProgress APIs来反馈上传和下载进度。
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")Alamofire.upload(fileURL, to: "https://httpbin.org/post").uploadProgress { progress in // 默认在主线程中执行print("Upload Progress: \(progress.fractionCompleted)")}.downloadProgress { progress in // 默认在主线程中执行print("Download Progress: \(progress.fractionCompleted)")}.responseJSON { response indebugPrint(response)
}

2.9 统计指标

2.9.1 时间表

  • Alamofire在一个请求周期内收集时间,并创建一个Tineline对象,它是响应类型的一个属性。
Alamofire.request("https://httpbin.org/get").responseJSON { response inprint(response.timeline)
}

上面的Timeline信息包括:
Latency: 0.428 seconds (延迟)
Request Duration: 0.428 seconds (请求时间)
Serialization Duration: 0.001 seconds (序列化时间)
Total Duration: 0.429 seconds (总时间)

2.9.2 URL会话任务指标

  • 在iOS和tvOS 10和macOS 10.12中,苹果发布了新的URLSessionTaskMetrics APIs。这个任务指标封装了关于请求和响应执行的神奇统计信息。这个API和Timeline非常相似,但是提供了很多Alamofire没有提供的统计信息。这些指标可以通过任何响应去访问。
Alamofire.request("https://httpbin.org/get").responseJSON { response inprint(response.metrics)
}
  • 这些API只能在iOS和tvOS 10和macOS 10.12中使用。所以,根据部署目标,可能需要加入版本判断:
Alamofire.request("https://httpbin.org/get").responseJSON { response inif #available(iOS 10.0. *) {print(response.metrics)}
}

2.9.3 cURL命令输出

  • 调试平台问题很让人厌烦。庆幸的是,Alamofire的Request对象遵循了CustomStringConvertibleCustomDebugStringConvertible协议来提供一些非常有用的调试工具。
  • CustomStringConvertible
let request = Alamofire.request("https://httpbin.org/ip")
print(request)
// GET https://httpbin.org/ip (200)
  • CustomDebugStringConvertible
let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)

输出结果:

$ curl -i \-H "User-Agent: Alamofire/4.0.0" \-H "Accept-Encoding: gzip;q=1.0, compress;q=0.5" \-H "Accept-Language: en;q=1.0,fr;q=0.9,de;q=0.8,zh-Hans;q=0.7,zh-Hant;q=0.6,ja;q=0.5" \"https://httpbin.org/get?foo=bar"

(三)Alamofire不同场景api使用

1. Session Manager

  • 我们可以自己创建后台会话和短暂会话的session manager,还可以自定义默认的会话配置来创建新的session manager,例如修改默认的header httpAdditionalHeaders和timeoutIntervalForRequest。
Alamofire.request("https://httpbin.org/get")let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")
  • 用默认的会话配置创建一个Session Manager
let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)
  • 用后台会话配置创建一个Session Manager
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)
  • 用默短暂会话配置创建一个Session Manager
let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)
  • 修改会话配置
//不推荐在Authorization或者Content-Type header使用。而应该使用Alamofire.requestAPI、URLRequestConvertible和ParameterEncoding的headers参数。
var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaderslet sessionManager = Alamofire.SessionManager(configuration: configuration)

1.1 会话代理 SessionDelegate

  • 默认情况下,一个SessionManager实例创建一个SessionDelegate对象来处理底层URLSession生成的不同类型的代理回调。每个代理方法的实现处理常见的情况。然后,高级用户可能由于各种原因需要重写默认功能。

  • 有两种方式实现SessionDelegate:

    • 方式一:自定义SessionDelegate的方法是通过重写闭包。我们可以在每个闭包重写SessionDelegate API对应的实现。

    • 方式二:重写SessionDelegate的实现的方法是把它子类化。通过子类化,我们可以完全自定义他的行为,或者为这个API创建一个代理并且仍然使用它的默认实现。通过创建代理,我们可以跟踪日志事件、发通知、提供前后实现。

  • 实现SessionDelegate 代码实例:

  1. 重写闭包的示例:
/// 重写URLSessionDelegate的`urlSession(_:didReceive:completionHandler:)`方法
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?/// 重写URLSessionDelegate的`urlSessionDidFinishEvents(forBackgroundURLSession:)`方法
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?/// 重写URLSessionTaskDelegate的`urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`方法
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?/// 重写URLSessionDataDelegate的`urlSession(_:dataTask:willCacheResponse:completionHandler:)`方法
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?
  1. 使用taskWillPerformHTTPRedirection来避免回调到任何apple.com域名。
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
let delegate: Alamofire.SessionDelegate = sessionManager.delegatedelegate.taskWillPerformHTTPRedirection = { session, task, response, request invar finalRequest = requestiflet originalRequest = task.originalRequest,let urlString = originalRequest.url?.urlString,urlString.contains("apple.com"){finalRequest = originalRequest}return finalRequest
}
  1. 下面这个例子演示了如何子类化SessionDelegate,并且有回调的时候打印信息:
class LoggingSessionDelegate: SessionDelegate {override func urlSession(_ session: URLSession,task: URLSessionTask,willPerformHTTPRedirection response: HTTPURLResponse,newRequest request: URLRequest,completionHandler: @escaping (URLRequest?) -> Void){print("URLSession will perform HTTP redirection to request: \(request)")super.urlSession(session,task: task,willPerformHTTPRedirection: response,newRequest: request,completionHandler: completionHandler)}
}
  • 总的来说,无论是默认实现还是重写闭包,都应该提供必要的功能。子类化应该作为最后的选择。

2. 请求 Request

  • requestdownloaduploadstream方法的结果是DataRequestDownloadRequestUploadRequestStreamRequest,并且所有请求都继承自Request。所有的Request并不是直接创建的,而是由session manager创建的。

  • 每个子类都有特定的方法,例如authenticatevalidateresponseJSONuploadProgress,都返回一个实例,以便方法链接(也就是用点语法连续调用方法)。

  • 请求可以被暂停、恢复和取消:

suspend():暂停底层的任务和调度队列
resume():恢复底层的任务和调度队列。如果manager的startRequestsImmediately不是true,那么必须调用resume()来开始请求。
cancel():取消底层的任务,并产生一个error,error被传入任何已经注册的响应handlers。

  • 随着应用的不多增大,当我们建立网络栈的时候要使用通用的模式。在通用模式的设计中,一个很重要的部分就是如何传送请求。遵循Router设计模式的URLConvertibleURLRequestConvertible协议可以帮助我们

2.1 DataRequest

2.2 DownloadRequest

2.3 UploadRequest

2.4 StreamRequest

2.5 URLConvertible

  • 遵循了URLConvertible协议的类型可以被用来构建URL,然后用来创建URL请求。String、URL和URLComponent默认是遵循URLConvertible协议的。它们都可以作为url参数传入requestuploaddownload方法.
  • 以一种有意义的方式和web应用程序交互的应用,都鼓励使用自定义的遵循URLConvertible协议的类型将特定领域模型映射到服务器资源,因为这样比较方便。
let urlString = "https://httpbin.org/post"
Alamofire.request(urlString, method: .post)let url = URL(string: urlString)!
Alamofire.request(url, method: .post)let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
Alamofire.request(urlComponents, method: .post)
  • 类型安全传送
extension User: URLConvertible {static let baseURLString = "https://example.com"func asURL() throws -> URL {let urlString = User.baseURLString + "/users/\(username)/"return try urlString.asURL()}
}let user = User(username: "mattt")
Alamofire.request(user) // https://example.com/users/mattt

2.6 URLRequestConvertible

  • 遵循URLRequestConvertible协议的类型可以被用来构建URL请求。URLRequest默认遵循了URLRequestConvertible,允许被直接传入requestuploaddownload(推荐用这种方法为单个请求自定义请求头)
  • 以一种有意义的方式和web应用程序交互的应用,都鼓励使用自定义的遵循URLRequestConvertible协议的类型来保证请求端点的一致性。这种方法可以用来抽象服务器端的不一致性,并提供类型安全传送,以及管理身份验证凭据和其他状态。
let url = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"let parameters = ["foo": "bar"]do {urlRequest.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {// No-op
}urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")Alamofire.request(urlRequest)
  • API参数抽象
enum Router: URLRequestConvertible {case search(query: String, page: Int)static let baseURLString = "https://example.com"static let perPage = 50// MARK: URLRequestConvertiblefunc asURLRequest() throws -> URLRequest {let result: (path: String, parameters: Parameters) = {switch self {case let .search(query, page) where page > 0:return ("/search", ["q": query, "offset": Router.perPage * page])case let .search(query, _):return ("/search", ["q": query])}}()let url = try Router.baseURLString.asURL()let urlRequest = URLRequest(url: url.appendingPathComponent(result.path))return try URLEncoding.default.encode(urlRequest, with: result.parameters)}
}
Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50

2.7 CRUD和授权

import Alamofireenum Router: URLRequestConvertible {case createUser(parameters: Parameters)case readUser(username: String)case updateUser(username: String, parameters: Parameters)case destroyUser(username: String)static let baseURLString = "https://example.com"var method: HTTPMethod {switch self {case .createUser:return .postcase .readUser:return .getcase .updateUser:return .putcase .destroyUser:return .delete}}var path: String {switch self {case .createUser:return "/users"case .readUser(let username):return "/users/\(username)"case .updateUser(let username, _):return "/users/\(username)"case .destroyUser(let username):return "/users/\(username)"}}// MARK: URLRequestConvertiblefunc asURLRequest() throws -> URLRequest {let url = try Router.baseURLString.asURL()var urlRequest = URLRequest(url: url.appendingPathComponent(path))urlRequest.httpMethod = method.rawValueswitch self {case .createUser(let parameters):urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)case .updateUser(_, let parameters):urlRequest = try URLEncoding.default.encode(urlRequest, with: parameters)default:break}return urlRequest}
}
Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt

2.8 适配和重试请求

  • 现在的大多数Web服务,都需要身份认证。现在比较常见的是OAuth。通常是需要一个access token来授权应用或者用户,然后才可以使用各种支持的Web服务。创建这些access token是比较麻烦的,当access token过期之后就比较麻烦了,我们需要重新创建一个新的。有许多线程安全问题要考虑。

  • RequestAdapterRequestRetrier协议可以让我们更容易地为特定的Web服务创建一个线程安全的认证系统。

2.8.1 RequestAdapter

  • RequestAdapter协议允许每一个SessionManager的Request在创建之前被检查和适配。一个非常特别的使用适配器方法是,在一个特定的认证类型,把Authorization header拼接到请求。
  1. 创建一个AccessTokenAdapter类继承RequestAdapter
class AccessTokenAdapter: RequestAdapter {private let accessToken: Stringinit(accessToken: String) {self.accessToken = accessToken}func adapt(_ urlRequest: URLRequest) throws -> URLRequest {var urlRequest = urlRequestif let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") {urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")}return urlRequest}}
  1. 创建SessionManager,并将AccessTokenAdapter赋值给sessionManager
let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")sessionManager.request("https://httpbin.org/get")

2.8.2 RequestRetrier

  • RequestRetrier协议允许一个在执行过程中遇到error的请求被重试。当一起使用RequestAdapterRequestRetrier协议时,我们可以为OAuth1OAuth2Basic Auth(每次请求API都要提供用户名和密码)甚至是exponential backoff重试策略创建资格恢复系统。下面的例子演示了如何实现一个OAuth2 access token的恢复流程。

实例282

注意:下面代码不是一个全面的OAuth2解决方案。这仅仅是演示如何把RequestAdapter和RequestRetrier协议结合起来创建一个线程安全的恢复系统。
重申: 不要把这个例子复制到实际的开发应用中,这仅仅是一个例子。每个认证系统必须为每个特定的平台和认证类型重新定制。

  1. 创建一个类OAuth2Handler,同时继承:RequestAdapter, RequestRetrier 两个协议
class OAuth2Handler: RequestAdapter, RequestRetrier {private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Voidprivate let sessionManager: SessionManager = {let configuration = URLSessionConfiguration.defaultconfiguration.httpAdditionalHeaders = SessionManager.defaultHTTPHeadersreturn SessionManager(configuration: configuration)}()private let lock = NSLock()private var clientID: Stringprivate var baseURLString: Stringprivate var accessToken: Stringprivate var refreshToken: Stringprivate var isRefreshing = falseprivate var requestsToRetry: [RequestRetryCompletion] = []// MARK: - Initializationpublic init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {self.clientID = clientIDself.baseURLString = baseURLStringself.accessToken = accessTokenself.refreshToken = refreshToken}// MARK: - RequestAdapterfunc adapt(_ urlRequest: URLRequest) throws -> URLRequest {if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {var urlRequest = urlRequesturlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")return urlRequest}return urlRequest}// MARK: - RequestRetrierfunc should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {lock.lock() ; defer { lock.unlock() }if let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 {requestsToRetry.append(completion)if !isRefreshing {refreshTokens { [weak self] succeeded, accessToken, refreshToken inguard let strongSelf = self else { return }strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }if let accessToken = accessToken, let refreshToken = refreshToken {strongSelf.accessToken = accessTokenstrongSelf.refreshToken = refreshToken}strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }strongSelf.requestsToRetry.removeAll()}}} else {completion(false, 0.0)}}// MARK: - Private - Refresh Tokensprivate func refreshTokens(completion: @escaping RefreshCompletion) {guard !isRefreshing else { return }isRefreshing = truelet urlString = "\(baseURLString)/oauth2/token"let parameters: [String: Any] = ["access_token": accessToken,"refresh_token": refreshToken,"client_id": clientID,"grant_type": "refresh_token"]sessionManager.request(urlString, method: .post, parameters: parameters, encoding: JSONEncoding.default).responseJSON { [weak self] response inguard let strongSelf = self else { return }if let json = response.result.value as? [String: Any], let accessToken = json["access_token"] as? String, let refreshToken = json["refresh_token"] as? String {completion(true, accessToken, refreshToken)} else {completion(false, nil, nil)}strongSelf.isRefreshing = false}}
}
  1. 创建sessionManager对象,并将sessionManager.adapter,sessionManager.retrier 都同时指向OAuth2Handler对象。
let baseURLString = "https://some.domain-behind-oauth2.com"let oauthHandler = OAuth2Handler(clientID: "12345678",baseURLString: baseURLString,accessToken: "abcd1234",refreshToken: "ef56789a"
)let sessionManager = SessionManager()
sessionManager.adapter = oauthHandler
sessionManager.retrier = oauthHandlerlet urlString = "\(baseURLString)/some/endpoint"sessionManager.request(urlString).validate().responseJSON { response indebugPrint(response)
}
  • 一旦OAuth2HandlerSessionManager被应用与adapterretrier,他将会通过自动恢复access token来处理一个非法的access token error,并且根据失败的顺序来重试所有失败的请求。(如果需要让他们按照创建的时间顺序来执行,可以使用他们的task identifier来排序)

  • 上面这个例子仅仅检查了401响应码,不是演示如何检查一个非法的access token error。在实际开发应用中,我们想要检查realmwww-authenticate header响应,虽然这取决于OAuth2的实现。

  • 还有一个要重点注意的是,这个认证系统可以在多个session manager之间共享。例如,可以在同一个Web服务集合使用defaultephemeral会话配置。上面这个例子可以在多个session manager间共享一个oauthHandler实例,来管理一个恢复流程。

2.9

3. 序列化

3.1 自定义响应序列化

  • Alamofire为datastringsJSONProperty List提供了内置的响应序列化:
Alamofire.request(...).responseData { (resp: DataResponse<Data>) in ... }
Alamofire.request(...).responseString { (resp: DataResponse<String>) in ... }
Alamofire.request(...).responseJSON { (resp: DataResponse<Any>) in ... }
Alamofire.request(...).responsePropertyList { resp: DataResponse<Any>) in ... }
  • 这些响应包装了反序列化的值(Data, String, Any)或者error (network, validation errors),以及元数据 (URL Request, HTTP headers, status code, metrics, …)。
  • 我们可以有多个方法来自定义所有响应元素:
    • 响应映射
    • 处理错误
    • 创建一个自定义的响应序列化器
    • 泛型响应对象序列化

3.1.1 响应映射

  • 响应映射是自定义响应最简单的方式。它转换响应的值,同时保留最终错误和元数据。例如,我们可以把一个json响应DataResponse转换为一个保存应用模型的的响应,例如DataResponse。使用DataResponse.map来进行响应映射:
Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) inlet userResponse = response.map { json in// We assume an existing User(json: Any) initializerreturn User(json: json)}// Process userResponse, of type DataResponse<User>:if let user = userResponse.value {print("User: { username: \(user.username), name: \(user.name) }")}
}
  • 当转换可能会抛出错误时,使用flatMap方法:
Alamofire.request("https://example.com/users/mattt").responseJSON { response inlet userResponse = response.flatMap { json intry User(json: json)}
}
  • 响应映射非常适合自定义completion handler:
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {return Alamofire.request("https://example.com/users/mattt").responseJSON { response inlet userResponse = response.flatMap { json intry User(json: json)}completionHandler(userResponse)}
}loadUser { response inif let user = userResponse.value {print("User: { username: \(user.username), name: \(user.name) }")}
}
  • 上面代码中loadUser方法被@discardableResult标记,意思是调用loadUser方法可以不接收它的返回值;也可以用_来忽略返回值。
  • 当 map/flatMap 闭包会产生比较大的数据量时,要保证这个闭包在子线程中执行:
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {let utilityQueue = DispatchQueue.global(qos: .utility)return Alamofire.request("https://example.com/users/mattt").responseJSON(queue: utilityQueue) { response inlet userResponse = response.flatMap { json intry User(json: json)}DispatchQueue.main.async {completionHandler(userResponse)}}
}
  • map和flatMap也可以用于下载响应。

3.1.2 处理错误

  • 在实现自定义响应序列化器或者对象序列化方法前,思考如何处理所有可能出现的错误是非常重要的。有两个方法:1)传递未修改的错误,在响应时间处理;2)把所有的错误封装在一个Error类型中。
  • 例如,下面是等会要用用到的后端错误:
enum BackendError: Error {case network(error: Error) // 捕获任何从URLSession API产生的错误case dataSerialization(error: Error)case jsonSerialization(error: Error)case xmlSerialization(error: Error)case objectSerialization(reason: String)
}

3.1.3 创建一个自定义的响应序列化器

  • Alamofire为stringsJSONProperty List提供了内置的响应序列化,但是我们可以通过扩展Alamofire.DataRequest或者Alamofire.DownloadRequest来添加其他序列化。
  • 例如,下面这个例子是一个使用Ono (一个实用的处理iOS和macOS平台的XML和HTML的方式)的响应handler的实现:
extension DataRequest {static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {return DataResponseSerializer { request, response, data, error in// 把任何底层的URLSession error传递给 .network caseguard error == nil else { return .failure(BackendError.network(error: error!)) }// 使用Alamofire已有的数据序列化器来提取数据,error为nil,因为上一行代码已经把不是nil的error过滤了let result = Request.serializeResponseData(response: response, data: data, error: nil)guard case let .success(validData) = result else {return .failure(BackendError.dataSerialization(error: result.error! as! AFError))}do {let xml = try ONOXMLDocument(data: validData)return .success(xml)} catch {return .failure(BackendError.xmlSerialization(error: error))}}}@discardableResultfunc responseXMLDocument(queue: DispatchQueue? = nil,completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)-> Self{return response(queue: queue,responseSerializer: DataRequest.xmlResponseSerializer(),completionHandler: completionHandler)}
}

3.1.4 泛型响应对象序列化

  • 泛型可以用来提供自动的、类型安全的响应对象序列化。

  • 代码1

protocol ResponseObjectSerializable {init?(response: HTTPURLResponse, representation: Any)
}extension DataRequest {func responseObject<T: ResponseObjectSerializable>(queue: DispatchQueue? = nil,completionHandler: @escaping (DataResponse<T>) -> Void)-> Self{let responseSerializer = DataResponseSerializer<T> { request, response, data, error inguard error == nil else { return .failure(BackendError.network(error: error!)) }let jsonResponseSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)let result = jsonResponseSerializer.serializeResponse(request, response, data, nil)guard case let .success(jsonObject) = result else {return .failure(BackendError.jsonSerialization(error: result.error!))}guard let response = response, let responseObject = T(response: response, representation: jsonObject) else {return .failure(BackendError.objectSerialization(reason: "JSON could not be serialized: \(jsonObject)"))}return .success(responseObject)}return response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler)}
}
  • 代码2
struct User: ResponseObjectSerializable, CustomStringConvertible {let username: Stringlet name: Stringvar description: String {return "User: { username: \(username), name: \(name) }"}init?(response: HTTPURLResponse, representation: Any) {guardlet username = response.url?.lastPathComponent,let representation = representation as? [String: Any],let name = representation["name"] as? Stringelse { return nil }self.username = usernameself.name = name}
}
  • 代码3
Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) indebugPrint(response)if let user = response.result.value {print("User: { username: \(user.username), name: \(user.name) }")}
}
  • 代码4
protocol ResponseCollectionSerializable {static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self]
}extension ResponseCollectionSerializable where Self: ResponseObjectSerializable {static func collection(from response: HTTPURLResponse, withRepresentation representation: Any) -> [Self] {var collection: [Self] = []if let representation = representation as? [[String: Any]] {for itemRepresentation in representation {if let item = Self(response: response, representation: itemRepresentation) {collection.append(item)}}}return collection}
}
  • 代码5
extension DataRequest {@discardableResultfunc responseCollection<T: ResponseCollectionSerializable>(queue: DispatchQueue? = nil,completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self{let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error inguard error == nil else { return .failure(BackendError.network(error: error!)) }let jsonSerializer = DataRequest.jsonResponseSerializer(options: .allowFragments)let result = jsonSerializer.serializeResponse(request, response, data, nil)guard case let .success(jsonObject) = result else {return .failure(BackendError.jsonSerialization(error: result.error!))}guard let response = response else {let reason = "Response collection could not be serialized due to nil response."return .failure(BackendError.objectSerialization(reason: reason))}return .success(T.collection(from: response, withRepresentation: jsonObject))}return response(responseSerializer: responseSerializer, completionHandler: completionHandler)}
}
  • 代码6
struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {let username: Stringlet name: Stringvar description: String {return "User: { username: \(username), name: \(name) }"}init?(response: HTTPURLResponse, representation: Any) {guardlet username = response.url?.lastPathComponent,let representation = representation as? [String: Any],let name = representation["name"] as? Stringelse { return nil }self.username = usernameself.name = name}
}
  • 代码7
Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) indebugPrint(response)if let users = response.result.value {users.forEach { print("- \($0)") }}
}

4. 安全

  • 对于安全敏感的数据来说,在与服务器和web服务交互时使用安全的HTTPS连接是非常重要的一步。默认情况下,Alamofire会使用苹果安全框架内置的验证方法来评估服务器提供的证书链。虽然保证了证书链是有效的,但是不能防止man-in-the-middle (MITM)攻击或者其他潜在的漏洞。为了减少MITM攻击,处理用户的敏感数据或财务信息的应用,应该使用ServerTrustPolicy提供的certificate或者public key pinning

4. 1 ServerTrustPolicy

  • 在通过HTTPS安全连接连接到服务器时,ServerTrustPolicy枚举通常会评估URLAuthenticationChallenge提供的server trust。
let serverTrustPolicy = ServerTrustPolicy.pinCertificates(certificates: ServerTrustPolicy.certificates(),validateCertificateChain: true,validateHost: true
)
  • 在验证的过程中,有多种方法可以让我们完全控制server trust的评估:
属性 作用
performDefaultEvaluation 使用默认的server trust评估,允许我们控制是否验证challenge提供的host。
pinCertificates 使用pinned certificates来验证server trust。如果pinned certificates匹配其中一个服务器证书,那么认为server trust是有效的。
pinPublicKeys 使用pinned public keys来验证server trust。如果pinned public keys匹配其中一个服务器证书公钥,那么认为server trust是有效的。
disableEvaluation 禁用所有评估,总是认为server trust是有效的。
customEvaluation 使用相关的闭包来评估server trust的有效性,我们可以完全控制整个验证过程。但是要谨慎使用。

4. 2 ServerTrustPolicyManager(服务器信任策略管理者 )

  • ServerTrustPolicyManager负责存储一个内部的服务器信任策略到特定主机的映射。这样Alamofire就可以评估每个主机不同服务器信任策略。
let serverTrustPolicies: [String: ServerTrustPolicy] = ["test.example.com": .pinCertificates(certificates: ServerTrustPolicy.certificates(),validateCertificateChain: true,validateHost: true),"insecure.expired-apis.com": .disableEvaluation
]let sessionManager = SessionManager(serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies)
)
  • 要确保有一个强引用引用着SessionManager实例,否则当sessionManager被销毁时,请求将会取消。
  • 这些服务器信任策略将会形成下面的结果:
  1. test.example.com:始终使用证书链固定的证书和启用主机验证,因此需要以下条件才能是TLS握手成功:
    (1) 证书链必须是有效的。
    (2) 证书链必须包含一个已经固定的证书。
    (3) Challenge主机必须匹配主机证书链的子证书。
  2. insecure.expired-apis.com:将从不评估证书链,并且总是允许TLS握手成功。
  3. 其他主机将会默认使用苹果提供的验证。

4. 3 子类化服务器信任策略管理者

  • 如果我们需要一个更灵活的服务器信任策略来匹配其他行为(例如通配符域名),可以子类化ServerTrustPolicyManager,并且重写serverTrustPolicyForHost方法。
class CustomServerTrustPolicyManager: ServerTrustPolicyManager {override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {var policy: ServerTrustPolicy?// Implement your custom domain matching behavior...return policy}
}

4. 4 验证主机

  • .performDefaultEvaluation.pinCertificates.pinPublicKeys这三个服务器信任策略都带有一个validateHost参数。把这个值设为true,服务器信任评估就会验证与challenge主机名字匹配的在证书里面的主机名字。如果他们不匹配,验证失败。如果设置为false,仍然会评估整个证书链,但是不会验证子证书的主机名字。
  • 建议在实际开发中,把validateHost设置为true

4. 5 验证证书链

  • Pinning certificatepublic keys 都可以通过validateCertificateChain参数拥有验证证书链的选项。把它设置为true,除了对Pinning certificatepublic keys进行字节相等检查外,还将会验证整个证书链。如果是false,将会跳过证书链验证,但还会进行字节相等检查。
  • 还有很多情况会导致禁用证书链认证。最常用的方式就是自签名和过期的证书。在这些情况下,验证始终会失败。但是字节相等检查会保证我们从服务器接收到证书。
  • 建议在实际开发中,把validateCertificateChain设置为true

4. 6 ATS 应用传输安全 (App Transport Security)

  • 从iOS9开始,就添加了App Transport Security (ATS),使用ServerTrustPolicyManager和多个ServerTrustPolicy对象可能没什么影响。如果我们不断看到CFNetwork SSLHandshake failed (-9806)错误,我们可能遇到了这个问题。苹果的ATS系统重写了整个challenge系统,除非我们在plist文件中配置ATS设置来允许应用评估服务器信任。
  • plist文件设置如下:
<dict><key>NSAppTransportSecurity</key><dict><key>NSExceptionDomains</key><dict><key>example.com</key><dict><key>NSExceptionAllowsInsecureHTTPLoads</key><true/><key>NSExceptionRequiresForwardSecrecy</key><false/><key>NSIncludesSubdomains</key><true/><!-- 可选的: 指定TLS的最小版本 --><key>NSTemporaryExceptionMinimumTLSVersion</key><string>TLSv1.2</string></dict></dict></dict>
</dict>
  • 是否需要把NSExceptionRequiresForwardSecrecy设置为NO取决于TLS连接是否使用一个允许的密码套件。在某些情况下,它需要设置为NO。NSExceptionAllowsInsecureHTTPLoads必须设置为YES,然后SessionDelegate才能接收到challenge回调。一旦challenge回调被调用,ServerTrustPolicyManager将接管服务器信任评估。如果我们要连接到一个仅支持小于1.2版本的TSL主机,那么还要指定NSTemporaryExceptionMinimumTLSVersion
  • 在实际开发中,建议始终使用有效的证书。

5. 网络可达性

5. 1 Network Reachability

  • NetworkReachabilityManager监听WWAN和WiFi网络接口和主机地址的可达性变化。
let manager = NetworkReachabilityManager(host: "www.apple.com")
manager?.listener = { status inprint("Network Status Changed: \(status)")
}
manager?.startListening()
  • 要确保manager被强引用,否则会接收不到状态变化。另外,在主机字符串中不要包含scheme,也就是说要把https://去掉,否则无法监听。
  • 当使用网络可达性来决定接下来要做什么时,有以下几点需要重点注意的:
    • 不要使用Reachability来决定是否发送一个网络请求,我们必须要发送请求。
    • 当Reachability恢复了,要重试网络请求。即使网络请求失败,在这个时候也非常适合重试请求。
    • 网络可达性的状态非常适合用来决定为什么网络请求会失败。如果一个请求失败,应该告诉用户是离线导致请求失败的,而不是技术错误,例如请求超时。

Alamofire(四)怎么合理使用Alamofire相关推荐

  1. Alamofire初探

    上一篇文章介绍了AFNetworking网络库,作为它的姊妹Alamofire,不得不介绍下. 今天我们就来介绍Alamofire的使用. 前言 Alamofire是使用Swift语言开发的网络库,它 ...

  2. Swift基础之使用Alamofire库进行网络请求和断点下载

    好久没有写过Swift相关的文章博客了,这里我就展示一下关于使用Alamofire库的方法 1.什么是Alamofire (1)Alamofire 的前身是 AFNetworking.AFNetwor ...

  3. Swift - 添加纯净的Alamofire

    Swift - 添加纯净的Alamofire 如果你有代码洁癖,不能容忍任何多余的东西,请继续往下看.  1. 下载Alamofire (https://github.com/Alamofire/Al ...

  4. Alamofire使用进阶

    高级使用说明 Alamofire是建立在 URLSession 和 底层 URL Loading System上的,为了充分利用这个库,建议熟悉底层网络堆栈的概念和功能 推荐阅读 URL Loadin ...

  5. Swift 使用Alamofire监听网络状态

    不用多说,第一步pod Alamofire,然后import Alamofire 第二步,如下代码 import UIKit import Alamofireenum ReachabilityStat ...

  6. 6、Xcode导入第三方库(Alamofire)的方法

    1.首先下载Alamofire库,地址为:https://github.com/Alamofire/Alamofire 2.将下载好的Alamofire-master整个目录拷贝到自己的工程中. 3. ...

  7. Xcode导入第三方库(Alamofire)的方法 (出现错误:No Such module Alamofire)

    1.首先下载Alamofire库,地址为:https://github.com/Alamofire/Alamofire 2.将下载好的Alamofire-master整个目录拷贝到自己的工程中. 3. ...

  8. IOS Swift 5.0 Alamofire的简单使用

    Alamofire 可以通过 CocoaPods 导入,也可以下载导入,这里是初学, CocoaPods 还不太熟悉,就直接下载导入使用了 1.Alamofire 下载 2.Alamofire 导入 ...

  9. 告别2016迎接2017,分享一些第三方插件

    告别2016迎接2017,分享一些第三方插件 2016-12-30 00:50 编辑: 踏浪帅 分类:iOS开发 来源:踏浪帅投稿 35 2166 插件第三方插件 招聘信息: UI设计 app推广与运 ...

最新文章

  1. 关于haproxy的重定向
  2. Sharding-JDBC水平分表(最终测试)_Sharding-Sphere,Sharding-JDBC分布式_分库分表工作笔记009
  3. 原型设计应当掌握的四个设计思维:初始、常态、边界、错误
  4. 某些网站加载的zul文件和zk文件是什么东西
  5. SVM实现多分类的三种方案
  6. 微信小程序中-[渲染层网络层错误] pages/card/card.wxss 中的本地资源图片无法通过 WXSS 获取-解决办法
  7. 雪亮工程建设标准_2018年雪亮工程建设方案
  8. 《大秦帝国之崛起》看后感
  9. 共享单车的扫码解锁原理是什么?2222
  10. 2021年安全员-C证(陕西省)考试资料及安全员-C证(陕西省)免费试题
  11. 零知识证明 Zero Knowledge Proof 以及 Layer2、跨链介绍
  12. crc 校验错误_信道编码之循环冗余校验(CRC)
  13. 用python编写AutoCAD插件日志
  14. oracle使用text类型,oracle数据库中text类型用什么类型表示
  15. NLP笔记:常用激活函数考察整理
  16. 强制推到远程git push
  17. 你很牛,且是刚毕业的,那就到华为上班吧!--绝对隐私:华为员工待遇全面揭秘...
  18. 【数论】最大公约数、约数的个数与约数之和定理
  19. 读《移山之道》——问渠哪得清如许,为有源头活水来
  20. 新手入门:你适合做程序员吗?

热门文章

  1. python入门—如何运行python程序
  2. ThinkPHP实现支付宝接口功能 代码实例
  3. 闲来无事系列-雨课堂抓包发包玩法(娱乐向)
  4. html 无效,如何让css失效?
  5. mysql字符串包含insert_字符串中包含关键字,insert into不成功
  6. arduinohanshu_Arduino 函数的位操作
  7. 用Python的Flask框架写微信小程序及其管理网页后台(准备篇)
  8. SV学习笔记—类型转换
  9. 215. 数组中的第 K个最大元素
  10. 实战python设计模式_实战Python设计模式:可复用面向对象软件开发实践