成长路上

不断放弃舒适区


  • Home

  • About

  • Tags

  • Categories

  • Archives

  • Search

Alamofire-首页

Posted on 2018-10-14 | In swift源码学习

Alamofire是一个用Swift编写的HTTP网络库。

  • 特征
  • 组件库
  • 要求
  • 迁移指南
  • 通讯
  • 安装
  • 用法
    • 简介 - 发出请求,响应处理,响应验证,响应缓存
    • HTTP - HTTP方法,参数编码,HTTP标头,身份验证
    • 大数据 - 将数据下载到文件,将数据上传到服务器
    • 工具 - 统计指标,cURL命令输出
  • 高级用法
    • URL会话 - 会话管理器,会话代理,请求
    • 路由 - 路由请求,调整和重试请求
    • 模型对象 - 自定义响应序列化
    • 连接 - 安全性,网络可达性
  • 打开雷达
  • 常问问题
  • 积分
  • 捐赠
  • 执照

特征

  • 可链接的请求/响应方法
  • URL / JSON / plist参数编码
  • 上传文件/数据/流/ MultipartFormData
  • 使用请求或恢复数据下载文件
  • 使用URLCredential进行身份验证
  • HTTP响应验证
  • 上传和下载进度闭包与进度
  • cURL命令输出
  • 动态调整和重试请求
  • TLS证书和公钥固定
  • 网络可达性
  • 综合单元和集成测试覆盖范围
  • 完整文档

组件库

为了使Alamofire专注于核心网络实施,Alamofire软件基金会创建了额外的组件库,为Alamofire生态系统带来了额外的功能。

  • AlamofireImage - 包含图像响应序列化器UIImage和UIImageView扩展,自定义图像过滤器,自动清除内存缓存和基于优先级的图像下载系统的图像库。
  • AlamofireNetworkActivityIndicator - 使用Alamofire控制iOS上网络活动指示器的可见性。它包含可配置的延迟计时器,有助于缓解闪烁,并且可以支持URLSession非Alamofire管理的实例。

要求

  • iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
  • Xcode 8.3+
  • Swift 3.1+

迁移指南

  • Alamofire 4.0迁移指南
  • Alamofire 3.0迁移指南
  • Alamofire 2.0迁移指南

通讯

  • 如果您需要有关网络请求的帮助,请使用Stack Overflow和tag alamofire。
  • 如果您需要查找或了解API,请查看我们的文档或Apple的文档URLSession,在其上构建Alamofire。
  • 如果您需要有关Alamofire功能的帮助,请使用swift.org上的论坛。
  • 如果您想讨论Alamofire最佳实践,请在swift.org上使用我们的论坛。
  • 如果您想讨论功能请求,请使用swift.org上的论坛。
  • 如果您发现了错误,请打开问题并按照指南操作。越详细越好!
  • 如果您想贡献,请提交拉取请求。

安装

的CocoaPods

CocoaPods是Cocoa项目的依赖管理器。您可以使用以下命令安装它:

1
$ gem install cocoapods

构建Alamofire 4.0+需要CocoaPods 1.1+。

要使用CocoaPods将Alamofire集成到您的Xcode项目中,请在以下位置指定Podfile:

1
2
3
4
5
6
7
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '<Your Target Name>' do
pod 'Alamofire', '~> 4.7'
end

然后,运行以下命令:

1
$ pod install

迦太基

Carthage是一个分散的依赖管理器,它构建您的依赖项并为您提供二进制框架。

您可以使用以下命令安装Carthage with Homebrew:

1
2
$ brew update
$ brew install carthage

要使用Carthage将Alamofire集成到您的Xcode项目中,请在以下位置指定Cartfile:

1
github "Alamofire/Alamofire" ~> 4.7

运行carthage update以构建框架并将内置拖动Alamofire.framework到Xcode项目中。

Swift包管理器

在斯威夫特软件包管理器是自动化的银行代码分配的工具,被集成到swift编译器。它处于早期开发阶段,但Alamofire支持在支持的平台上使用它。

一旦你设置了Swift软件包,添加Alamofire作为依赖项就像添加它的dependencies价值一样简单Package.swift。

Swift 3

1
2
3
dependencies: [
.Package(url: "https://github.com/Alamofire/Alamofire.git", majorVersion: 4)
]

Swift 4

1
2
3
dependencies: [
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "4.0.0")
]

手动

如果您不想使用上述任何依赖管理器,可以手动将Alamofire集成到您的项目中。

嵌入式框架

  • 打开Terminal,cd进入您的顶级项目目录,然后运行以下命令“if”您的项目未初始化为git存储库:

    1
    $ git init
  • 通过运行以下命令将Alamofire添加为git 子模块:

    1
    $ git submodule add https://github.com/Alamofire/Alamofire.git
  • 打开新Alamofire文件夹,然后将其拖到Alamofire.xcodeproj应用程序Xcode项目的Project Navigator中。

    它应该嵌套在应用程序的蓝色项目图标下面。无论是高于还是低于所有其他Xcode组并不重要。

  • Alamofire.xcodeproj在Project Navigator中选择并验证部署目标是否与应用程序目标的目标匹配。

  • 接下来,在Project Navigator中选择您的应用程序项目(蓝色项目图标)以导航到目标配置窗口,并在侧栏中的“Targets”标题下选择应用程序目标。

  • 在该窗口顶部的标签栏中,打开“常规”面板。

  • 单击+“嵌入式二进制文件”部分下的按钮。

  • 您将看到两个不同的Alamofire.xcodeproj文件夹,每个文件夹都有两个不同版本的Alamofire.framework嵌套在Products文件夹中。

    Products您选择哪个文件夹无关紧要,但无论您选择顶部还是底部都无关紧要Alamofire.framework。

  • 选择Alamofire.frameworkiOS 的顶部,OS X的底部。

    您可以通过检查项目的构建日志来验证您选择的那个。为构建目标Alamofire将被列为要么Alamofire iOS,Alamofire macOS,Alamofire tvOS或Alamofire watchOS。

  • 就是这样!

    该Alamofire.framework是自动添加为目标的依赖,链接框架和嵌入式框架,在文件拷贝建造阶段这是所有你需要建立在模拟器和设备。

打开雷达

以下雷达对Alamofire的当前实施有一定影响。

  • rdar://21349340 - 由于测试用例中的免费桥接问题,编译器抛出警告
  • rdar://26870455 - 后台URL会话配置在模拟器中不起作用
  • rdar://26849668 - 某些URLProtocol API无法正确处理 URLRequest
  • rdar://36082113- URLSessionTaskMetrics未能在watchOS 3.0+上链接

已解决的雷达

在针对Alamofire项目提出申请后,以下雷达已经解决了。

  • rdar://26761490 - Swift字符串插值导致内存泄漏,常见用法(在Xcode 9 beta 6中于9/1/17解决)。

常问问题

Alamofire这个名字的起源是什么?

