基本用法,高级用法

Alamofire是在URLSession和URL加载系统的基础上写的。所以,为了更好地学习这个框架,建议先熟悉下列几个底层网络协议栈:

【iOS开发】Alamofire框架的使用二 —— 高级用法

这边文章介绍的是Alamofire框架的高级用法,如果之前没有看过基本用法的,可以先去看看【iOS开发】Alamofire框架的使用一 —— 基本用法

Alamofire是在URLSession和URL加载系统的基础上写的。所以,为了更好地学习这个框架,建议先熟悉下列几个底层网络协议栈:

  • URL Loading System Programming Guide >>
  • URLSession Class Reference >>
  • URLCache Class Reference >>
  • URLAuthenticationChallenge Class Reference >>

Alamofire 是一款 Swift 写的 HTTP 网络请求库

Alamofire框架的使用一 —— 基本用法

对于使用Objective-C的开发者,一定非常熟悉AFNetworking这个网络框架。在苹果推出的Swift之后,AFNetworking的作者专门用Swift来编写一个类似AFNetworking的网络框架,称为Alamofire。Alamofire地址 >>

我分两篇文章介绍如何使用Alamofire框架。文章的内容主要是翻译Alamofire的readme。第二篇文章 >>

  • URL Loading System Programming Guide >>
  • URLSession Class Reference >>
  • URLCache Class Reference >>
  • URLAuthenticationChallenge Class Reference >>

Session Manager

高级别的方便的方法,例如Alamofire.request,使用的是默认的Alamofire.SessionManager,并且这个SessionManager是用默认URLSessionConfiguration配置的。

例如,下面两个语句是等价的:

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

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

我们可以自己创建后台会话和短暂会话的session manager,还可以自定义默认的会话配置来创建新的session manager,例如修改默认的header httpAdditionalHeaderstimeoutIntervalForRequest

本篇内容为 Alamofire 官方 Readme 文件的翻译,如有翻译不恰当的地方,您也可以给我提交PR,该翻译github地址。本篇包含如下内容:

功能

  • 链式请求/响应方法
  • URL / JSON / plist参数编码
  • 上传文件/数据/流/ 多部分表单数据
  • 使用请求下载文件或恢复数据
  • 使用URL凭据进行身份认证
  • HTTP响应的验证
  • 上传和下载进度闭包
  • cURL命令的输出
  • 动态适应和重试请求
  • TLS证书和Public Key Pinning
  • 网络可达性
  • 全面的单元和集成测试覆盖率

Session Manager

高级别的方便的方法,例如Alamofire.request,使用的是默认的Alamofire.SessionManager,并且这个SessionManager是用默认URLSessionConfiguration配置的。

例如,下面两个语句是等价的:

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

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

我们可以自己创建后台会话和短暂会话的session manager,还可以自定义默认的会话配置来创建新的session manager,例如修改默认的header httpAdditionalHeaderstimeoutIntervalForRequest

用默认的会话配置创建一个Session Manager

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

Alamofire 是 Swift 语言编写的 HTTP 网络库。

组件库

为了让Alamofire专注于核心网络的实现,Alamofire生态系统还有另外两个库:

  • AlamofireImage:一个图片库,包括图像响应序列化器、UIImageUIImageView的扩展、自定义图像滤镜、内存中自动清除和基于优先级的图像下载系统。
  • AlamofireNetworkActivityIndicator:控制iOS应用的网络活动指示器。包含可配置的延迟计时器来帮助减少闪光和支持URLSession实例。

用默认的会话配置创建一个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)
  • 特性
  • 组件
  • 环境需求
  • 移植指南
  • 安装
  • 用法
    • 简介 - 发起请求, 响应回调, 响应验证, 响应缓存
      • HTTP - [HTTP 方法], 请求参数编码, HTTP Headers, 认证
      • 大量数据 - 下载数据到文件, 上传数据到服务器
      • 工具 - 指标统计, [cURL 命令输出](#cURL 命令输出)
    • 高级用法
      • URL 会话 - 会话管理, 会话代理, 请求
      • 请求路由 - 请求路由, Adapting and Retrying Requests
      • 模型对象 - 自定义响应序列化器
      • 网络连接 - 安全性, 网络可用性
  • Open Radars
  • FAQ
  • 致谢
  • 捐款
  • 开源协议

要求的使用环境

  • iOS 8.0 / macOS 10.10 / tvOS 9.0 / watchOS 2.0
  • Xcode 8.1
  • Swift 3.0

用后台会话配置创建一个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)

安装方法

用默短暂会话配置创建一个Session Manager

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

修改会话配置

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 header使用。而应该使用Alamofire.requestAPI、URLRequestConvertibleParameterEncoding的headers参数。

  • [x] 链式请求 / 响应方法调用
  • [x] URL / JSON / plist 请求参数编码
  • [x] 上传 File / Data / Stream / MultipartFormData
  • [x] 文件下载和断点续传
  • [x] URLCredential 认证方式
  • [x] HTTP 响应验证
  • [x] 上传下载进度
  • [x] cURL 命令输出
  • [x] Dynamically Adapt and Retry Requests
  • [x] TLS Certificate and Public Key Pinning
  • [x] 网络可用性
  • [x] 测试单元和集成测试

CocoaPods

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target '项目名称' do
    pod 'Alamofire', '~> 4.4'
end

iOS版本和Alamofire版本可以自己根据实际情况自行更改。CocoaPods是比较常用的第三方库管理工具,其他方法就不详细说了。

修改会话配置

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 header使用。而应该使用Alamofire.requestAPI、URLRequestConvertibleParameterEncoding的headers参数。

会话代理

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

为了让 Alamofire 集中于核心网络操作的实现,Alamofire Software Foundation 采用组件的形式为 Alamofire 添加额外的功能。

如何使用

会话代理

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

重写闭包

第一种自定义SessionDelegate的方法是通过重写闭包。我们可以在每个闭包重写SessionDelegate API对应的实现。下面是重写闭包的示例:

/// 重写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?)?

下面的示例演示了如何使用taskWillPerformHTTPRedirection来避免回调到任何apple.com域名。

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
}
  • AlamofireImage - 一个包含图片响应序列化, UIImageUIImageView 的扩展,自定义图片过滤器,自动清理的内存缓存,基于优先级的图片下载的图片库。
  • AlamofireNetworkActivityIndicator - 控制网络活动指示器的显示。允许用户配置延迟时间来延缓活动指示器的显示,同时也支持独立创建的 URLSession 实例对象。

发请求

Alamofire.request("")

重写闭包

第一种自定义SessionDelegate的方法是通过重写闭包。我们可以在每个闭包重写SessionDelegate API对应的实现。下面是重写闭包的示例:

/// 重写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?)?

下面的示例演示了如何使用taskWillPerformHTTPRedirection来避免回调到任何apple.com域名。

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创建一个代理并且仍然使用它的默认实现。通过创建代理,我们可以跟踪日志事件、发通知、提供前后实现。下面这个例子演示了如何子类化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
        )
    }
}

总的来说,无论是默认实现还是重写闭包,都应该提供必要的功能。子类化应该作为最后的选择。

响应处理

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

Alamofire.request("https://httpbin.org/get").responseJSON { response in
    print(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直接拼接到请求后面,当请求完成后被调用。这个回调形式的闭包一旦收到响应后,就会处理这个响应,并不会因为等待服务器的响应而造成阻塞执行。请求的结果仅在响应闭包的范围内可用。其他任何与服务器返回的响应或者数据相关的操作,都必须在这个闭包内执行。

Alamofire默认情况下包含五种不同的响应handler:

// 响应 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

所有的响应handler都不会对响应进行验证。也就是说响应状态码在400..<499500..<599范围内,都不会触发错误。

子类化

另一个重写SessionDelegate的实现的方法是把它子类化。通过子类化,我们可以完全自定义他的行为,或者为这个API创建一个代理并且仍然使用它的默认实现。通过创建代理,我们可以跟踪日志事件、发通知、提供前后实现。下面这个例子演示了如何子类化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
        )
    }
}

总的来说,无论是默认实现还是重写闭包,都应该提供必要的功能。子类化应该作为最后的选择。

请求

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

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

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

  • suspend():暂停底层的任务和调度队列
  • resume():恢复底层的任务和调度队列。如果manager的startRequestsImmediately不是true,那么必须调用resume()来开始请求。
  • cancel():取消底层的任务,并产生一个error,error被传入任何已经注册的响应handlers。
  • iOS 8.0 / macOS 10.10 / tvOS 9.0 / watchOS 2.0
  • Xcode 8.1
  • Swift 3.0
响应 Handler

response handler不处理任何相应数据。它仅仅是从URL会话代理中转发信息。

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)")
    }
}

一般情况下不建议使用这种没有响应序列化器的handler,而应该使用下面有特定序列化器的handler。

请求

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

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

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

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

传送请求

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

响应数据 Handler

responseData使用responseDataSerializer(这个对象把服务器的数据序列化成其他类型)来提取服务器返回的数据。如果没有返回错误并且有数据返回,那么响应Result将会是.successvalueData类型。

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)")
    }
}

传送请求

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

URLConvertible

遵循了URLConvertible协议的类型可以被用来构建URL,然后用来创建URL请求。StringURLURLComponent默认是遵循URLConvertible协议的。它们都可以作为url参数传入requestuploaddownload方法:

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协议的类型将特定领域模型映射到服务器资源,因为这样比较方便。

  • Alamofire 4.0 移植指南
  • Alamofire 3.0 移植指南
  • Alamofire 2.0 移植指南
响应字符串 Handler

responseString handler使用responseStringSerializer对象根据特定的编码格式把服务器返回的数据转换成String。如果没有返回错误并且服务器的数据成功地转换为String,那么响应Result将会是.successvalueString类型。

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

如果没有指定编码格式,将会使用服务器的HTTPURLResponse指定的格式。如果服务器无法确定编码格式,那么默认使用.isoLatin1

URLConvertible

遵循了URLConvertible协议的类型可以被用来构建URL,然后用来创建URL请求。StringURLURLComponent默认是遵循URLConvertible协议的。它们都可以作为url参数传入requestuploaddownload方法:

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协议的类型将特定领域模型映射到服务器资源,因为这样比较方便。

类型安全传送
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

Cocoapods

CocoaPods 是一款 Cocoa 项目的依赖库管理工具。使用下面命令安装 cocoapods:

$ gem install cocoapods

编译 Alamofire 4.0.0 需要 1.1.0 以上版本的 cocoapods

Podfile 中进行声明来集成 Alamofire:

source 'https://github.com/CocoaPods/Specs.git'platform :ios, '10.0'use_frameworks!target '<Your Target Name>' do pod 'Alamofire', '~> 4.0'end

然后运行下面的命令进行安装:

$ pod install
响应 JSON Handler

responseJSON handler使用responseJSONSerializer对象根据指定的JSONSerialization.ReadingOptions把服务器返回的数据转换成Any类型。如果没有返回错误并且服务器的数据成功地转换为JSON对象,那么响应Result将会是.successvalueAny类型。

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

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

所有JSON的序列化,都是使用JSONSerialization完成的。

类型安全传送
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,允许被直接传入requestuploaddownload(推荐用这种方法为单个请求自定义请求头)。

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协议的类型来保证请求端点的一致性。这种方法可以用来抽象服务器端的不一致性,并提供类型安全传送,以及管理身份验证凭据和其他状态。

Carthage

Carthage 是一款编译管理依赖库的工具,可提供编译好的 frameworks。

通过 Homebrew 使用一下命令安装 Carthage:

$ brew update$ brew install carthage

Cartfile 中进行声明来集成 Alamofire:

github "Alamofire/Alamofire" ~> 4.0

运行 carthage update 命令进行编译,并把编译好的 Alamofire.framework 拖拽到你的 Xcode 项目中。

链式响应handler

响应handler可以链接在一起:

Alamofire.request("https://httpbin.org/get")
    .responseString { response in
        print("Response String: (response.result.value)")
    }
    .responseJSON { response in
        print("Response JSON: (response.result.value)")
    }

注意:在同一个请求中使用多个响应handler,要求服务器的数据会被序列化多次,每次对应一个handler。

URLRequestConvertible

遵循URLRequestConvertible协议的类型可以被用来构建URL请求。URLRequest默认遵循了URLRequestConvertible,允许被直接传入requestuploaddownload(推荐用这种方法为单个请求自定义请求头)。

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参数抽象
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 bar&offset=50

发起请求

import AlamofireAlamofire.request("https://httpbin.org/get")
响应handler队列

默认情况下,响应handler是在主队列执行的。但是我们也可以自定义队列:

let utilityQueue = DispatchQueue.global(qos: .utility)

Alamofire.request("https://httpbin.org/get").responseJSON(queue: utilityQueue) { response in
    print("Executing response handler on utility queue")
}
API参数抽象
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 bar&offset=50
CRUD和授权
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

响应回调

处理请求的响应只需在 Request 后面加上处理响应的回调。