Alamofire以Alamo Fire花命名,Alamo Fire花是得克萨斯州官方州花Bluebonnet的混合变体。

路由器与请求适配器中的逻辑属于什么?

简单的静态数据(如路径,参数和公共头)属于Router。动态数据,例如Authorization其值可以根据认证系统而改变的标题属于a RequestAdapter。

动态数据必须放入的原因RequestAdapter是支持重试操作。当一个Request重试,原来的请求不被重建意味着Router不会再次调用。在RequestAdapter被称为再次允许动态数据上重试前的原始请求更新Request。

积分

Alamofire由Alamofire Software Foundation拥有和维护。您可以在Twitter上关注他们@AlamofireSF以获取项目更新和发布。

安全披露

如果您认为自己发现了Alamofire的安全漏洞,则应尽快通过电子邮件向security@alamofire.org报告。请不要将其发布到公共问题跟踪器。

捐赠

该ASF正在筹钱留正式注册为联邦非营利组织。注册将允许我们的成员获得一些法律保护,并允许我们捐赠使用,免税。捐赠给ASF将使我们能够:

  • 支付我们的年度法律费用,以保持非营利组织的良好状态
  • 支付我们的邮件服务器,以帮助我们掌握所有问题和安全问题
  • 可能为测试服务器提供资金,以便我们更轻松地测试边缘情况
  • 可能资助开发人员全职工作我们的一个项目

社区对ASF库的采用令人惊叹。我们对您对项目的热情非常谦卑,并希望继续尽我们所能推动未来发展。在您的不断支持下,ASF将能够改善其覆盖范围,并为核心成员提供更好的法律保障。如果您使用我们的任何图书馆工作,请查看您的雇主是否有兴趣捐赠。您今天捐赠的任何金额都可以帮助我们实现目标,我们将不胜感激。

贝宝

执照

Alamofire根据MIT许可证发布。有关详细信息,请参阅LICENSE。

Alamofire-用法

Posted on 2018-10-14 | In swift源码学习

用法

发出请求

1
2
3
import Alamofire

Alamofire.request("https://httpbin.org/get")

响应处理

处理Alamofire中Response的一个Request制作涉及将响应处理程序链接到Request。

1
2
3
4
5
6
7
8
9
10
11
12
13
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print("Request: \(String(describing: response.request))") // original url request
print("Response: \(String(describing: response.response))") // http url response
print("Result: \(response.result)") // response serialization result

if let json = response.result.value {
print("JSON: \(json)") // serialized json response
}

if let data = response.data, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)") // original server data as UTF8 string
}
}

在上面的示例中,responseJSON处理程序附加到完成Request后要执行的处理程序Request。不是阻止执行以等待来自服务器的响应,而是指定闭包形式的回调以在收到响应后处理响应。请求的结果仅在响应闭包的范围内可用。根据响应或从服务器接收的数据执行的任何执行都必须在响应闭包内完成。

Alamofire中的网络是异步完成的。对于不熟悉这个概念的程序员来说,异步编程可能会让人感到沮丧,但这样做有很好的理由。

Alamofire默认包含五种不同的响应处理程序,包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//响应处理程序 - 反序列化响应
func response(
queue: DispatchQueue?,
completionHandler: @escaping (DefaultDataResponse) -> Void)
-> Self

//响应数据处理程序 - 序列化为数据
func responseData(
queue:DispatchQueue ? ,
completionHandler:@escaping(DataResponse <Data>)- > Void)
- > Self

//响应字符串处理程序 - 序列化为字符串
func responseString(
queue:DispatchQueue ? ,
encoding:String .Encoding ?,
completionHandler:@escaping(DataResponse < String >)- > Void)
- > Self

//响应JSON处理程序 - 序列化为任意
func responseJSON(
queue:DispatchQueue ? ,
completionHandler:@escaping(DataResponse < Any >)- > Void)
- > Self

//响应PropertyList(plist)处理程序 - 序列化为Any
func responsePropertyList(
queue:DispatchQueue ? ,
completionHandler:@escaping(DataResponse < Any >)- > Void))
- > Self

没有任何响应处理程序执行HTTPURLResponse从服务器返回的任何验证。

例如,400..<500和500..<600范围中的响应状态代码不会自动触发Error。Alamofire使用响应验证方法链接来实现此目的。

响应处理程序

该response处理程序不评价任何响应数据的。它只是直接从URL会话委托转发所有信息。这是Alamofire相当于cURL用来执行一个Request。

1
2
3
4
5
6
7
8
9
Alamofire.request("https://httpbin.org/get").response { response in
print("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)")
}
}

我们强烈建议您利用其他响应序列化程序来利用Response和Result类型。

响应数据处理程序

该responseData处理程序使用responseDataSerializer(服务器数据序列化到一些其它类型的对象),以提取Data由服务器返回。如果没有错误发生并Data返回,则响应Result将是a .success,并且value将是类型Data。

1
2
3
4
5
6
7
Alamofire.request("https://httpbin.org/get").responseData { response in
debugPrint("All Response Info: \(response)")

if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) {
print("Data: \(utf8Text)")
}
}

响应字符串处理程序

该responseString处理器采用了responseStringSerializer转换的Data服务器返回到String与指定的编码。如果没有发生错误并且服务器数据成功序列化为a String,则响应Result将为a .success且value类型为String。

1
2
3
4
Alamofire.request("https://httpbin.org/get").responseString { response in
print("Success: \(response.result.isSuccess)")
print("Response String: \(response.result.value)")
}

如果未指定编码,Alamofire将使用HTTPURLResponse服务器中指定的文本编码。如果服务器响应无法确定文本编码,则默认为.isoLatin1。

响应JSON处理程序

该responseJSON处理器采用了responseJSONSerializer转换的Data服务器返回到Any使用指定的类型JSONSerialization.ReadingOptions。如果没有发生错误并且服务器数据成功序列化为JSON对象,则响应Result将为a .success且value类型为Any。

1
2
3
4
5
6
7
Alamofire.request("https://httpbin.org/get").responseJSON { response in
debugPrint(response)

if let json = response.result.value {
print("JSON: \(json)")
}
}

所有JSON序列化都由框架中的JSONSerializationAPI 处理Foundation。

链式响应处理程序

响应处理程序甚至可以链接:

1
2
3
4
5
6
7
Alamofire.request("https://httpbin.org/get")
.responseString { response in
print("Response String: \(response.result.value)")
}
.responseJSON { response in
print("Response JSON: \(response.result.value)")
}

重要的是要注意,在同一个上使用多个响应处理程序Request需要多次序列化服务器数据。每个响应处理程序一次。

响应处理程序队列

默认情况下,响应处理程序在主调度队列上执行。但是,可以提供自定义调度队列。

1
2
3
4
5
let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
print("Executing response handler on utility queue")
}

响应验证

默认情况下,无论响应的内容如何,Alamofire都会将任何已完成的请求视为成功。validate如果响应具有不可接受的状态代码或MIME类型,则在响应处理程序之前调用会导致生成错误。