Alamofire.request("https://httpbin.org/get").responseJSON { response in print(response.request) // original URL request print(response.response) // HTTP URL response print(response.data) // server data print(response.result) // result of response serialization if let JSON = response.result.value { print("JSON:  }}

上面的例子中,responseJSON 回调拼接在 Request 后面,一旦网络请求完成便会执行该 responseJSON。这里没有阻塞线程来等待响应,而是采用了闭包形式的回调来异步接受响应。请求的结果只能在响应的闭包中进行处理。对响应或从服务器接收到的数据只能在响应闭包中处理。

Alamofire 中网络请求是异步处理的。对异步编程相关的概念不太熟悉的话会让人一头雾水,但有太多的理由让我们采用异步编程。

Alamofire 默认包含五种响应回调:

// Response Handler - Unserialized Responsefunc response( queue: DispatchQueue?, completionHandler: @escaping (DefaultDataResponse) -> Void) -> Self// Response Data Handler - Serialized into Datafunc responseData( queue: DispatchQueue?, completionHandler: @escaping (DataResponse<Data>) -> Void) -> Self// Response String Handler - Serialized into Stringfunc responseString( queue: DispatchQueue?, encoding: String.Encoding?, completionHandler: @escaping (DataResponse<String>) -> Void) -> Self// Response JSON Handler - Serialized into Anyfunc responseJSON( queue: DispatchQueue?, completionHandler: @escaping (DataResponse<Any>) -> Void) -> Self// Response PropertyList  Handler - Serialized into Anyfunc responsePropertyList( queue: DispatchQueue?, completionHandler: @escaping (DataResponse<Any>) -> Void)) -> Self

这些回调都没有对 HTTPURLResponse 进行验证。

比如,400..499500..599 之间的响应状态码不会自动触发 Error。Alamofire 采用 Response Validation 的链式方法来进行验证。

response 不对响应数据进行任何处理,直接把 URL session 代理中的数据交给后面的流程,与使用 cURL 执行请求效果一样。

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: ") }}

强烈建议使用其他响应序列化器将数据变为更易使用的 ResponseResult 类型

responseData 回调使用 responseDataSerializer (该对象用于将从服务器接收的数据序列化为其他类型) 处理服务器端返回的 Data。如果没有错误就返回 Data,response 的 Result 会被设置为 .success value 会被设置为 Data 类型。

Alamofire.request("https://httpbin.org/get").responseData { response in debugPrint("All Response Info: ") if let data = response.result.value, let utf8Text = String(data: data, encoding: .utf8) { print("Data: ") }}

responseString 回调使用 responseStringSerializer 根据指定的编码方式将从服务器接收到的 Data 转换为 String 类型。如果没有错误并且数据被成功序列化为 String,则 response 的 Result 会被设置为 .successvalue 会被设置为 String 类型。

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

如果未指定编码方式,Alamofire 会根据从服务器端接收到的 HTTPURLResponse 中的编码方式进行编码。如果响应中也未指定编码方式,默认使用 .isoLatin1 编码方式

responseJSON 使用 responseJSONSerializer 根据指定的 JSONSerialization.ReadingOptions 将从服务器接收到的数据 Data 转换为 Any 类型。如果没有错误并且接收到的数据成功的序列化为 JSON 对象,则 response 的 Result 会被设置为 .success 并且 value 会被设置为 Any 类型。

Alamofire.request("https://httpbin.org/get").responseJSON { response in debugPrint if let json = response.result.value { print("JSON:  }}

JSON 的序列化由 Foundation 框架中的 JSONSerialization 接口完成。

响应回调也可以链式调用:

Alamofire.request("https://httpbin.org/get") .responseString { response in print("Response String: (response.result.value)") } .responseJSON { response in print("Response JSON: (response.result.value)") }

注意:对同一个请求使用多个响应回调需要服务器对数据进行多次序列化,每次序列化针对一个响应回调。

响应回调默认是在主派发队列中执行。然而可以为响应回调指定自定义的操作队列。

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把所有完成的请求当做是成功的请求,无论响应的内容是什么。如果响应有一个不能被接受的状态码或者MIME类型,在响应handler之前调用验证将会产生错误。

CRUD和授权
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。通常是需要一个access token来授权应用或者用户,然后才可以使用各种支持的Web服务。创建这些access token是比较麻烦的,当access token过期之后就比较麻烦了,我们需要重新创建一个新的。有许多线程安全问题要考虑。

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

响应验证

默认情况下 Alamofire 会忽略响应内容是否正确,只要请求完成就标志着成功。在响应回调调用之前调用 validata 时,若响应中有错误的网络状态码或错误的 MIME 格式的数据则会抛出错误。

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 } }

自动验证会验证 200...299 之间的状态码并验证响应数据的 Content-Type 是否和请求头的指定的 Accept 类型是否匹配。

Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in switch response.result { case .success: print("Validation Successful") case .failure(let error): print }}
手动验证
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)
    }
}

适配和重试请求

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

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

RequestAdapter

RequestAdapter协议允许每一个SessionManagerRequest在创建之前被检查和适配。一个非常特别的使用适配器方法是,在一个特定的认证类型,把Authorization header拼接到请求。

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")

响应缓存

响应的缓存操作由系统级框架 URLCache 完成。其同时提供了内存,硬盘两种缓存方式并且用户可以设置可缓存的大小。

Alamofire 默认会使用共享的 URLCache。查看 Session Manager Configurations 进行自定义。

自动验证

自动验证在200…299范围内的状态码;如果请求头中有指定Accept,那么也会验证响应头的与请求头Accept一样的Content-Type

Alamofire.request("https://httpbin.org/get").validate().responseJSON { response in
    switch response.result {
    case .success:
        print("Validation Successful")
    case .failure(let error):
        print(error)
    }
}

RequestAdapter

RequestAdapter协议允许每一个SessionManagerRequest在创建之前被检查和适配。一个非常特别的使用适配器方法是,在一个特定的认证类型,把Authorization header拼接到请求。

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

免责声明:这不是一个全面的OAuth2解决方案。这仅仅是演示如何把RequestAdapterRequestRetrier协议结合起来创建一个线程安全的恢复系统。

重申: 不要把这个例子复制到实际的开发应用中,这仅仅是一个例子。每个认证系统必须为每个特定的平台和认证类型重新定制。

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)
}

一旦OAuth2HandlerSessionManager被应用与adapterretrier,他将会通过自动恢复access token来处理一个非法的access token error,并且根据失败的顺序来重试所有失败的请求。(如果需要让他们按照创建的时间顺序来执行,可以使用他们的task identifier来排序)

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

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

HTTP 方法

HTTPMethod 枚举出了 RFC 7231 §4.3 中定义的 HTTP 方法:

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 接口的 method 参数设置这些值:

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 的 method 参数默认是 .get

响应缓存

响应缓存是使用系统的框架URLCache来处理的。它提供了内存和磁盘上的缓存,并允许我们控制内存和磁盘的大小。

默认情况下,Alamofire利用共享的URLCache。如果要自定义,下面会讲到。

RequestRetrier

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

免责声明:这不是一个全面的OAuth2解决方案。这仅仅是演示如何把RequestAdapterRequestRetrier协议结合起来创建一个线程安全的恢复系统。

重申: 不要把这个例子复制到实际的开发应用中,这仅仅是一个例子。每个认证系统必须为每个特定的平台和认证类型重新定制。

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)
}