手动验证

1
2
3
4
5
6
7
8
9
10
11
Alamofire.request("https://httpbin.org/get")
.validate(statusCode: 200..<300)
.validate(contentType: ["application/json"])
.responseData { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}

自动验证

自动验证200..<300范围内的状态代码,并且Content-Type响应的Accept标头与请求的标头匹配(如果提供了标头)。

1
2
3
4
5
6
7
8
Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
switch response.result {
case .success:
print("Validation Successful")
case .failure(let error):
print(error)
}
}

响应缓存

响应缓存在系统框架级别上处理URLCache。它提供了复合内存和磁盘缓存,并允许您操作内存和磁盘部分的大小。

默认情况下,Alamofire利用共享URLCache。要自定义它,请参阅“ 会话管理器配置”部分。

HTTP方法

该HTTPMethod枚举列出定义的HTTP方法RFC 7231第4.3节:

1
2
3
4
5
6
7
8
9
10
11
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"
}

这些值可以作为method参数传递给Alamofire.requestAPI:

1
2
3
4
5
Alamofire.request("https://httpbin.org/get") // method defaults to `.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)

所述Alamofire.request方法参数的缺省值为.get。

参数编码

Alamofire支持三种类型的参数编码,包括:URL,JSON和PropertyList。它还可以支持任何符合ParameterEncoding协议的自定义编码。

网址编码

该URLEncoding类型创建一个URL编码的查询字符串,该字符串将被设置为或附加到任何现有的URL查询字符串,或者设置为URL请求的HTTP正文。查询字符串是设置还是附加到任何现有URL查询字符串或设置为HTTP正文取决于Destination编码。该Destination枚举有三种情况:

  • .methodDependent- 将编码的查询字符串结果应用于现有查询字符串GET,HEAD并将DELETE请求和集合应用为使用任何其他HTTP方法的请求的HTTP正文。
  • .queryString - 将编码查询字符串结果设置或附加到现有查询字符串。
  • .httpBody - 将编码的查询字符串结果设置为URL请求的HTTP正文。

Content-TypeHTTP主体的编码请求的HTTP头字段设置为application/x-www-form-urlencoded; charset=utf-8。由于没有关于如何编码集合类型的已发布规范,因此附加[]了数组值(foo[]=1&foo[]=2)的键,以及为嵌套字典值(foo[bar]=baz)附加方括号括起来的键的约定。

使用URL编码参数获取请求
1
2
3
4
5
6
7
8
let parameters: Parameters = ["foo": "bar"]

// All three of these calls are equivalent
Alamofire.request("https://httpbin.org/get", parameters: parameters) // encoding defaults to `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请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let parameters: Parameters = [
"foo": "bar",
"baz": ["a", 1],
"qux": [
"x": 1,
"y": 2,
"z": 3
]
]

// All three of these calls are equivalent
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枚举提供了编码以下方法Bool参数:

  • .numeric-编码true作为1和false作为0。
  • .literal- 编码true和false字符串文字。

默认情况下,Alamofire使用.numeric编码。

您可以创建自己的URLEncoding并Bool在初始化程序中指定所需的编码:

1
let encoding = URLEncoding(boolEncoding: .literal)
配置Array参数编码

该URLEncoding.ArrayEncoding枚举提供了编码以下方法Array参数:

  • .brackets - 每个值的键都附加一组空方括号。
  • .noBrackets - 不附加括号。密钥按原样编码。

默认情况下,Alamofire使用.brackets编码,foo=[1,2]编码为foo[]=1&foo[]=2。

使用.noBrackets编码将编码foo=[1,2]为foo=1&foo=2。

您可以创建自己的URLEncoding并Array在初始化程序中指定所需的编码:

1
let encoding = URLEncoding(arrayEncoding: .noBrackets)

JSON编码

该JSONEncoding类型创建参数对象的JSON表示,该表示被设置为请求的HTTP主体。Content-Type编码请求的HTTP头字段设置为application/json。

使用JSON编码参数的POST请求
1
2
3
4
5
6
7
8
9
10
11
12
let parameters: Parameters = [
"foo": [1,2,3],
"bar": [
"baz": "qux"
]
]

// Both calls are equivalent
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"}}

Plist编码

的PropertyListEncoding用途PropertyListSerialization来创建参数对象的plist表示时,根据相关联的格式,并写入选项的值,其被设置为请求的主体。Content-Type编码请求的HTTP头字段设置为application/x-plist。

自定义编码

如果提供的ParameterEncoding类型不符合您的需求,您可以创建自己的自定义编码。这是一个快速示例,说明如何构建自定义JSONStringArrayEncoding类型以将JSON字符串数组编码到Request。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct JSONStringArrayEncoding: ParameterEncoding {
private let array: [String]

init(array: [String]) {
self.array = array
}

func encode(_ urlRequest: URLRequestConvertible, with parameters: Parameters?) throws -> URLRequest {
var urlRequest = try urlRequest.asURLRequest()

let data = try JSONSerialization.data(withJSONObject: array, options: [])

if urlRequest.value(forHTTPHeaderField: "Content-Type") == nil {
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
}

urlRequest.httpBody = data

return urlRequest
}
}

URLRequest的手动参数编码

该ParameterEncodingAPI可让网络请求之外使用。

1
2
3
4
5
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)

HTTP头部

Request在global request方法中直接支持向a添加自定义HTTP标头。这样可以轻松地将HTTP标头附加到Request可以不断更改的标头上。

1
2
3
4
5
6
7
8
let headers: HTTPHeaders = [
"Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
"Accept": "application/json"
]

Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
debugPrint(response)
}

对于不更改的HTTP标头,建议将它们设置为,URLSessionConfiguration以便它们自动应用于URLSessionTask底层创建的任何标头URLSession。有关更多信息,请参阅会话管理器配置部分。

默认的Alamofire SessionManager为每个提供默认的标头集Request。这些包括:

  • Accept-Encoding,默认为gzip;q=1.0, compress;q=0.5,每RFC 7230§4.2.3。
  • Accept-Language,缺省到顶部6偏好的语言在系统上,格式化等en;q=1.0,每RFC 7231§5.3.5。
  • User-Agent,其中包含有关当前应用程序的版本信息。例如:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0,根据RFC7231§5.5.3。

如果需要自定义这些标头,URLSessionConfiguration则应创建自定义,defaultHTTPHeaders更新属性并将配置应用于新SessionManager实例。

认证

身份验证在系统框架级别由URLCredential和处理URLAuthenticationChallenge。

支持的身份验证方案

  • HTTP基础
  • HTTP摘要
  • Kerberos的
  • NTLM

HTTP基本身份验证

在authenticate上一个方法Request自动将提供URLCredential给一个URLAuthenticationChallenge适当的时候:

1
2
3
4
5
6
7
8
let user = "user"
let password = "password"