一旦OAuth2HandlerSessionManager被应用与adapterretrier,他将会通过自动恢复access token来处理一个非法的access token error,并且根据失败的顺序来重试所有失败的请求。(如果需要让他们按照创建的时间顺序来执行,可以使用他们的task identifier来排序)

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

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

自定义响应序列化

Alamofire为data、strings、JSON和Property 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, ...)。

我们可以有多个方法来自定义所有响应元素:

  • 响应映射
  • 处理错误
  • 创建一个自定义的响应序列化器
  • 泛型响应对象序列化

请求参数编码

Alamofire 默认提供了三种参数编码方式,包括 URL,JSON,PropertyList。同时也支持遵循了 ParameterEncoding 协议的编码方式。

URLEncoding 编码方式创建了 url 编码的查询字符串并将其拼接到存在的请求字符串后或者设置为 URL 请求的 HTTP body。对于编码后的查询字符串,是直接使用,拼接还是设置为 HTTP body 取决于编码的 DestinationDestination 包含三种方式:

  • .methodDependent - 若请求方式是 GET,HEAD,DELETE,则将编码的查询字符串与存在的查询字符串进行拼接,对于其他请求方式则设置为请求的 HTTP body。
  • .queryString - 将编码的查询字符串与存在的查询字符串进行拼接。
  • .httpBody - 设置为请求的 HTTP body。