Alamofire.request("https://httpbin.org/basic-auth/\(user)/\(password)")
.authenticate(user: user, password: password)
.responseJSON { response in
debugPrint(response)
}

根据您的服务器实现,Authorization标头也可能是合适的:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 in
debugPrint(response)
}

使用URLCredential进行身份验证

1
2
3
4
5
6
7
8
9
10
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 in
debugPrint(response)
}

值得注意的是,当使用URLCredentialfor进行身份验证时,URLSession如果服务器发出质询,则底层实际上最终会发出两个请求。第一个请求将不包括“可能”从服务器触发质询的凭证。然后,Alamofire接收挑战,附加凭证并由底层重试请求URLSession。

将数据下载到文件

在Alamofire中从服务器获取数据的请求可以下载内存中或磁盘上的数据。Alamofire.request到目前为止,所有示例中使用的API始终在内存中下载服务器数据。这对于较小的有效负载来说非常有用,因为它更有效,但对于较大的有效负载却非常糟糕,因为下载可以使整个应用程序运行在内存之外。因此,您还可以使用Alamofire.downloadAPI将服务器数据下载到磁盘上的临时文件。

这只能按现状运行macOS。其他平台不允许访问应用程序沙箱之外的文件系统。要在其他平台上下载文件,请参阅“ 下载文件目标”部分。

1
2
3
4
5
Alamofire.download("https://httpbin.org/image/png").responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}

该Alamofire.downloadAPI还应该如果你需要下载数据,同时您的应用程序在后台使用。有关更多信息,请参阅会话管理器配置部分。

下载文件目的地

您还可以提供DownloadFileDestination闭包以将文件从临时目录移动到最终目标。在临时文件实际移动到之前destinationURL,DownloadOptions将执行闭包中指定的内容。目前支持的两个DownloadOptions是:

  • .createIntermediateDirectories - 如果指定,则为目标URL创建中间目录。
  • .removePreviousFile - 如果指定,则从目标URL中删除以前的文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("pig.png")

return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

Alamofire.download(urlString, to: destination).response { response in
print(response)

if response.error == nil, let imagePath = response.destinationURL?.path {
let image = UIImage(contentsOfFile: imagePath)
}
}

您还可以使用建议的下载目标API。

1
2
let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)
Alamofire.download("https://httpbin.org/image/png", to: destination)

下载进度

很多时候向用户报告下载进度会很有帮助。任何人DownloadRequest都可以使用downloadProgressAPI 报告下载进度。

1
2
3
4
5
6
7
8
9
Alamofire.download("https://httpbin.org/image/png")
.downloadProgress { progress in
print("Download Progress: \(progress.fractionCompleted)")
}
.responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}

该downloadProgressAPI也需要queue定义哪个参数DispatchQueue的下载进度闭合应该叫上。

1
2
3
4
5
6
7
8
9
10
11
let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.download("https://httpbin.org/image/png")
.downloadProgress(queue: utilityQueue) { progress in
print("Download Progress: \(progress.fractionCompleted)")
}
.responseData { response in
if let data = response.result.value {
let image = UIImage(data: data)
}
}

恢复下载

如果a DownloadRequest被取消或中断,则基础URL会话可以生成活动的恢复数据DownloadRequest。如果发生这种情况,可以重新使用恢复数据重新启动DownloadRequest它停止的位置。可以通过下载响应访问恢复数据,然后在尝试重新启动请求时重用。

重要提示:在所有Apple平台的某些版本(iOS 10 - 10.2,macOS 10.12 - 10.12.2,tvOS 10 - 10.1,watchOS 3 - 3.1.1)resumeData上,后台URL会话配置中断。在resumeData生成逻辑中存在一个潜在的错误,其中数据写错了,并且总是无法恢复下载。有关错误和可能的解决方法的更多信息,请参阅此Stack Overflow帖子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 = { _, _ in
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
let fileURL = documentsURL.appendingPathComponent("pig.png")

return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}

let request: DownloadRequest

if let resumeData = resumeData {
request = Alamofire.download(resumingWith: resumeData)
} else {
request = Alamofire.download("https://httpbin.org/image/png")
}

request.responseData { response in
switch response.result {
case .success(let data):
self.image = UIImage(data: data)
case .failure:
self.resumeData = response.resumeData
}
}
}
}

将数据上传到服务器

使用JSON或URL编码参数向服务器发送相对少量的数据时,Alamofire.requestAPI通常就足够了。如果您需要从文件URL或文件URL发送更大量的数据InputStream,那么Alamofire.uploadAPI就是您要使用的。

该Alamofire.uploadAPI还应该如果你需要上传的数据,同时您的应用程序在后台使用。有关更多信息,请参阅会话管理器配置部分。

上传数据

1
2
3
4
5
let imageData = UIImagePNGRepresentation(image)!

Alamofire.upload(imageData, to: "https://httpbin.org/post").responseJSON { response in
debugPrint(response)
}

上传文件

1
2
3
4
5
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
debugPrint(response)
}

上传多部分表单数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Alamofire.upload(
multipartFormData: { multipartFormData in
multipartFormData.append(unicornImageURL, withName: "unicorn")
multipartFormData.append(rainbowImageURL, withName: "rainbow")
},
to: "https://httpbin.org/post",
encodingCompletion: { encodingResult in
switch encodingResult {
case .success(let upload, _, _):
upload.responseJSON { response in
debugPrint(response)
}
case .failure(let encodingError):
print(encodingError)
}
}
)

上传进度

当您的用户等待上传完成时,有时可以方便地向用户显示上传的进度。任何人UploadRequest都可以使用uploadProgress和downloadProgressAPI 报告上传进度和响应数据的下载进度。

1
2
3
4
5
6
7
8
9
10
11
12
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post")
.uploadProgress { progress in // main queue by default
print("Upload Progress: \(progress.fractionCompleted)")
}
.downloadProgress { progress in // main queue by default
print("Download Progress: \(progress.fractionCompleted)")
}
.responseJSON { response in
debugPrint(response)
}

统计指标

时间线

Alamofire在a的整个生命周期中收集时间,Request并创建Timeline在所有响应类型上作为属性公开的对象。

1
2
3
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.timeline)
}

以上报告以下Timeline信息:

  • Latency:0.428秒
  • Request Duration:0.428秒
  • Serialization Duration:0.001秒
  • Total Duration:0.429秒

URL会话任务度量标准

在iOS和tvOS 10以及macOS 10.12中,Apple引入了新的URLSessionTaskMetrics API。任务指标封装了一些关于请求和响应执行的精彩统计信息。API非常类似Timeline,但提供了Alamofire无法计算的更多统计信息。可以通过任何响应类型访问度量标准。

1
2
3
Alamofire.request("https://httpbin.org/get").responseJSON { response in
print(response.metrics)
}

值得注意的是,这些API仅适用于iOS和tvOS 10以及macOS 10.12。因此,根据您的部署目标,您可能需要使用这些内部可用性检查:

1
2
3
4
5
Alamofire.request("https://httpbin.org/get").responseJSON { response in
if #available(iOS 10.0, *) {
print(response.metrics)
}
}

cURL命令输出

调试平台问题可能令人沮丧。值得庆幸的是,Alamofire Request对象符合协议CustomStringConvertible和CustomDebugStringConvertible协议,以提供一些非常有用的调试工具。

CustomStringConvertible

1
2
3
4
let request = Alamofire.request("https://httpbin.org/ip")

print(request)
// GET https://httpbin.org/ip (200)

CustomDebugStringConvertible

1
2
let request = Alamofire.request("https://httpbin.org/get", parameters: ["foo": "bar"])
debugPrint(request)

输出:

1
2
3
4
5
$ 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-高级用法

Posted on 2018-10-14 | In swift源码学习

高级用法

Alamofire建立在URLSessionFoundation URL加载系统之上。要充分利用此框架,建议您熟悉底层网络堆栈的概念和功能。

推荐阅读

  • URL加载系统编程指南
  • URLSession类参考
  • URLCache类参考
  • URLAuthenticationChallenge类参考

会话管理器

顶级便捷方法,例如Alamofire.request使用默认实例Alamofire.SessionManager配置的默认实例URLSessionConfiguration。

因此,以下两个陈述是等效的:

1
2
3
4
Alamofire.request("https://httpbin.org/get")

let sessionManager = Alamofire.SessionManager.default
sessionManager.request("https://httpbin.org/get")

应用程序可以为后台和短暂会话创建会话管理器,以及自定义默认会话配置的新管理器,例如默认标头(httpAdditionalHeaders)或超时间隔(timeoutIntervalForRequest)。

使用默认配置创建会话管理器

1
2
let configuration = URLSessionConfiguration.default
let sessionManager = Alamofire.SessionManager(configuration: configuration)

使用后台配置创建会话管理器

1
2
let configuration = URLSessionConfiguration.background(withIdentifier: "com.example.app.background")
let sessionManager = Alamofire.SessionManager(configuration: configuration)

使用临时配置创建会话管理器

1
2
let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: configuration)

修改会话配置

1
2
3
4
5
6
7
var defaultHeaders = Alamofire.SessionManager.defaultHTTPHeaders
defaultHeaders["DNT"] = "1 (Do Not Track Enabled)"

let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = defaultHeaders

let sessionManager = Alamofire.SessionManager(configuration: configuration)

这是不推荐Authorization或Content-Type头。相反,使用headers参数在顶层Alamofire.request的API,URLRequestConvertible并ParameterEncoding分别。

会话代表

默认情况下,Alamofire SessionManager实例创建一个SessionDelegate对象来处理由底层生成的所有各种类型的委托回调URLSession。每个委托方法的实现处理这些类型的调用的最常见用例,从顶层API中抽象出复杂性。但是,高级用户可能会因各种原因发现需要覆盖默认功能。

覆盖闭包

自定义SessionDelegate行为的第一种方法是使用覆盖闭包。每个闭包使您能够覆盖匹配SessionDelegateAPI 的实现,但仍然使用所有其他API的默认实现。这样可以轻松自定义委托功能的子集。以下是一些可用的覆盖闭包的示例:

1
2
3
4
5
6
7
8
9
10
11
/// Overrides default behavior for URLSessionDelegate method `urlSession(_:didReceive:completionHandler:)`.
open var sessionDidReceiveChallenge: ((URLSession, URLAuthenticationChallenge) -> (URLSession.AuthChallengeDisposition, URLCredential?))?

/// Overrides default behavior for URLSessionDelegate method `urlSessionDidFinishEvents(forBackgroundURLSession:)`.
open var sessionDidFinishEventsForBackgroundURLSession: ((URLSession) -> Void)?

/// Overrides default behavior for URLSessionTaskDelegate method `urlSession(_:task:willPerformHTTPRedirection:newRequest:completionHandler:)`.
open var taskWillPerformHTTPRedirection: ((URLSession, URLSessionTask, HTTPURLResponse, URLRequest) -> URLRequest?)?

/// Overrides default behavior for URLSessionDataDelegate method `urlSession(_:dataTask:willCacheResponse:completionHandler:)`.
open var dataTaskWillCacheResponse: ((URLSession, URLSessionDataTask, CachedURLResponse) -> CachedURLResponse?)?

以下是如何使用taskWillPerformHTTPRedirection以避免重定向到任何apple.com域的简短示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)
let delegate: Alamofire.SessionDelegate = sessionManager.delegate

delegate.taskWillPerformHTTPRedirection = { session, task, response, request in
var finalRequest = request

if
let originalRequest = task.originalRequest,
let urlString = originalRequest.url?.urlString,
urlString.contains("apple.com")
{
finalRequest = originalRequest
}

return finalRequest
}

子类

覆盖默认实现的另一种方法SessionDelegate是将其子类化。子类化允许您完全自定义API的行为或为API创建代理,并仍使用默认实现。创建代理允许您记录事件,发出通知,提供前后钩实现等。这是一个SessionDelegate在重定向发生时子类化和记录消息的快速示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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
)
}
}

一般来说,默认实现或覆盖闭包应提供所需的必要功能。子类只应作为最后的手段使用。

请记住,subdelegates在默认实现中初始化和销毁它是很重要的。子类化时要小心,不要引入内存泄漏。

请求

的结果request,download,upload或stream方法是a DataRequest,DownloadRequest,UploadRequest和StreamRequest其中所有的继承Request。所有Request实例始终由拥有的会话管理器创建,并且从不直接初始化。

每个子类有专门的方法,比如authenticate,validate,responseJSON和uploadProgress每个以利于方法链接返回调用者的实例。

请求可以暂停,恢复和取消:

  • suspend():暂停基础任务和调度队列。
  • resume():恢复基础任务和调度队列。如果拥有管理器未startRequestsImmediately设置为true,则请求必须调用resume()才能启动。
  • cancel():取消基础任务,产生传递给任何已注册响应处理程序的错误。

路由请求

随着应用程序规模的扩大,在构建网络堆栈时采用通用模式非常重要。该设计的一个重要部分是如何路由您的请求。Alamofire URLConvertible和URLRequestConvertible协议以及Router设计模式可以提供帮助。

URLConvertible

采用该URLConvertible协议的类型可用于构造URL,然后用于在内部构造URL请求。String,URL以及URLComponents符合URLConvertible在默认情况下,允许任何人将其交给url参数的request,upload和download方法:

1
2
3
4
5
6
7
8
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)

鼓励以显着方式与Web应用程序交互的应用程序使自定义类型符合将URLConvertible特定于域的模型映射到服务器资源的便捷方式。

类型安全路由
1
2
3
4
5
6
7
8
9
10
11
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

URLRequestConvertible

采用该URLRequestConvertible协议的类型可用于构造URL请求。默认情况下URLRequest符合URLRequestConvertible,允许它直接传递给request,upload和download方法(这是为各个请求指定自定义HTTP正文的推荐方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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)

鼓励以显着方式与Web应用程序交互的应用程序使自定义类型符合URLRequestConvertible确保所请求端点的一致性的方式。这种方法可用于抽象出服务器端的不一致性,并提供类型安全的路由,以及管理身份验证凭据和其他状态。

API参数抽象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
enum Router: URLRequestConvertible {
case search(query: String, page: Int)

static let baseURLString = "https://example.com"
static let perPage = 50

// MARK: URLRequestConvertible

func 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
CRUD和授权
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import Alamofire

enum 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 .post
case .readUser:
return .get
case .updateUser:
return .put
case .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: URLRequestConvertible

func asURLRequest() throws -> URLRequest {
let url = try Router.baseURLString.asURL()

var urlRequest = URLRequest(url: url.appendingPathComponent(path))
urlRequest.httpMethod = method.rawValue

switch 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

适应和重试请求

如今,大多数Web服务都支持某种身份验证系统。今天比较常见的一个是OAuth。这通常涉及生成访问令牌,授权您的应用程序或用户调用各种支持的Web服务。虽然创建这些初始访问令牌可能很麻烦,但是当访问令牌过期并且您需要获取新令牌时,它可能会更加复杂。需要考虑许多线程安全问题。

在RequestAdapter和RequestRetrier协议的建立是为了使它更容易为一组特定的Web服务创建一个线程安全的身份验证系统。

RequestAdapter

该RequestAdapter协议允许在创建之前Request对每个上面的内容SessionManager进行检查和调整。使用适配器的一种非常具体的方法是向Authorization特定类型的身份验证后面的请求附加标头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class AccessTokenAdapter: RequestAdapter {
private let accessToken: String

init(accessToken: String) {
self.accessToken = accessToken
}

func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
var urlRequest = urlRequest

if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix("https://httpbin.org") {
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
}

return urlRequest
}
}

let sessionManager = SessionManager()
sessionManager.adapter = AccessTokenAdapter(accessToken: "1234")

sessionManager.request("https://httpbin.org/get")

RequestRetrier

该RequestRetrier协议允许重试执行Request的Error一段时间。同时使用RequestAdapter和RequestRetrier协议时,您可以为OAuth1,OAuth2,Basic Auth甚至指数退避重试策略创建凭据刷新系统。可能性是无止境。以下是如何为OAuth2访问令牌实现刷新流程的示例。

免责声明:这不是一个全球性的OAuth2解决方案。它只是一个示例,演示了如何RequestAdapter结合使用它RequestRetrier来创建一个线程安全的刷新系统。

重申一下,请勿复制此示例代码并将其放入生产应用程序中。这只是一个例子。每个身份验证系统必须针对特定平台和身份验证类型进行定制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
class OAuth2Handler: RequestAdapter, RequestRetrier {
private typealias RefreshCompletion = (_ succeeded: Bool, _ accessToken: String?, _ refreshToken: String?) -> Void

private let sessionManager: SessionManager = {
let configuration = URLSessionConfiguration.default
configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

return SessionManager(configuration: configuration)
}()

private let lock = NSLock()

private var clientID: String
private var baseURLString: String
private var accessToken: String
private var refreshToken: String

private var isRefreshing = false
private var requestsToRetry: [RequestRetryCompletion] = []

// MARK: - Initialization

public init(clientID: String, baseURLString: String, accessToken: String, refreshToken: String) {
self.clientID = clientID
self.baseURLString = baseURLString
self.accessToken = accessToken
self.refreshToken = refreshToken
}

// MARK: - RequestAdapter

func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
if let urlString = urlRequest.url?.absoluteString, urlString.hasPrefix(baseURLString) {
var urlRequest = urlRequest
urlRequest.setValue("Bearer " + accessToken, forHTTPHeaderField: "Authorization")
return urlRequest
}

return urlRequest
}

// MARK: - RequestRetrier

func 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 in
guard let strongSelf = self else { return }

strongSelf.lock.lock() ; defer { strongSelf.lock.unlock() }

if let accessToken = accessToken, let refreshToken = refreshToken {
strongSelf.accessToken = accessToken
strongSelf.refreshToken = refreshToken
}

strongSelf.requestsToRetry.forEach { $0(succeeded, 0.0) }
strongSelf.requestsToRetry.removeAll()
}
}
} else {
completion(false, 0.0)
}
}

// MARK: - Private - Refresh Tokens

private func refreshTokens(completion: @escaping RefreshCompletion) {
guard !isRefreshing else { return }

isRefreshing = true

let 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 in
guard 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
}
}
}

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 = oauthHandler

let urlString = "\(baseURLString)/some/endpoint"

sessionManager.request(urlString).validate().responseJSON { response in
debugPrint(response)
}

一旦OAuth2Handler被同时作为应用adapter和retrier为SessionManager,它会自动刷新访问令牌,并在重试失败,他们以相同的顺序所有失败的请求的处理无效访问令牌错误。

如果您需要按照创建它们的顺序执行它们,则可以按任务标识符对它们进行排序。

上面的示例仅检查401响应代码,该代码不够健壮,但确实演示了如何检查无效的访问令牌错误。在生产应用程序中,尽管取决于OAuth2实现,但仍希望检查realm并且最有可能是www-authenticate标头响应。

另一个重要的注意事项是该认证系统可以在多个会话管理器之间共享。例如,您可能需要对同一组Web服务使用a default和ephemeral会话配置。上面的示例允许oauthHandler在多个会话管理器之间共享同一实例,以管理单个刷新流。

自定义响应序列化

Alamofire为数据,字符串,JSON和属性列表提供内置的响应序列化:

1
2
3
4
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 ... }

这些响应包括反序列化的值(数据,字符串,任何)或错误(网络,验证错误),以及元数据(URL请求,HTTP标头,状态代码,度量标准 ……)。

您可以通过多种方式自定义所有响应元素:

  • 响应映射
  • 处理错误
  • 创建自定义响应序列化程序
  • 通用响应对象序列化

响应映射

响应映射是生成自定义响应的最简单方法。它可以转换响应的值,同时保留最终的错误和元数据。例如,您可以将json响应DataResponse<Any>转换为包含应用程序模型的响应,例如DataResponse<User>。您使用以下DataResponse.map方法执行响应映射:

1
2
3
4
5
6
7
8
9
10
11
Alamofire.request("https://example.com/users/mattt").responseJSON { (response: DataResponse<Any>) in
let userResponse = response.map { json in
// We assume an existing User(json: Any) initializer
return User(json: json)
}

// Process userResponse, of type DataResponse<User>:
if let user = userResponse.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}

当转换可能引发错误时,请flatMap改为使用:

1
2
3
4
5
Alamofire.request("https://example.com/users/mattt").responseJSON { response in
let userResponse = response.flatMap { json in
try User(json: json)
}
}