请求头中的 Content-Type 字段被设置为 application/x-www-form-urlencoded; charset=utf-8。URL 编码中并没有规定集合类型该如何进行编码。我们约定,对数组类型将[]拼接到 key 后面(foo[]=1&foo[]=2),对字典类型将中括号包围的 key 拼接在请求的键后(foo[bar]=baz`)。

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)

自定义响应序列化

Alamofire为data、strings、JSON和Property 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, ...)。

我们可以有多个方法来自定义所有响应元素:

  • 响应映射
  • 处理错误
  • 创建一个自定义的响应序列化器
  • 泛型响应对象序列化

响应映射

响应映射是自定义响应最简单的方式。它转换响应的值,同时保留最终错误和元数据。例如,我们可以把一个json响应DataResponse<Any>转换为一个保存应用模型的的响应,例如DataResponse<User>。使用DataResponse.map来进行响应映射:

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方法:

Alamofire.request("https://example.com/users/mattt").responseJSON { response in
    let userResponse = response.flatMap { json in
        try 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 in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

        completionHandler(userResponse)
    }
}

loadUser { response in
    if 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 in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

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

mapflatMap也可以用于下载响应。

获取使用 URL 编码参数的请求
let parameters: Parameters = ["foo": "bar"]// All three of these calls are equivalentAlamofire.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

参数编码

Alamofire支持三种参数编码:URLJSONPropertyList。还支持遵循了ParameterEncoding协议的自定义编码。

响应映射

响应映射是自定义响应最简单的方式。它转换响应的值,同时保留最终错误和元数据。例如,我们可以把一个json响应DataResponse<Any>转换为一个保存应用模型的的响应,例如DataResponse<User>。使用DataResponse.map来进行响应映射:

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方法:

Alamofire.request("https://example.com/users/mattt").responseJSON { response in
    let userResponse = response.flatMap { json in
        try 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 in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

        completionHandler(userResponse)
    }
}

loadUser { response in
    if 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 in
        let userResponse = response.flatMap { json in
            try User(json: json)
        }

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

mapflatMap也可以用于下载响应。

处理错误

在实现自定义响应序列化器或者对象序列化方法前,思考如何处理所有可能出现的错误是非常重要的。有两个方法: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)
}
发起使用 URL 编码参数的请求
let parameters: Parameters = [ "foo": "bar", "baz": ["a", 1], "qux": [ "x": 1, "y": 2, "z": 3 ]]// All three of these calls are equivalentAlamofire.request("https://httpbin.org/post", parameters: parameters)Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.default)Alamofire.request("https://httpbin.org/post", parameters: parameters, encoding: URLEncoding.httpBody)// HTTP body: foo=bar&baz[]=a&baz[]=1&qux[x]=1&qux[y]=2&qux[z]=3

JSONEncoding 的编码方式创建了 JSON 格式的请求参数,并设置为请求的 HTTP body。HTTP 请求头的 Content-Type 字段设置为 applicatioin/json

URL编码

URLEncoding类型创建了一个URL编码的查询字符串来设置或者添加到一个现有的URL查询字符串,或者设置URL请求的请求体。查询字符串是否被设置或者添加到现有的URL查询字符串,或者被作为HTTP请求体,决定于编码的Destination。编码的Destination有三个case:

  • .methodDependent:为GETHEADDELETE请求使用编码查询字符串来设置或者添加到现有查询字符串,并且使用其他HTTP方法来设置请求体。
  • .queryString:设置或者添加编码查询字符串到现有查询字符串
  • .httpBody:把编码查询字符串作为URL请求的请求体

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

处理错误

在实现自定义响应序列化器或者对象序列化方法前,思考如何处理所有可能出现的错误是非常重要的。有两个方法: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)
}

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

Alamofire为strings、JSON和Property 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 case
            guard 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))
            }
        }
    }

    @discardableResult
    func responseXMLDocument(
        queue: DispatchQueue? = nil,
        completionHandler: @escaping (DataResponse<ONOXMLDocument>) -> Void)
        -> Self
    {
        return response(
            queue: queue,
            responseSerializer: DataRequest.xmlResponseSerializer(),
            completionHandler: completionHandler
        )
    }
}
发起使用 JSON 编码参数的请求
let parameters: Parameters = [ "foo": [1,2,3], "bar": [ "baz": "qux" ]]// Both calls are equivalentAlamofire.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"}}

当 Alamofire 提供的参数编码方式不能满足需求时,可以创建自定义的编码方式。下面是一个自定义的 JSONStringEncoding 编码方式的例子,该方式将 string 数组的 JSON 对象编码到 Request 中。

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.urlRequest 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 }}

Alamofire 从服务器接收到的数据保存在缓存或硬盘上。到目前为止所有例子中使用 Alamofire.request 接口获取的数据都保存在缓存中。对于小数据这是很高效的,但对于较大的数据量可能会耗尽缓存。因此需要使用 Alamofire.download 接口将数据保存在硬盘的临时文件中。

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

当需要在后台下载数据时也应该使用 Alamofire.download 接口。更多信息请查看 Session Manager Configurations 章节

你可以提供一个 DownloadFileDestination 闭包用于把临时文件移动到指定的路径下。在移动临时文件前会先执行闭包中指定的 DownloadOptioins。当前支持的两种 DownloadOptions 分别是:

  • .createIntermediateDirectories - 为指定的路径创建完整的路径
  • .removePreviousFile - 移除目标路径下存在的文件
let destination: DownloadRequest.DownloadFileDestination = { _, _ in let 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 in print 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)

在下载时能够报告下载进度是非常有用的。任何 DownloadRequest 请求可以通过 downloadProgress 接口报告下载进度。

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) } }

也可以为 downloadProgress 接口指定下载进度闭包执行的派发队列。

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) } }

如果一个 DownloadRequest 请求取消或中断了,URL 会话可能会为该请求生成恢复数据,该恢复数据可用于 DownloadRequest 请求从中断的地方恢复下载。恢复数据可以从下载响应中获取,然后用于恢复下载。

class ImageRequestor { private var resumeData: Data? private var image: UIImage? func fetchImage(completion:  -> Void) { guard image == nil else { completion ; return } let destination: DownloadRequest.DownloadFileDestination = { _, _ in let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] let fileURL = documentsURL.appendPathComponent("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: self.image = UIImage(data: data) case .failure: self.resumeData = response.resumeData } } }}
使用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

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

Alamofire为strings、JSON和Property 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 case
            guard 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))
            }
        }
    }

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

泛型响应对象序列化

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

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) }")
    }
}

同样地方法可以用来处理返回对象集合的接口:

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)") }
    }
}

上传数据到服务器

上传少量的数据到服务器可以采用 JSON 或者 URL 编码参数的方式进行,这时 Alamofire.request 接口通常很高效。当需要上传的数据较大,比如文件或者 InputStream,这时需要使用 Alamofire.upload 接口。

当需要在后台上传数据时也应该使用 Alamofire.upload,更多信息请查看 Session Manager Configurations 章节。

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

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

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 } case .failure(let encodingError): print(encodingError) } })

当用户在上传时能够显示上传进度是非常友好的。任何 UploadRequest 请求都能通过 uploadProgressdoanloadProgress 接口报告上传进度和下载进度。

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 }
使用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

泛型响应对象序列化

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

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) }")
    }
}

同样地方法可以用来处理返回对象集合的接口:

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会使用苹果安全框架内置的验证方法来评估服务器提供的证书链。虽然保证了证书链是有效的,但是不能防止man-in-the-middle (MITM)攻击或者其他潜在的漏洞。为了减少MITM攻击,处理用户的敏感数据或财务信息的应用,应该使用ServerTrustPolicy提供的certificate或者public key pinning。

Statistical Metrics

Alamofire collects timings throughout the lifecycle of a Request and creates a Timeline object exposed as a property on all response types.

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

The above reports the following Timeline info:

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

In iOS and tvOS 10 and macOS 10.12, Apple introduced the new URLSessionTaskMetrics APIs. The task metrics encapsulate some fantastic statistical information about the request and response execution. The API is very similar to the Timeline, but provides many more statistics that Alamofire doesn't have access to compute. The metrics can be accessed through any response type.

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

注意,这些接口仅在 iOS,tvOS 10 和 macOS 10.12 三个平台上可用。因此,取决于您的部署环境,您需要做以下检测:

Alamofire.request("https://httpbin.org/get").responseJSON { response in if #available(iOS 10.0. *) { print(response.metrics) }}
JSON编码

JSONEncoding类型创建了一个参数对象的JOSN展示,并作为请求体。编码请求的请求头的Content-Type请求字段被设置为application/json

安全

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

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的有效性,我们可以完全控制整个验证过程。但是要谨慎使用。

cURL 命令输出

不好的调试平台会让工作变得很麻烦. 幸好, Alamofire Request 对象实现了 CustomStringConvertibleCustomDebugStringConvertible 协议,这为我们提供了很好的调试工具。

let request = Alamofire.request("https://httpbin.org/ip")print// GET https://httpbin.org/ip 

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

输出:

$ 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 建立在 URLSession 和 URL 加载系统上。为了更好的使用该框架,强烈建议要非常熟悉底层网络栈的相关概念

推荐阅读

  • URL 加载系统编程指南
  • URLSession 参考文档
  • URLCache 参考文档
  • URLAuthenticationChallenge 参考文档
使用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"}}

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的有效性,我们可以完全控制整个验证过程。但是要谨慎使用。

服务器信任策略管理者 (Server Trust Policy Manager)

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被销毁时,请求将会取消。

这些服务器信任策略将会形成下面的结果:

  • test.example.com:始终使用证书链固定的证书和启用主机验证,因此需要以下条件才能是TLS握手成功:
    • 证书链必须是有效的。
    • 证书链必须包含一个已经固定的证书。
    • Challenge主机必须匹配主机证书链的子证书。
  • insecure.expired-apis.com:将从不评估证书链,并且总是允许TLS握手成功。
  • 其他主机将会默认使用苹果提供的验证。

会话管理

顶层的 Alamofire 接口例如 Alamofire.request 使用了默认的 Alamofire.SessionManager 会话管理对象发起网络请求。该会话管理对象默认使用了 URLSessionConfiguration 进行配置。

因此下面两段代码的是等效的:

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

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

您可以为应用创建会后台任务会话管理对象,临时会话管理对象,同时也可以修改默认的会话配置,比如默认的请求头 (httpAdditionalHeaders) 或者请求超时时间 (timeoutIntervalForRequest)。

let configuration = URLSessionConfiguration.defaultlet sessionManager = Alamofire.SessionManager(configuration: configuration)

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

let configuration = URLSessionConfiguration.ephemerallet sessionManager = Alamofire.SessionManager(configuration: configuration)

var defaultHeaders = Alamofire.SessionManager.default.defaultHTTPHeadersdefaultHeaders["DNT"] = "1 (Do Not Track Enabled)"let configuration = URLSessionConfiguration.defaultconfiguration.httpAdditionalHeaders = defaultHeaderslet sessionManager = Alamofire.SessionManager(configuration: configuration)

推荐使用这种方式修改 AuthorizationContent-Type 等请求头信息。推荐使用 Alamofire.request 接口中的 headers 参数, URLRequestConvertibleParameterEncoding 等方式修改请求头信息。

属性列表编码

PropertyListEncoding根据关联格式和写选项值,使用PropertyListSerialization来创建一个参数对象的属性列表展示,并作为请求体。编码请求的请求头的Content-Type请求字段被设置为application/x-plist

服务器信任策略管理者 (Server Trust Policy Manager)

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被销毁时,请求将会取消。

这些服务器信任策略将会形成下面的结果:

  • test.example.com:始终使用证书链固定的证书和启用主机验证,因此需要以下条件才能是TLS握手成功:
    • 证书链必须是有效的。
    • 证书链必须包含一个已经固定的证书。
    • Challenge主机必须匹配主机证书链的子证书。
  • insecure.expired-apis.com:将从不评估证书链,并且总是允许TLS握手成功。
  • 其他主机将会默认使用苹果提供的验证。
子类化服务器信任策略管理者

如果我们需要一个更灵活的服务器信任策略来匹配其他行为(例如通配符域名),可以子类化ServerTrustPolicyManager,并且重写serverTrustPolicyForHost方法。

class CustomServerTrustPolicyManager: ServerTrustPolicyManager {
    override func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? {
        var policy: ServerTrustPolicy?

        // Implement your custom domain matching behavior...

        return policy
    }
}

会话代理

Alamofire 的会话管理对象默认创建了一个会话代理对象来处理 URLSession 产生的各种代理回调事件。这些代理方法实现的功能能够应付绝大部分的使用场景并且为隐藏了复杂的内部调用为用户提供了简单的上层接口。然而,您仍有可能会因为各种各样的需求而重载这些代理方法的实现。

第一种自定义 SessionDelegate 行为的方式是重载闭包。通过闭包您可以重载对应的 SessionDelegate 接口,并且其他接口的实现将保持不变。这让实现一个自定义的代理方法集合变得很容易。下面是一些可用的可重载的闭包:

/// 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的域名。

let sessionManager = Alamofire.SessionManager(configuration: URLSessionConfiguration.default)let delegate: Alamofire.SessionDelegate = sessionManager.delegatedelegate.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 默认实现的方式是继承。通过继承您可以实现完全的自定义或者仍然使用默认实现仅为接口创建一个代理。通过为接口创建代理,您可以在调用接口默认实现的前后增加日志消息,派发通知等功能。下面的例子继承了 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: ") super.urlSession( session, task: task, willPerformHTTPRedirection: response, newRequest: request, completionHandler: completionHandler ) }}
自定义编码

如果提供的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.urlRequest

        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
    }
}
子类化服务器信任策略管理者

如果我们需要一个更灵活的服务器信任策略来匹配其他行为(例如通配符域名),可以子类化ServerTrustPolicyManager,并且重写serverTrustPolicyForHost方法。

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,服务器信任评估就会验证与challenge主机名字匹配的在证书里面的主机名字。如果他们不匹配,验证失败。如果设置为false,仍然会评估整个证书链,但是不会验证子证书的主机名字。

注意:建议在实际开发中,把validateHost设置为true

请求

request,download,upload,stream 等方法的返回值 DataRequest, DownloadRequest, UploadRequestStreamRequest 均是继承于 Request。所有的 Request 实例都是由自己的会话管理对象创建,并且不会直接初始化。

每个子类都有一些特殊的方法比如 authenticate, validate, responseJSONuploadProgress,这些方法均返回调用者以便可以进行链式调用。

请求可以被挂起,恢复,取消:

  • suspend(): 挂起底层任务和派发队列。
  • resume(): 恢复任务和派发队列。如果会话管理对象没有设置 startRequestsImmediatelytrue,那么请求需要调用 resume() 才能开始。
  • cancel(): 取消任务,产生错误信息并将错误信息传递到响应回调。
手动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)

验证主机

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

注意:建议在实际开发中,把validateHost设置为true

验证证书链

Pinning certificate 和 public keys 都可以通过validateCertificateChain参数拥有验证证书链的选项。把它设置为true,除了对Pinning certificate 和 public keys进行字节相等检查外,还将会验证整个证书链。如果是false,将会跳过证书链验证,但还会进行字节相等检查。

还有很多情况会导致禁用证书链认证。最常用的方式就是自签名和过期的证书。在这些情况下,验证始终会失败。但是字节相等检查会保证我们从服务器接收到证书。

注意:建议在实际开发中,把validateCertificateChain设置为true

请求路由

随着 App 变得复杂,使用通用模式创建你自己的网络栈就变得非常重要了。其中一个重要的设计就是如何路由你的请求。遵循 URLConvertibleURLRequestConvertible 协议的 Router 就变得非常有用。

遵循 URLConvertible 协议的类可以用来构造 URLs,然后将 URLs 用来构造 URL 请求。String, URL, 和 URLComponents 都遵循了 URLConvertible 协议,这三个类的对象均可以作为 url 参数传递给 request, upload, 和 download 方法:

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(.post, URLComponents)

与 web 服务器交互时推荐通过实现 URLConvertible 协议来做域名型模型与服务器资源的映射。

HTTP请求头

可以直接在请求方法添加自定义HTTP请求头,这有利于我们在请求中添加请求头。

let headers: HTTPHeaders = [
    "Authorization": "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
    "Accept": "application/json"
]

Alamofire.request("https://httpbin.org/headers", headers: headers).responseJSON { response in
    debugPrint(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实例。

验证证书链

Pinning certificate 和 public keys 都可以通过validateCertificateChain参数拥有验证证书链的选项。把它设置为true,除了对Pinning certificate 和 public keys进行字节相等检查外,还将会验证整个证书链。如果是false,将会跳过证书链验证,但还会进行字节相等检查。

还有很多情况会导致禁用证书链认证。最常用的方式就是自签名和过期的证书。在这些情况下,验证始终会失败。但是字节相等检查会保证我们从服务器接收到证书。

注意:建议在实际开发中,把validateCertificateChain设置为true

应用传输安全 (App Transport Security)

从iOS9开始,就添加了App Transport Security (ATS),使用ServerTrustPolicyManager和多个ServerTrustPolicy对象可能没什么影响。如果我们不断看到CFNetwork SSLHandshake failed (-9806)错误,我们可能遇到了这个问题。苹果的ATS系统重写了整个challenge系统,除非我们在plist文件中配置ATS设置来允许应用评估服务器信任。

<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连接是否使用一个允许的密码套件。在某些情况下,它需要设置为NONSExceptionAllowsInsecureHTTPLoads必须设置为YES,然后SessionDelegate才能接收到challenge回调。一旦challenge回调被调用,ServerTrustPolicyManager将接管服务器信任评估。如果我们要连接到一个仅支持小于1.2版本的TSL主机,那么还要指定NSTemporaryExceptionMinimumTLSVersion

注意:在实际开发中,建议始终使用有效的证书。

类型安全路由
extension User: URLConvertible { static let baseURLString = "https://example.com" func asURL() throws -> URL { let urlString = User.baseURLString   "/users//" return try urlString.asURL() }}

let user = User(username: "mattt")Alamofire.request // https://example.com/users/mattt

实现了 URLRequestConvertible 协议的类型可以用来构造 URL 请求。URLRequest 默认实现了 URLRequestConvertible 协议,这使得 URLRequest 可直接传递给 request,upload,download等方法(推荐使用这种方式实现自定义 HTTP body)

let url = URL(string: "https://httpbin.org/post")!var urlRequest = URLRequesturlRequest.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 协议以确保请求端点的一致性。这种方法可以用于抽象出服务器端不一致并提供类型安全路由,以及管理认证凭证和其他状态

认证

认证是使用系统框架URLCredentialURLAuthenticationChallenge实现的。

应用传输安全 (App Transport Security)

从iOS9开始,就添加了App Transport Security (ATS),使用ServerTrustPolicyManager和多个ServerTrustPolicy对象可能没什么影响。如果我们不断看到CFNetwork SSLHandshake failed (-9806)错误,我们可能遇到了这个问题。苹果的ATS系统重写了整个challenge系统,除非我们在plist文件中配置ATS设置来允许应用评估服务器信任。

<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连接是否使用一个允许的密码套件。在某些情况下,它需要设置为NONSExceptionAllowsInsecureHTTPLoads必须设置为YES,然后SessionDelegate才能接收到challenge回调。一旦challenge回调被调用,ServerTrustPolicyManager将接管服务器信任评估。如果我们要连接到一个仅支持小于1.2版本的TSL主机,那么还要指定NSTemporaryExceptionMinimumTLSVersion

注意:在实际开发中,建议始终使用有效的证书。

网络可达性 (Network Reachability)

NetworkReachabilityManager监听WWANWiFi网络接口和主机地址的可达性变化。

let manager = NetworkReachabilityManager(host: "www.apple.com")

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

manager?.startListening()

注意:要确保manager被强引用,否则会接收不到状态变化。另外,在主机字符串中不要包含scheme,也就是说要把https://去掉,否则无法监听。

当使用网络可达性来决定接下来要做什么时,有以下几点需要重点注意的:

  • 不要使用Reachability来决定是否发送一个网络请求。
    • 我们必须要发送请求。
  • 当Reachability恢复了,要重试网络请求。
    • 即使网络请求失败,在这个时候也非常适合重试请求。
  • 网络可达性的状态非常适合用来决定为什么网络请求会失败。
    • 如果一个请求失败,应该告诉用户是离线导致请求失败的,而不是技术错误,例如请求超时。

有兴趣的可以看看WWDC 2012 Session 706, "Networking Best Practices"。

API 抽象参数
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: 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 bar&offset=50
支持的认证方案
  • HTTP Basic
  • HTTP Digest
  • Kerberos
  • NTLM

网络可达性 (Network Reachability)

NetworkReachabilityManager监听WWANWiFi网络接口和主机地址的可达性变化。

let manager = NetworkReachabilityManager(host: "www.apple.com")

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

manager?.startListening()

注意:要确保manager被强引用,否则会接收不到状态变化。另外,在主机字符串中不要包含scheme,也就是说要把https://去掉,否则无法监听。

当使用网络可达性来决定接下来要做什么时,有以下几点需要重点注意的:

  • 不要使用Reachability来决定是否发送一个网络请求。
    • 我们必须要发送请求。
  • 当Reachability恢复了,要重试网络请求。
    • 即使网络请求失败,在这个时候也非常适合重试请求。
  • 网络可达性的状态非常适合用来决定为什么网络请求会失败。
    • 如果一个请求失败,应该告诉用户是离线导致请求失败的,而不是技术错误,例如请求超时。

有兴趣的可以看看WWDC 2012 Session 706, "Networking Best Practices"。

FAQ

CRUD & Authorization
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 .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/" case .updateUser(let username, _): return "/users/" case .destroyUser(let username): return "/users/" } } // MARK: URLRequestConvertible func asURLRequest() throws -> URLRequest { let url = try Router.baseURLString.asURL() var urlRequest = URLRequest(url: url.appendingPathComponent 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 // GET https://example.com/users/mattt
HTTP Basic认证

在合适的时候,在一个请求的authenticate方法会自动提供一个URLCredentialURLAuthenticationChallenge

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 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 in
        debugPrint(response)
}

FAQ

Alamofire的起源是什么?

Alamofire是根据 Alamo Fire flower 命名的,是一种矢车菊的混合变种,德克萨斯的州花。

Adapting and Retrying Requests

当今的很多 web 服务都可以通过授权系统进行访问。其中最常用的是 OAuth。OAuth 会生成一个访问令牌来授权你的应用访问权限内的 web 服务。创建令牌可能会很麻烦,令牌过期需要考虑很多线程安全的问题,这会让情况变得更复杂。

RequestAdapterRequestRetrier 协议让创建线程安全的授权系统变得容易。

RequestAdapter 协议允许 SessionManager 在创建 Request 前为 Request 做额外的检查和适配工作。比较常用的应用场景是为请求拼接授权参数。

class AccessTokenAdapter: RequestAdapter { private let accessToken: String init(accessToken: String) { self.accessToken = accessToken } func adapt(_ urlRequest: URLRequest) throws -> URLRequest { var urlRequest = urlRequest if urlRequest.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 协议允许网络请求发生错误时重新发起请求。通过同时实现 RequestAdapterRequestRetrier 协议,您可以为 OAuth1,OAuth2,基本授权,重试策略创建一个证书刷新系统。您能实现的功能不局限于此。下面的例子展示了 OAuth2 令牌的刷新流程。

免责声明:不是一个全局的 OAuth2 解决方案。下面的代码仅作为简单示例展示了如何通过 RequestAdapterRequestRetrier 协议来实现线程安全的刷新系统。

重申,不要拷贝下面的示例代码到您的产品中。该代码片段仅能作为示例。每一个授权系统应该基于平台和授权类型做相应的修改。

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 url = urlRequest.url, url.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 = oauthHandlersessionManager.retrier = oauthHandlerlet urlString = "(baseURLString)/some/endpoint"sessionManager.request(urlString).validate().responseJSON { response in debugPrint}

SessionManageradapterretrier 被设置为 OAuth2Handler后,当令牌失效时,便会自动刷新令牌并尝试按失败的顺序重新发起请求。

如果您想按创建网络请求的顺序重新发起请求,您可以通过网络请求任务的 id 进行排序。

该示例仅检查了响应的 401 状态码,作为检测失效令牌的例子这已经足够。O在实际产品中,您应该还要检测响应头中的 reamlwww-authenticate 等字段。

还需要注意的是该授权系统可以在多个会话管理对象间共享。比如,您可以为同一个 web 服务集同时使用 defaultephemeral 会话配置。上面的例子允许 oauthHandler 实例对象在多个会话管理对象间共享并管理各自的刷新流程。

使用URLCredential认证
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)
}

注意:使用URLCredential来做认证,如果服务器发出一个challenge,底层的URLSession实际上最终会发两次请求。第一次请求不会包含credential,因为可能会触发服务器发出一个challenge。这个challenge会被Alamofire接收,credential会被添加,并且URLSessin会重新获取请求。

Alamofire的起源是什么?

Alamofire是根据 Alamo Fire flower 命名的,是一种矢车菊的混合变种,德克萨斯的州花。

Router和Request Adapter的逻辑是什么?

简单和静态的数据,例如paths、parameters和共同的headers放在Router。动态的数据,例如一个Authorization header,它的值会随着一个认证系统变化,放在RequestAdapter

动态的数据必须放在ReqeustAdapter的原因是要支持重试操作。当重试一个请求时,原来的请求不会重新建立,也就意味着Router不会再重新调用。RequestAdapter可以重新调用,这可以让我们在重试请求之前更新原始请求的动态数据。


自定义响应序列化器

过去在实现自定义响应序列化器或对象序列化方法时着重考虑的是错误信息的处理。这里有两个可选项:对错误信息不做任何处理直接向下传递,由用户在响应回调处处理;或者为您的应用定义一个包含所有错误类型的 Error 枚举类。

下面的 BackendError 枚举类在后面的例子中也会出现:

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 为 strings,JSON,property lsits 提供了内置的响应序列化器,您也可以为 Alamofire.DataRequestAlamofire.DownloadRequest 进行扩展。

下面的例子展示了响应序列化器使用 Ono 的实现方式:

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 } 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 ) }}

通用序列化可以进行自动,类型安全的对象序列化。

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: , 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 if let user = response.result.value { print("User: { username: (user.username), name: (user.name) }") }}

相同的方法也可以用于处理终端返回的对象集合:

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 } } } 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: , 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 if let users = response.result.value { users.forEach { print") } }}

将数据下载到文件

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

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

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

Router和Request Adapter的逻辑是什么?

简单和静态的数据,例如paths、parameters和共同的headers放在Router。动态的数据,例如一个Authorization header,它的值会随着一个认证系统变化,放在RequestAdapter

动态的数据必须放在ReqeustAdapter的原因是要支持重试操作。当重试一个请求时,原来的请求不会重新建立,也就意味着Router不会再重新调用。RequestAdapter可以重新调用,这可以让我们在重试请求之前更新原始请求的动态数据。


安全性

在与 web 服务器交互传输敏感数据时应该使用安全的 HTTPS 连接。默认情况下,Alamofire 会使用苹果提供的 Security 框架对服务器提供的证书串进行验证。这样仅仅能确保服务器端证书是否有效,并不能防止中间人攻击 man-in-the-middle 或其他潜在的漏洞。为了降低遭受中间人攻击的可能性,应用在处理敏感用户的数据或金融信息时应该配合使用证书或 ServerTrustPolicy 提供的公钥锁定

下载文件目标

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

  • .createIntermediateDirectories:如果指定了目标URL,将会创建中间目录。
  • .removePreviousFile:如果指定了目标URL,将会移除之前的文件
let destination: DownloadRequest.DownloadFileDestination = { _, _ in
    let 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 in
    print(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)

作者:Lebron_James
链接:
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。

继承 Server Trust Policy Manager
下载进度

所有的DownloadRequest都可以使用downloadProgressAPI来反馈下载进度。

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中执行。

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)
    }
}
验证证书串
<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>
恢复下载

如果一个DownloadRequest被取消或中断,底层的URL会话会生成一个恢复数据。恢复数据可以被重新利用并在中断的位置继续下载。恢复数据可以通过下载响应访问,然后在重新开始请求的时候被利用。

重要:在最新的iOS 10, macOS 10.12, tvOS 10, watchOS 3中,resumeData会被后台URL会话配置破坏。因为在resumeData的生成逻辑有一个底层的bug,不能恢复下载。具体情况可以到Stack Overflow看看。目前的最新情况是:在iOS 10.2中已经修复了这个bug。

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.appendPathComponent("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
            }
        }
    }
}

网络可用性

NetworkReachabilityManager 可用于监听 WWAN 和 WiFi 网络到指定主机或 IP 地址的连接状态。

let manager = NetworkReachabilityManager(host: "www.apple.com")manager?.listener = { status in print("Network Status Changed: }manager?.startListening()

请确保对 网络状态监听对象 有强引用,否则不会监听到任何网络状态。

在监听网络状态时需要注意以下几点:

  • 不要根据网络状态来决定是否发送网络请求。
    • 只管发送就行
  • 当网络恢复连接,对失败的网络请求重新发起请求。
    • 尽管重新发起请求仍有可能失败,但您仍应该尝试。
  • 网络状态有助于分析出请求失败原因。
    • 如果网络请求失败,提示用户网络处于离线状态要比更具体的错误信息比如"请求超时"等更友好。

更多信息请参考 WWDC 2012 Session 706, "Networking Best Practices" for more info.

The following radars have some effect on the current implementation of Alamofire.

  • rdar://21349340 - Compiler throwing warning due to toll-free bridging issue in test case
  • rdar://26761490 - Swift string interpolation causing memory leak with common usage
  • rdar://26870455 - Background URL Session Configurations do not work in the simulator
  • rdar://26849668 - Some URLProtocol APIs do not properly handle URLRequest

上传数据到服务器

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

Alamofire 名字由来

Alamofire 花,矢车菊的一种,是德克萨斯州的官方州花。

上传数据
let imageData = UIPNGRepresentation(image)!

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

请求路由和请求适配器的区别

资源路径,请求参数,公共请求头这些静态数据属于 路由 范畴。认证 头这类会随着认证系统发生变化的动态数据属于 请求适配器 范畴。

Alamofire 由 Alamofire 软件基金会 所有并维护。您可以通过关注我们的 Twitter 官方账号 @AlamofireSF 来获取最新的更新发布消息。

Alamofire 在 MIT 开源协议下发布。更多信息请查看 LICENSE 文件。

欢迎关注我的简书,我会定期做一些技术分享:)

上传文件
let fileURL = Bundle.main.url(forResource: "video", withExtension: "mov")

Alamofire.upload(fileURL, to: "https://httpbin.org/post").responseJSON { response in
    debugPrint(response)
}
上传多部分表单数据
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都可以使用uploadProgressdownloadProgress 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 in
        debugPrint(response)
}

统计指标

时间表

Alamofire在一个请求周期内收集时间,并创建一个Tineline对象,它是响应类型的一个属性。

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

上面的Timeline信息包括:

  • Latency: 0.428 seconds (延迟)
  • Request Duration: 0.428 seconds (请求时间)
  • Serialization Duration: 0.001 seconds (序列化时间)
  • Total Duration: 0.429 seconds (总时间)
URL会话任务指标

在iOS和tvOS 10和macOS 10.12中,苹果发布了新的URLSessionTaskMetrics APIs。这个任务指标封装了关于请求和响应执行的神奇统计信息。这个API和Timeline非常相似,但是提供了很多Alamofire没有提供的统计信息。这些指标可以通过任何响应去访问。

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

注意:这些API只能在iOS和tvOS 10和macOS 10.12中使用。所以,根据部署目标,可能需要加入版本判断:

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

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"

第一部分完。

第二篇文章 >>

本文由星彩网app下载发布于计算机编程,转载请注明出处:基本用法,高级用法

TAG标签: 星彩网app下载
Ctrl+D 将本页面保存为书签,全面了解最新资讯,方便快捷。