响应映射非常适合您的自定义完成处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@discardableResult
func loadUser(completionHandler: @escaping (DataResponse<User>) -> Void) -> Alamofire.DataRequest {
return Alamofire.request("https://example.com/users/mattt").responseJSON { response in
let userResponse = response.flatMap { json in
try User(json: json)
}

completionHandler(userResponse)
}
}

loadUser { response in
if let user = response.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}

当map / flatMap闭包可以处理大量数据时,请确保在主线程之外执行它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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 in
let userResponse = response.flatMap { json in
try User(json: json)
}

DispatchQueue.main.async {
completionHandler(userResponse)
}
}
}

map并且flatMap还可以下载回复。

处理错误

在实现自定义响应序列化程序或对象序列化方法之前,考虑如何处理可能发生的任何错误非常重要。有两个基本选项:沿未修改的方式传递现有错误,在响应时处理; 或者,将所有错误包装在Error特定于您的应用的类型中。

例如,这是一个简单的BackendError枚举,将在后面的例子中使用:

1
2
3
4
5
6
7
enum BackendError: Error {
case network(error: Error) // Capture any underlying Error from the URLSession API
case dataSerialization(error: Error)
case jsonSerialization(error: Error)
case xmlSerialization(error: Error)
case objectSerialization(reason: String)
}

创建自定义响应序列化程序

Alamofire为字符串,JSON和属性列表提供内置的响应序列化,但是其他可以在扩展中添加Alamofire.DataRequest和/或Alamofire.DownloadRequest。

例如,以下是如何实现使用Ono的响应处理程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
extension DataRequest {
static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
return DataResponseSerializer { request, response, data, error in
// Pass through any underlying URLSession error to the .network case.
guard error == nil else { return .failure(BackendError.network(error: error!)) }

// Use Alamofire's existing data serializer to extract the data, passing the error as nil, as it has
// already been handled.
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))
}
}
}

@discardableResult
func responseXMLDocument(
queue: DispatchQueue? = nil,
completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
-> Self
{
return response(
queue: queue,
responseSerializer: DataRequest.xmlResponseSerializer(),
completionHandler: completionHandler
)
}
}

通用响应对象序列化

泛型可用于提供自动,类型安全的响应对象序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 in
guard 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)
}
}

struct User: ResponseObjectSerializable, CustomStringConvertible {
let username: String
let name: String

var description: String {
return "User: { username: \(username), name: \(name) }"
}

init?(response: HTTPURLResponse, representation: Any) {
guard
let username = response.url?.lastPathComponent,
let representation = representation as? [String: Any],
let name = representation["name"] as? String
else { return nil }

self.username = username
self.name = name
}
}

Alamofire.request("https://example.com/users/mattt").responseObject { (response: DataResponse<User>) in
debugPrint(response)

if let user = response.result.value {
print("User: { username: \(user.username), name: \(user.name) }")
}
}

同样的方法也可用于处理返回对象集合表示的端点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
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
}
}

extension DataRequest {
@discardableResult
func responseCollection<T: ResponseCollectionSerializable>(
queue: DispatchQueue? = nil,
completionHandler: @escaping (DataResponse<[T]>) -> Void) -> Self
{
let responseSerializer = DataResponseSerializer<[T]> { request, response, data, error in
guard 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)
}
}

struct User: ResponseObjectSerializable, ResponseCollectionSerializable, CustomStringConvertible {
let username: String
let name: String

var description: String {
return "User: { username: \(username), name: \(name) }"
}

init?(response: HTTPURLResponse, representation: Any) {
guard
let username = response.url?.lastPathComponent,
let representation = representation as? [String: Any],
let name = representation["name"] as? String
else { return nil }

self.username = username
self.name = name
}
}

Alamofire.request("https://example.com/users").responseCollection { (response: DataResponse<[User]>) in
debugPrint(response)

if let users = response.result.value {
users.forEach { print("- \($0)") }
}
}

安全

在与服务器和Web服务通信时使用安全HTTPS连接是保护敏感数据的重要步骤。默认情况下,Alamofire将使用安全框架提供的Apple内置验证来评估服务器提供的证书链。虽然这可以保证证书链的有效性,但它不会阻止中间人(MITM)攻击或其他潜在漏洞。为了减轻MITM攻击,处理敏感客户数据或财务信息的应用程序应使用由提供的证书或公钥固定ServerTrustPolicy。

ServerTrustPolicy

该ServerTrustPolicy枚举评估一般由提供的服务器信任URLAuthenticationChallenge连接到服务器通过安全的HTTPS连接时。

1
2
3
4
5
let serverTrustPolicy = ServerTrustPolicy.pinCertificates(
certificates: ServerTrustPolicy.certificates(),
validateCertificateChain: true,
validateHost: true
)

服务器信任评估有许多不同的情况,使您可以完全控制验证过程:

  • performDefaultEvaluation:使用默认服务器信任评估,同时允许您控制是否验证质询提供的主机。
  • pinCertificates:使用固定证书来验证服务器信任。如果其中一个固定证书与其中一个服务器证书匹配,则认为服务器信任有效。
  • pinPublicKeys:使用固定的公钥来验证服务器信任。如果其中一个固定公钥与其中一个服务器证书公钥匹配,则认为服务器信任有效。
  • disableEvaluation:禁用所有评估,而评估又始终将任何服务器信任视为有效。
  • customEvaluation:使用关联的闭包来评估服务器信任的有效性,从而使您可以完全控制验证过程。谨慎使用。

服务器信任策略管理器

该ServerTrustPolicyManager负责存储服务器的信任策略的内部映射到特定的主机。这允许Alamofire根据不同的服务器信任策略评估每个主机。

1
2
3
4
5
6
7
8
9
10
11
12
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握手成功:

    • 证书链必须有效。
    • 证书链必须包含一个固定证书。
    • 挑战主机必须匹配证书链的叶证书中的主机。
  • insecure.expired-apis.com 永远不会评估证书链,并始终允许TLS握手成功。

  • 所有其他主机将使用Apple提供的默认评估。

子类化服务器信任策略管理器

如果您发现自己需要更灵活的服务器信任策略匹配行为(即通配域),则使用您自己的自定义实现继承ServerTrustPolicyManager并覆盖该serverTrustPolicyForHost方法。

1
2
3
4
5
6
7
8
9
class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
var policy: ServerTrustPolicy?

// Implement your custom domain matching behavior...

return policy
}
}

验证主机

的.performDefaultEvaluation,.pinCertificates和.pinPublicKeys服务器信任的政策都将一个validateHost参数。将值设置为true将导致服务器信任评估验证证书中的主机名是否与质询的主机名匹配。如果它们不匹配,评估将失败。一个validateHost价值false仍然会评估整个证书链,但不会验证叶证书的主机名。

建议validateHost始终true在生产环境中进行设置。

验证证书链

固定证书和公钥都可以选择使用validateCertificateChain参数验证证书链。通过将此值设置为true,除了对固定证书或公钥执行字节相等性检查之外,还将评估完整证书链。值false将跳过证书链验证,但仍将执行字节相等性检查。

在某些情况下,禁用证书链验证可能是有意义的。禁用验证的最常见用例是自签名和过期证书。在这两种情况下,评估总是会失败,但字节相等检查仍将确保您收到您希望从服务器获得的证书。

建议validateCertificateChain始终true在生产环境中进行设置。

App Transport Security

通过在iOS 9中添加App Transport Security(ATS),使用ServerTrustPolicyManager具有多个ServerTrustPolicy对象的自定义可能无效。如果你不断看到CFNetwork SSLHandshake failed (-9806)错误,你可能遇到了这个问题。除非您在应用程序的plist中配置ATS设置以禁用足够的ATS设置以允许您的应用评估服务器信任,否则Apple的ATS系统会覆盖整个质询系统。

如果您遇到此问题(使用自签名证书的概率很高),您可以通过将以下内容添加到您的问题来解决此问题Info.plist。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<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/>
<!-- Optional: Specify minimum TLS version -->
<key>NSTemporaryExceptionMinimumTLSVersion</key>
<string>TLSv1.2</string>
</dict>
</dict>
</dict>
</dict>

是否需要设置NSExceptionRequiresForwardSecrecy为NO取决于您的TLS连接是否使用允许的密码套件。在某些情况下,需要将其设置为NO。在NSExceptionAllowsInsecureHTTPLoads必须设置为YES以允许SessionDelegate接受挑战回调。一旦调用挑战回调,ServerTrustPolicyManager将接管服务器信任评估。您可能还需要指定NSTemporaryExceptionMinimumTLSVersion是否尝试连接到仅支持低于TLS版本的主机1.2。

建议始终在生产环境中使用有效证书。

将自签名证书与本地网络配合使用

如果您尝试连接到localhost上运行的服务器,并且您使用的是自签名证书,则需要将以下内容添加到您的Info.plist。

1
2
3
4
5
6
7
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>

根据苹果的文档,设置NSAllowsLocalNetworking以YES允许本地资源负载,而无需禁用ATS为您的应用程序的其余部分。

网络可达性

该NetworkReachabilityManager主机和地址都WWAN和WiFi网络接口的可达性变化监听。

1
2
3
4
5
6
7
let manager = NetworkReachabilityManager(host: "www.apple.com")

manager?.listener = { status in
print("Network Status Changed: \(status)")
}

manager?.startListening()

请务必记住保留manager上面的示例,否则不会报告任何状态更改。此外,不要在host字符串中包含该方案,否则可达性将无法正常运行。

使用网络可达性来确定下一步该做什么时,需要记住一些重要的事项。

  • 请勿

    使用Reachability确定是否应发送网络请求。

    • 你应该总是发送它。
  • 恢复可达性后,使用该事件重试失败的网络请求。

    • 即使网络请求可能仍然失败,这是重试它们的好时机。
  • 网络可达性状态可用于确定网络请求可能失败的原因。

    • 如果网络请求失败,告诉用户网络请求由于脱机而不是更技术性错误(例如“请求超时”)更有用。

建议查看WWDC 2012 Session 706,“Networking Best Practices”以获取更多信息。

leetCode-0234_回文链表

Posted on 2018-10-13 | In 算法

题目描述

Given a singly linked list, determine if it is a palindrome.

Example 1:

1
2
Input: 1->2
Output: false

Example 2:

1
2
Input: 1->2->2->1
Output: true

Follow up:
Could you do it in O(n) time and O(1) space?

中文题目

请判断一个链表是否为回文链表。

示例 1:

1
2
输入: 1->2
输出: false

示例 2:

1
2
输入: 1->2->2->1
输出: true

进阶:
你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

Read more »

leetCode-0025_k个一组翻转链表

Posted on 2018-10-13 | In 算法

题目描述

英文题目

Given a linked list, reverse the nodes of a linked list k at a time and return its modified list.

k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

Example:

Given this linked list: 1->2->3->4->5

For k = 2, you should return: 2->1->4->3->5

For k = 3, you should return: 3->2->1->4->5

Note:

  • Only constant extra memory is allowed.
  • You may not alter the values in the list’s nodes, only nodes itself may be changed.
中文题目

给出一个链表,每 k 个节点一组进行翻转,并返回翻转后的链表。

k 是一个正整数,它的值小于或等于链表的长度。如果节点总数不是 k 的整数倍,那么将最后剩余节点保持原有顺序。

示例 :

给定这个链表:1->2->3->4->5

当 k = 2 时,应当返回: 2->1->4->3->5

当 k = 3 时,应当返回: 3->2->1->4->5

说明 :

  • 你的算法只能使用常数的额外空间。
  • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
Read more »

leetCode-0142_环形链表II

Posted on 2018-10-12 | In 算法

题目描述

英文题目

Given a linked list, return the node where the cycle begins. If there is no cycle, return null.

Note: Do not modify the linked list.

Follow up:
Can you solve it without using extra space?

中文题目

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。

说明:不允许修改给定的链表。

进阶:
你是否可以不用额外空间解决此题?

Read more »

leetCode-0141_环形链表

Posted on 2018-10-11 | In 算法

题目描述

英文题目

Given a linked list, determine if it has a cycle in it.

Follow up:
Can you solve it without using extra space?

中文题目

给定一个链表,判断链表中是否有环。

进阶:
你能否不使用额外空间解决此题?

Read more »

leetCode-0024_两两交换链表中的节点

Posted on 2018-10-10 | In 算法

题目描述

英文题目
  • Given a linked list, swap every two adjacent nodes and return its head.

    Example:

    1
    Given 1->2->3->4, you should return the list as 2->1->4->3.

    Note:

    • Your algorithm should use only constant extra space.
    • You may not modify the values in the list’s nodes, only nodes itself may be changed.
中文题目
  • 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。

    示例:

    1
    给定 1->2->3->4, 你应该返回 2->1->4->3.

    说明:

    • 你的算法只能使用常数的额外空间。
    • 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
Read more »

leetCode-0206_反转链表

Posted on 2018-10-09 | In 算法

题目描述

英文题目
  • Reverse a singly linked list.

    Example:

    1
    2
    Input: 1->2->3->4->5->NULL
    Output: 5->4->3->2->1->NULL
中文题目
  • 反转一个单链表。

    示例:

    1
    2
    输入: 1->2->3->4->5->NULL
    输出: 5->4->3->2->1->NULL
Read more »

数据结构之二叉树

Posted on 2018-09-28 | In 数据结构

二叉树源代码

Read more »
1…456
LiYouCheng2014

LiYouCheng2014

iOS\算法\项目管理

56 posts
8 categories
29 tags
GitHub E-Mail
© 2018 LiYouCheng2014
Powered by Hexo
|
Theme — NexT.Gemini v5.1.4