IOS框架使用: Alamofire 5

原创:知识点总结性文章
创作不易,请珍惜,之后会持续更新,不断完善
个人比较喜欢做笔记和写总结,毕竟好记性不如烂笔头哈哈,这些文章记录了我的IOS成长历程,希望能与大家一起进步
温馨提示:由于简书不支持目录跳转,大家可通过command + F 输入目录标题后迅速寻找到你所需要的内容

目录

  • 一、系统提供的原生框架URLSession的使用
    • 1、请求网络的流程
    • 2、属性
    • 3、HTTP
  • 二、初涉Alamofire
    • 1、发起请求
    • 2、HTTP Methods
    • 3、请求参数和参数编码器
    • 4、HTTP Headers
    • 5、响应验证
    • 6、响应处理
    • 7、身份验证
    • 8、下载文件
    • 9、上传数据到服务器
    • 10、网络可达性
  • 三、玩转Alamofire
    • 1、Session
    • 2、Request
    • 3、Security
  • Demo
  • 参考文献

一、系统提供的原生框架URLSession的使用

1、请求网络的流程

a、请求网络的基本方式
  • URLSession.shared提供了一个共享的单例会话对象,它为创建任务提供了一个默认行为。使用共享会话仅用几行代码就可以将URL的内容获取到。
  • dataTask创建一个网络会话数据任务。
  • 网络任务默认是挂起的,调用resume开始进行连接请求网络
  • 请求成功或者失败都会返回结果闭包,其实闭包只是一层封装,真正来的是URLSession的代理
  • 在下面的过程中,我们省略了一个重要的东西:URLSessionConfiguration
let url = URL(string: "https://www.baidu.com")!
URLSession.shared.dataTask(with: url)
{ (data, response, error) in
    if error == nil
    {
        print("请求网络成功:\(String(describing: response))" )
    }
}.resume()

输出结果为:

请求网络成功:Optional(<NSHTTPURLResponse: 0x600001b1ee20> { URL: https://www.baidu.com/ } { Status Code: 200, Headers {
    "Content-Encoding" =     (
        gzip
    );
    "Content-Length" =     (
        1145
    );
    "Content-Type" =     (
        "text/html"
    );
    Date =     (
        "Thu, 21 Jan 2021 07:15:16 GMT"
    );
    Server =     (
        bfe
    );
} })

b、区别Configuration中的default与ephemeral
  • default:默认模式,通常我们用这种模式就足够了。default模式下系统会创建一个持久化的缓存并在用户的钥匙串中存储证书
  • ephemeral:系统没有进行任何持久性存储,所有内容的生命周期都与session相同,当session无效时,所有内容自动释放
let defaultConfiguration = URLSessionConfiguration.default
let ephemeralConfiguration = URLSessionConfiguration.ephemeral
print("default 沙盒大小: \(String(describing: defaultConfiguration.urlCache?.diskCapacity))")
print("default 内存大小: \(String(describing: defaultConfiguration.urlCache?.memoryCapacity))")
print("ephemeral 沙盒大小: \(String(describing: ephemeralConfiguration.urlCache?.diskCapacity))")
print("ephemeral 内存大小: \(String(describing: ephemeralConfiguration.urlCache?.memoryCapacity))")

从输出结果中可以看到ephemeral的沙盒大小为0,而default有一个沙盒大小,即系统为default提供了一定大小的沙盒来存储证书等内容。

default 沙盒大小: Optional(10000000)
default 内存大小: Optional(512000)
ephemeral 沙盒大小: Optional(0)
ephemeral 内存大小: Optional(512000)

c、切换到后台停止下载问题
  • background会创建一个可以在后台甚至APP已经关闭的时候仍然传输数据的会话
  • background模式与default模式非常相似,不过background模式会用一个独立线程来进行数据传输
  • background模式可以在程序挂起,退出,崩溃的情况下,在重新启动APP时继续运行task
  • 可以利用标识符来进行恢复task。后台Session一定要在创建的时候赋予一个唯一的identifier,这样在APP下次运行的时候,能够根据identifier来进行相关的区分
  • 如果用户强制关闭了APP,IOS 系统会关闭所有的background Session,只有当用户下次启动了APP,数据传输任务才会继续执行
// 配置Configuration
let backgroundConfiguration = URLSessionConfiguration.background(withIdentifier: createID())

// 创建URLSession
let backgroundURLSession = URLSession.init(configuration: backgroundConfiguration, delegate: self, delegateQueue: OperationQueue.main)

// 开始下载
backgroundURLSession.downloadTask(with: downloadUrl).resume()
❶ 下载完成后进行沙盒迁移,拷贝下载完成的文件到用户目录(文件名以时间戳命名)
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
{
    print("下载完成后文件位置:\(location)")
    let originalLocationPath = location.path
    let destinationPath = NSHomeDirectory() + "/Documents/" + currentDateTurnString() + ".mp4"
    print("文件移动后的位置:\(destinationPath)")
    let fileManager = FileManager.default
    try! fileManager.moveItem(atPath: originalLocationPath, toPath: destinationPath)
}

输出结果为:

下载完成后文件位置:file:///Users/xiejiapei/Library/Developer/CoreSimulator/Devices/9AF7A16E-76FF-4711-8BFA-66DDF38D03F2/data/Containers/Data/Application/67D3E5B6-989E-4137-9ED0-06F852F31CA3/Library/Caches/com.apple.nsurlsessiond/Downloads/com.xiejiapei.UseAlamofire/CFNetworkDownload_LX9nZT.tmp
文件移动后的位置:/Users/xiejiapei/Library/Developer/CoreSimulator/Devices/9AF7A16E-76FF-4711-8BFA-66DDF38D03F2/data/Containers/Data/Application/67D3E5B6-989E-4137-9ED0-06F852F31CA3/Documents/20210121145809.mp4
❷ 计算下载进度
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
{
    print("bytesWritten: \(bytesWritten)\n totalBytesWritten: \(totalBytesWritten)\n totalBytesExpectedToWrite: \(totalBytesExpectedToWrite)")
    
    print("下载进度条:\( Double(totalBytesWritten) / Double(totalBytesExpectedToWrite) )")
}

输出结果为:

......
bytesWritten: 139246
 totalBytesWritten: 17218551
 totalBytesExpectedToWrite: 17244422
下载进度条:0.998499746758691
bytesWritten: 25871
 totalBytesWritten: 17244422
 totalBytesExpectedToWrite: 17244422
下载进度条:1.0
❸ 保存后台下载时的completionHandler,用于开启后台下载权限
var backgroundSessionCompletionHandler: (() -> Void)?

func application(_ application: UIApplication, handleEventsForBackgroundURLSession identifier: String, completionHandler: @escaping () -> Void)
{
    self.backgroundSessionCompletionHandler = completionHandler
}

如果不这样做,当用户将APP切换到后台的时候下载就会直接中断,但是当回到APP的时候会继续之前的进度进行下载。

2021-01-21 15:34:12.825608+0800 UseAlamofire[53235:1879599] BackgroundSession <01EC4B4A-A81E-4D5B-9004-CF615DEDFD87> connection to background transfer daemon interrupted
❹ 调用保存的后台下载回调,告诉系统及时更新屏幕
func urlSessionDidFinishEvents(forBackgroundURLSession session: URLSession)
{
    print("让后台任务保持下载")
    DispatchQueue.main.async {
        guard let appDelegate = UIApplication.shared.delegate as? AppDelegate, let backgroundHandle = appDelegate.backgroundSessionCompletionHandler else { return }
        backgroundHandle()
    }
}

输出结果为:

让后台任务保持下载

需要注意的是,当我们切换到后台的时候,任务虽然还在下载,但是下载进度将不会再打印到控制台上,你不要以为下载终止了(😂,我起初就是这样以为的,还查了些资料)。


2、属性

常规属性
  • identifier:配置对象的后台会话标识符
  • httpAdditionalHeaders:与请求一起发送的附加头文件的字典
  • networkServiceType:网络服务的类型
  • allowsCellularAccess:一个布尔值,用于确定是否应通过蜂窝网络进行连接
  • timeoutIntervalForRequest:等待其他数据时使用的超时间隔
  • timeoutIntervalForResource:资源请求应该允许的最大时间量
  • sharedContainerIdentifier:应该下载后台URL会话中的文件的共享容器的标识符
  • waitsForConnectivity:一个布尔值,指示会话是否应等待连接变为可用或者立即失败
设置Cookie政策
  • httpCookieAcceptPolicy:决定何时应该接受Cookie的策略常量
  • httpShouldSetCookies:一个布尔值,用于确定请求是否应包含来自Cookie存储的内容
  • httpCookieStorage:管理cookie存储的单一对象(共享实例)
  • HTTPCookie:表示HTTP cookie的对象。它是一个不可变的对象,从包含cookie属性的字典中初始化
设置安全策略
  • tlsMaximumSupportedProtocol:在此会话中进行连接时客户端应请求的最大TLS协议版本
  • tlsMinimumSupportedProtocol:协议协商期间应该接受的最小TLS协议
  • urlCredentialStorage:提供身份验证凭据的凭证存储
设置缓存策略
  • urlCache:用于向会话中的请求提供缓存响应的URL缓存
  • requestCachePolicy:一个预定义常量,用于确定何时从缓存中返回响应
支持后台转移
  • sessionSendsLaunchEvents:一个布尔值,指示在传输完成时是否应该在后台继续或启动应用程序
  • isDiscretionary:一个布尔值,用于确定是否可以根据系统的判断来调度后台任务以获得最佳性能
支持自定义协议
  • protocolClasses:在会话中处理请求的额外协议子类的数组
  • URLProtocol:一个NSURLProtocol对象处理加载协议特定的URL数据。NSURLProtocol类本身是一个抽象类,可以为与特定URL方案的URL处理基础设施。您可以为您的应用支持的任何自定义协议或URL方案创建子类
支持多路径TCP
  • multipathServiceType:指定用于通过Wi-Fi和蜂窝接口传输数据的多路径TCP连接策略的服务类型
  • URLSessionConfiguration.MultipathServiceType:指定多路径TCP使用的服务类型的常量
设置HTTP策略和代理属性
  • httpMaximumConnectionsPerHost:同时连接到给定主机的最大数量
  • httpShouldUsePipelining:一个布尔值,用于确定会话是否应使用HTTP流水线
  • connectionProxyDictionary:包含有关在此会话中使用的代理信息的字典
支持连接变化
  • waitsForConnectivity:一个布尔值,指示会话是否应等待连接变为可用或者立即失败
默认缓存策略
  • NSURLRequestUseProtocolCachePolicy = 0:如果请求拥有一个缓存的响应,那么URL加载系统会检查这个响应来决定。假如内容必须重新生效,将建立一个连向源端的连接来查看内容是否发生变化。假如内容没有变化,那么响应就从本地缓存返回数据。如果内容变化了,那么数据将从源端获取。

3、HTTP

a、三次握手
为什么不是两次握手?

没有第三次握手,服务端就不知道客户端是否可以接收到消息,即确定服务端和客户端两者都可以发送消息也可以接收消息。

为什么不是四次握手?

通过三次握手已经确定服务端和客户端两者都可以发送消息也可以接收消息了,没必要再进行第四次握手。


b、四次挥手
为什么不是两次挥手?

因为服务器需要通过第三次挥手将还没有传输完成的数据全部传输完成。

为什么不是三次挥手?

因为服务器需要通过第三次挥手将还没有传输完成的数据全部传输完成,而客户端需要通过第四次挥手告诉服务端第三次挥手发送过来的数据已经全部接收完毕,通过四次挥手可以保证整个通讯过程的完整性。


c、使用Wireshark工具抓包
❶ 两个终端之间达到握手

当在下图左侧的终端输入的时候会自动显示到右侧终端进行同步,即两个终端之间成功达到了握手。

❷ 进入Loopback界面
❸ 在终端重新进行握手让Loopback开始监听
❹ 监听到三次握手流程中Seq和Ack的变化
❺ 在终端断掉握手让Loopback监听四次挥手
❺ 监听到四次挥手流程中Seq和Ack的变化

d、OSL七层协议
  • 应用层(HTTP、FTP、DNS):文件传输,电子邮件,文件服务,虚拟终端
  • 表示层(没有协议):数据格式化,代码转换,数据加密
  • 会话层(没有协议):解除或建立与别的接点的联系
  • 传输层(TCP、UDP):提供端对端的接口
  • 网络层(IP):为数据包选择路由
  • 数据链路层(SLIP):传输有地址的帧以及错误检测功能
  • 物理层(ISO2110):以二进制数据形式在物理媒体上传输数据
七层协议
包装与解包

二、初涉Alamofire

AlamofireHTTP 网络请求提供了一个优雅且可组合的接口。它没有实现自己的 HTTP 网络功能。取而代之的是,它建立在由Foundation 框架提供的URL 加载系统之上。系统的核心是 URLSessionURLSessionTask子类。

1、发起请求

Alamofire 为发出 HTTP 请求提供了多种方便的方法。

a、最简单的请求方式只需提供一个可以转换为 URL 的 String
  • 链式语法
  • 返回JSON
  • 直接放入String,而不是URL
AF.request("https://httpbin.org/get").response
{ response in
    debugPrint(response)
}

输出结果为:

[Request]: GET https://httpbin.org/get
    [Headers]: None
    [Body]: None
[Response]:
    [Status Code]: 200
    [Headers]:
        access-control-allow-credentials: true
        Access-Control-Allow-Origin: *
        Content-Length: 426
        Content-Type: application/json
        Date: Mon, 25 Jan 2021 05:35:54 GMT
        Server: gunicorn/19.9.0
    [Body]:
        {
          "args": {}, 
          "headers": {
            "Accept": "*/*", 
            "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", 
            "Accept-Language": "en;q=1.0", 
            "Host": "httpbin.org", 
            "User-Agent": "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1", 
            "X-Amzn-Trace-Id": "Root=1-600e58ba-22848fce5ae105571e598f1e"
          }, 
          "origin": "222.76.251.163", 
          "url": "https://httpbin.org/get"
        }
[Network Duration]: 1.434872031211853s
[Serialization Duration]: 0.0s
[Result]: success(Optional(426 bytes))

这实际上是一种缩写形式,它的完整定义如下。此方法创建一个 DataRequest,允许传入多个参数。

open func request<Parameters: Encodable>(
    _ convertible: URLConvertible,
    method: HTTPMethod = .get,
    parameters: Parameters? = nil,
    encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default,
    headers: HTTPHeaders? = nil,
    interceptor: RequestInterceptor? = nil
) -> DataRequest

b、为遵循 Alamofire 的 URLRequestConvertible 协议的任何类型创建 DataRequest
open func request(
    _ urlRequest: URLRequestConvertible,
    interceptor: RequestInterceptor? = nil
) -> DataRequest

2、HTTP Methods

a、作为 method 参数传递给 AF.request API

不同的 HTTP 方法可能有不同的语义,需要不同的参数编码,这取决于服务器的期望。例如,URLSessionAlamofire 不支持在 GET 请求中传递 body 数据,否则将返回错误。

public struct HTTPMethod: RawRepresentable, Equatable, Hashable 
{
    public static let connect = HTTPMethod(rawValue: "CONNECT")
    public static let delete = HTTPMethod(rawValue: "DELETE")
    public static let get = HTTPMethod(rawValue: "GET")
    public static let head = HTTPMethod(rawValue: "HEAD")
    public static let options = HTTPMethod(rawValue: "OPTIONS")
    public static let patch = HTTPMethod(rawValue: "PATCH")
    public static let post = HTTPMethod(rawValue: "POST")
    public static let put = HTTPMethod(rawValue: "PUT")
    public static let trace = HTTPMethod(rawValue: "TRACE")

    public let rawValue: String

    public init(rawValue: String) 
    {
        self.rawValue = rawValue
    }
}

这些值可以作为 method 参数传递给 AF.request API

AF.request("https://httpbin.org/get")
AF.request("https://httpbin.org/post", method: .post)
AF.request("https://httpbin.org/put", method: .put)
AF.request("https://httpbin.org/delete", method: .delete)

b、Alamofire 还提供了对 URLRequest 的扩展

以桥接将字符串返回到 HTTPMethod 值的 httpMethod 属性。

extension URLRequest
{
    /// Returns the `httpMethod` as Alamofire's `HTTPMethod` type.
    public var method: HTTPMethod?
    {
        get { httpMethod.flatMap(HTTPMethod.init) }
        set { httpMethod = newValue?.rawValue }
    }
}

c、扩展HTTPMethod类型添加自定义值

如果需要使用 AlamofireHTTPMethod 类型不支持的 HTTP 方法,可以扩展该类型以添加自定义值。

extension HTTPMethod
{
    static let custom = HTTPMethod(rawValue: "CUSTOM")
}

3、请求参数和参数编码器

a、请求参数

Alamofire 支持将遵守 Encodable 协议的类型作为请求参数。这些请求参数通过遵循 ParameterEncoder 协议的参数编码器进行传递并添加到 URLRequest 中,最后通过网络发送。Alamofire 包含两种遵循 ParameterEncoder 协议的参数编码器:JSONParameterEncoderURLEncodedFormParameterEncoder 。这两种参数编码器涵盖了最常见的编码方式。

struct Login: Encodable
{
    let email: String
    let password: String
}

let login = Login(email: "2170928274@qq.com", password: "19970118")

AF.request("https://httpbin.org/post", method: .post, parameters: login, encoder: JSONParameterEncoder.default).response
{ response in
    debugPrint(response)
}

输出结果为:

[Request]: POST https://httpbin.org/post
    [Headers]:
        Content-Type: application/json
    [Body]:
        {"email":"2170928274@qq.com","password":"19970118"}
[Response]:
    [Status Code]: 200
    [Headers]:
        access-control-allow-credentials: true
        Access-Control-Allow-Origin: *
        Content-Length: 682
        Content-Type: application/json
        Date: Mon, 25 Jan 2021 06:01:51 GMT
        Server: gunicorn/19.9.0
    [Body]:
        {
          "args": {}, 
          "data": "{\"email\":\"2170928274@qq.com\",\"password\":\"19970118\"}", 
          "files": {}, 
          "form": {}, 
          "headers": {
            "Accept": "*/*", 
            "Accept-Encoding": "br;q=1.0, gzip;q=0.9, deflate;q=0.8", 
            "Accept-Language": "en;q=1.0", 
            "Content-Length": "51", 
            "Content-Type": "application/json", 
            "Host": "httpbin.org", 
            "User-Agent": "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1", 
            "X-Amzn-Trace-Id": "Root=1-600e5ecf-01e2ada305b5bd8a0e0e0dc6"
          }, 
          "json": {
            "email": "2170928274@qq.com", 
            "password": "19970118"
          }, 
          "origin": "222.76.251.163", 
          "url": "https://httpbin.org/post"
        }
[Network Duration]: 2.090991973876953s
[Serialization Duration]: 0.0s
[Result]: success(Optional(682 bytes))

b、使用 URL 编码参数的 GET 请求(默认编码方式)
// https://httpbin.org/get?foo=bar
let parameters = ["foo": "bar"]

// 下面三种方法都是等价的
AF.request("https://httpbin.org/get", parameters: parameters)
AF.request("https://httpbin.org/get", parameters: parameters, encoder: URLEncodedFormParameterEncoder.default)
AF.request("https://httpbin.org/get", parameters: parameters, encoder: URLEncodedFormParameterEncoder(destination: .methodDependent))

输出结果为:

[Request]: GET https://httpbin.org/get?foo=bar

c、使用 URL 编码参数的 POST 请求
// HTTP body: "qux[]=x&qux[]=y&qux[]=z&baz[]=a&baz[]=b&foo[]=bar"
let parameters: [String: [String]] =
[
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

// 下面三种方法都是等价的
AF.request("https://httpbin.org/post", method: .post, parameters: parameters)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder.default)
AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: URLEncodedFormParameterEncoder(destination: .httpBody))

输出结果为:

[Request]: POST https://httpbin.org/post
    [Headers]:
        Content-Type: application/x-www-form-urlencoded; charset=utf-8
    [Body]: 73 bytes

"form": {
  "baz[]": [
    "a",
    "b"
  ],
  "foo[]": "bar",
  "qux[]": [
    "x",
    "y",
    "z"
  ]
}, 

d、JSON 编码参数的 POST 请求
// HTTP body: {"baz":["a","b"],"foo":["bar"],"qux":["x","y","z"]}
let parameters: [String: [String]] =
[
    "foo": ["bar"],
    "baz": ["a", "b"],
    "qux": ["x", "y", "z"]
]

AF.request("https://httpbin.org/post", method: .post, parameters: parameters, encoder: Alamofire.JSONParameterEncoder.default).response
{ response in
    debugPrint(response)
}

输出结果为:

[Request]: POST https://httpbin.org/post
    [Headers]:
        Content-Type: application/json
    [Body]:
        {"foo":["bar"],"qux":["x","y","z"],"baz":["a","b"]}a

4、HTTP Headers

a、构造HTTP Headers

Alamofire 包含自己的 HTTPHeaders 类型,这是一种保持顺序且不区分大小写的 name/value 对的表示。HTTPHeader 类型可以封装单个 name/value 对,并为常用的 headers 提供各种静态值。向 Request 添加自定义 HTTPHeaders 就像向 request 方法传递值一样简单。

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

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

HTTPHeaders 类型也可以采用如下方式进行构造:

let anotherHeaders: HTTPHeaders =
[
    .authorization(username: "Username", password: "Password"),
    .accept("application/json")
]

b、Session 为每个 Request 提供一组默认的 headers
  • Accept-Encoding:默认为 "br;q=1.0, gzip;q=0.9, deflate;q=0.8"
  • Accept-Language:默认系统中最多含有 6 种首选语言,格式为 "en;q=1.0"
  • User-Agent:包含有关应用程序的版本信息,例如"UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1"

5、响应验证

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

a、自动验证

validate() 会自动验证状态代码是否在200..<300范围内,以及响应的Content-Type header 是否与请求的 Accept 匹配(如果有提供)。

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

输出结果为:

[Response]:
    [Status Code]: 200
    [Headers]:
        access-control-allow-credentials: true
        Access-Control-Allow-Origin: *
        Content-Length: 427
        Content-Type: application/json
        Date: Mon, 25 Jan 2021 07:38:36 GMT
        Server: gunicorn/19.9.0

b、手动验证
AF.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 let .failure(error):
            print(error)
        }
    }

输出结果为:

Validation Successful

6、响应处理

不管被序列化成哪种类型,结果都会通过闭包的参数response返回,如果是被序列化的数据,就通过resonse中的result.value来获取数据。源码中response闭包函数的返回值是Self,也就是Request,这就让我们能够使用链式访问来做一些很有意思的事情,任务按照顺序依次放入到队列中。

a、Handler

不计算任何响应数据。它只是直接从 URLSessionDelegate 转发所有信息。

// 未序列化的 Response
func response(
    queue: DispatchQueue = .main,
    completionHandler: @escaping (AFDataResponse<Data?>) -> Void
) -> Self

// 序列化的 Response
func response<Serializer: DataResponseSerializerProtocol>(
    queue: DispatchQueue = .main,
    responseSerializer: Serializer,
    completionHandler: @escaping (AFDataResponse<Serializer.SerializedObject>) -> Void
) -> Self

使用方式为:

AF.request("https://httpbin.org/get").response
{ response in
    debugPrint("Response: \(response)")
}

输出结果为:

"Response: success(Optional(426 bytes))"

b、Data Handler

使用 DataResponseSerializer 提取并验证服务器返回的数据。如果没有发生错误并且返回数据,则响应结果将为 .successvalue 将为从服务器返回的 Data

func responseData(
    queue: DispatchQueue = .main,
    completionHandler: @escaping (AFDataResponse<Data>) -> Void
) -> Self

使用方式为:

AF.request("https://httpbin.org/get").responseData
{ response in
    debugPrint("Response: \(response)")
}

输出结果为:

"Response: success(427 bytes)"

c、String Handler

使用 StringResponseSerializer 将服务器返回的数据转换为具有指定编码的String。如果没有发生错误,并且服务器数据成功序列化为 String,则响应结果将为 .success,并且值的类型为 String

func responseString(
    queue: DispatchQueue = .main,
    encoding: String.Encoding? = nil,
    completionHandler: @escaping (AFDataResponse<String>) -> Void
) -> Self

使用方式为:

AF.request("https://httpbin.org/get").responseString
{ response in
    debugPrint("Response: \(response)")
}

输出结果为:

"Response: success(\"{\\n  \\\"args\\\": {}, \\n  \\\"headers\\\": {\\n    \\\"Accept\\\": \\\"*/*\\\", \\n    \\\"Accept-Encoding\\\": \\\"br;q=1.0, gzip;q=0.9, deflate;q=0.8\\\", \\n    \\\"Accept-Language\\\": \\\"en;q=1.0\\\", \\n    \\\"Host\\\": \\\"httpbin.org\\\", \\n    \\\"User-Agent\\\": \\\"UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1\\\", \\n    \\\"X-Amzn-Trace-Id\\\": \\\"Root=1-600e8ac6-70026c993647252b60805135\\\"\\n  }, \\n  \\\"origin\\\": \\\"218.104.139.115\\\", \\n  \\\"url\\\": \\\"https://httpbin.org/get\\\"\\n}\\n\")"

d、JSON Handler

使用指定的 JSONSerialization.ReadingOptions 将服务器返回的数据转换为 Any 类型。如果没有出现错误,并且服务器数据成功序列化为 JSON 对象,则响应 AFResult 将为 .success,值将为 Any 类型。

func responseJSON(
    queue: DispatchQueue = .main,
    options: JSONSerialization.ReadingOptions = .allowFragments,
    completionHandler: @escaping (AFDataResponse<Any>) -> Void
) -> Self

使用方式为:

AF.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint("Response: \(response)")
}

输出结果为:

"Response: success({\n    args =     {\n    };\n    headers =     {\n        Accept = \"*/*\";\n        \"Accept-Encoding\" = \"br;q=1.0, gzip;q=0.9, deflate;q=0.8\";\n        \"Accept-Language\" = \"en;q=1.0\";\n        Host = \"httpbin.org\";\n        \"User-Agent\" = \"UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1\";\n        \"X-Amzn-Trace-Id\" = \"Root=1-600e8b4a-28fe0a1064c7a6e1033414fa\";\n    };\n    origin = \"218.104.139.115\";\n    url = \"https://httpbin.org/get\";\n})"

e、可解码类型的Handler

使用 DecodableResponseSerializer 和 指定的 DataDecoderDecoder 的协议抽象,可以从 Data 解码)将服务器返回的数据转换为传递进来的 Decodable 类型。如果没有发生错误,并且服务器数据已成功解码为 Decodable 类型,则响应 Result 将为 .success,并且 value 将为传递进来的类型。

func responseDecodable<T: Decodable>(
    of type: T.Type = T.self,
    queue: DispatchQueue = .main,
    decoder: DataDecoder = JSONDecoder(),
    completionHandler: @escaping (AFDataResponse<T>) -> Void
) -> Self

使用方式为:

struct HTTPBinResponse: Decodable
{
    let url: String
}

AF.request("https://httpbin.org/get").responseDecodable(of: HTTPBinResponse.self)
{ response in
    debugPrint("Response: \(response)")
}

输出结果为:

"Response: success(UseAlamofire.HTTPBinResponse(url: \"https://httpbin.org/get\"))"

f、链式响应

没有一个响应 handlers 对从服务器返回的 HTTPURLResponse 执行任何验证。例如,400..<500500..<600 范围内的响应状态代码不会自动触发错误。Alamofire 使用链式的响应验证来实现这一点。对同一请求使用多个响应 handlers 需要多次序列化服务器数据,每个响应 handlers 均处理一次。通常应避免对同一请求使用多个响应 handlers,特别是在生产环境中。

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

输出结果为:

Response String: Optional("{\n  \"args\": {}, \n  \"headers\": {\n    \"Accept\": \"*/*\", \n    \"Accept-Encoding\": \"br;q=1.0, gzip;q=0.9, deflate;q=0.8\", \n    \"Accept-Language\": \"en;q=1.0\", \n    \"Host\": \"httpbin.org\", \n    \"User-Agent\": \"UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1\", \n    \"X-Amzn-Trace-Id\": \"Root=1-600e8e25-66ed7b9b0986f02971550391\"\n  }, \n  \"origin\": \"218.104.139.115\", \n  \"url\": \"https://httpbin.org/get\"\n}\n")
Response JSON: Optional({
    args =     {
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        Host = "httpbin.org";
        "User-Agent" = "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1";
        "X-Amzn-Trace-Id" = "Root=1-600e8e25-66ed7b9b0986f02971550391";
    };
    origin = "218.104.139.115";
    url = "https://httpbin.org/get";
})

g、响应队列

默认情况下,传递给响应 handler 的闭包在 .main 队列上执行,但可以传递一个指定的 DispatchQueue 来执行闭包。实际的序列化工作(将 Data 转换为其他类型)总是在后台队列上执行。

let utilityQueue = DispatchQueue.global(qos: .utility)
AF.request("https://httpbin.org/get").responseJSON(queue: utilityQueue)
{ response in
    print("在全局队列上执行此网络请求:\(Thread.current)")
    debugPrint(response)
}

输出结果为:

在全局队列上执行此网络请求:<NSThread: 0x600000401a00>{number = 8, name = (null)}

7、身份验证

a、自动提供 URLCredential

Requestauthenticate方法将在使用 URLAuthenticationChallenge 进行质询时自动提供 URLCredential

let user = "user"
let password = "password"

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

输出结果为:

[Response]:
    [Status Code]: 200
    [Headers]:
        access-control-allow-credentials: true
        Access-Control-Allow-Origin: *
        Content-Length: 47
        Content-Type: application/json
        Date: Mon, 25 Jan 2021 10:01:37 GMT
        Server: gunicorn/19.9.0
    [Body]:
        {
          "authenticated": true, 
          "user": "user"
        }

b、自己提供 URLCredential 进行验证
let user = "user"
let password = "password"

let credential = URLCredential(user: user, password: password, persistence: .forSession)

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

输出结果同上。


8、下载文件

a、下载数据到文件中

除了将数据提取到内存中之外,Alamofire 还提供了 Session.downloadDownloadRequestDownloadResponse<Success,Failure:Error> 以方便下载数据到磁盘。虽然下载到内存中对小负载(如大多数 JSON API 响应)非常有用,但获取更大的资源(如图像和视频)应下载到磁盘,以避免应用程序出现内存问题。DownloadRequest 具有与 DataRequest 相同的大多数响应 handlers。但是,由于它将数据下载到磁盘,因此序列化响应涉及从磁盘读取,还可能涉及将大量数据读入内存。在设计下载处理时,记住这些事实是很重要的。

AF.download("https://httpbin.org/image/png").responseData
{ response in
    if let data = response.value
    {
        let image = UIImage(data: data)
        self.imageView.image = image!
    }
}

b、下载文件的存放位置

所有下载的数据最初都存储在系统临时目录中。它最终会在将来的某个时候被系统删除,所以如果它需要更长的寿命,将文件移到其他地方是很重要的。我们可以提供 Destination 闭包,将文件从临时目录移动到最终的存放位置。在临时文件实际移动到 destinationURL 之前,将执行闭包中指定的 Options。当前支持的两个 Options 是:.createIntermediateDirectories 如果指定,则为目标 URL 创建中间目录。.removePreviousFile如果指定,则从目标 URL中删除以前的文件。

AF.download("https://httpbin.org/image/png", to: destination).response
{ response in
    debugPrint(response)

    if response.error == nil, let imagePath = response.fileURL?.path
    {
        let image = UIImage(contentsOfFile: imagePath)
        self.imageView.image = image!
    }
}

输出结果为:

[Request]: GET https://httpbin.org/image/png
    [Headers]: None
    [Body]: None
[Response]:
    [Status Code]: 200
    [Headers]:
        access-control-allow-credentials: true
        Access-Control-Allow-Origin: *
        Content-Length: 8090
        Content-Type: image/png
        Date: Mon, 25 Jan 2021 10:14:50 GMT
        Server: gunicorn/19.9.0
[File URL]: /Users/xiejiapei/Library/Developer/CoreSimulator/Devices/9AF7A16E-76FF-4711-8BFA-66DDF38D03F2/data/Containers/Data/Application/730436F5-DE88-42CA-96AF-B5FC5A4C9019/Documents/image.png
[Resume Data]: None
[Network Duration]: 1.8333059549331665s
[Serialization Duration]: 0.0s
[Result]: success(Optional(file:///Users/xiejiapei/Library/Developer/CoreSimulator/Devices/9AF7A16E-76FF-4711-8BFA-66DDF38D03F2/data/Containers/Data/Application/730436F5-DE88-42CA-96AF-B5FC5A4C9019/Documents/image.png))

文件存储的位置为

还可以使用建议的文件存储位置,效果同上。

let destination = DownloadRequest.suggestedDownloadDestination(for: .documentDirectory)
AF.download("https://httpbin.org/image/png", to: destination).response
{ response in
    debugPrint(response)

    if response.error == nil, let imagePath = response.fileURL?.path
    {
        let image = UIImage(contentsOfFile: imagePath)
        self.imageView.image = image!
    }
}

c、下载进度

任何 DownloadRequest 都可以使用 downloadProgress 报告下载进度。只有在服务器正确返回可用于计算进度的 Content-Length header 时才能工作。如果没有这个 header,进度将保持在 0.0,直到下载完成,此时进度将跳到 1.0。还可以接收一个queue参数,该参数定义应该对哪个 DispatchQueue 调用下载进度闭包。

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

AF.download("https://httpbin.org/image/png")
    .downloadProgress(queue: utilityQueue)
    { progress in
        print("下载进度: \(progress.fractionCompleted)")
    }
    .responseData
    { response in
        if let data = response.value
        {
            let image = UIImage(data: data)
            self.imageView.image = image!
        }
    }

输出结果为:

下载进度: 1.0

d、取消和恢复下载

除了所有请求类都有 cancel() 方法外,DownloadRequest 还可以生成恢复数据,这些数据可以用于以后恢复下载。此 API 有两种形式:1)cancel(producingResumeData: Bool),它允许控制是否生成恢复数据,但仅在 DownloadResponse 可用;2)cancel(byProducingResumeData: (_ resumeData: Data?) -> Void),它执行相同的操作,但恢复数据在 completion handler中可用。如果DownloadRequest 被取消或中断,则底层的 URLSessionDownloadTask 可能会生成恢复数据。如果发生这种情况,可以重新使用恢复数据来重新启动停止的 DownloadRequest

let download = AF.download("https://httpbin.org/image/png")

var resumeData: Data!

// 正常下载
download.responseData
{ response in
    if let data = response.value
    {
        let image = UIImage(data: data)
        self.imageView.image = image!
    }
}

// 从cancel的回调闭包中获得resumeData
download.cancel
{ data in
    resumeData = data
}

// 使用resumeData继续下载
AF.download(resumingWith: resumeData).responseData
{ response in
    if let data = response.value
    {
        let image = UIImage(data: data)
        self.imageView.image = image!
    }
}

9、上传数据到服务器

当使用 JSONURL 编码的参数向服务器发送相对少量的数据时,request() 通常就足够了。如果需要从内存、文件 URLInputStream 中的 Data 发送大量数据,那么 upload() 就是您想要使用的。

a、上传 Data
let data = Data("XieJiaPei".utf8)

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

输出结果为:

[Result]: success({
    args =     {
    };
    data = "";
    files =     {
    };
    form =     {
        XieJiaPei = "";
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        "Content-Length" = 9;
        "Content-Type" = "application/x-www-form-urlencoded";
        Host = "httpbin.org";
        "User-Agent" = "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1";
        "X-Amzn-Trace-Id" = "Root=1-600f6966-48de0e395d96f17f396e2b67";
    };
    json = "<null>";
    origin = "222.76.251.163";
    url = "https://httpbin.org/post";
})
b、上传多表单数据
AF.upload(multipartFormData: { multipartFormData in
    multipartFormData.append(Data("Boy".utf8), withName: "JiaPei")
    multipartFormData.append(Data("Girl".utf8), withName: "YuQing")
}, to: "https://httpbin.org/post")
.responseJSON { response in
    debugPrint(response)
}

输出结果为:

[Result]: success({
    args =     {
    };
    data = "";
    files =     {
    };
    form =     {
        JiaPei = Boy;
        YuQing = Girl;
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        "Content-Length" = 228;
        "Content-Type" = "multipart/form-data; boundary=alamofire.boundary.6d21176fdb63050f";
        Host = "httpbin.org";
        "User-Agent" = "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1";
        "X-Amzn-Trace-Id" = "Root=1-600f6d84-25a167ab5ddb02c206ff4697";
    };
    json = "<null>";
    origin = "218.104.139.115";
    url = "https://httpbin.org/post";
})
c、上传文件
let fileURL = Bundle.main.url(forResource: "girl", withExtension: "mp4")!
AF.upload(fileURL, to: "https://httpbin.org/post").responseJSON
{ response in
    debugPrint(response)
}

输出结果为:

d、上传进度

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

let fileURL = Bundle.main.url(forResource: "girl", withExtension: "mp4")!

if FileManager.default.fileExists(atPath: fileURL.path)
{
    AF.upload(fileURL, to: "https://httpbin.org/post")
        .uploadProgress
        { progress in
            print("上传进度: \(progress.fractionCompleted)")
        }
        .responseJSON
        { response in
            print("上传完成")
            print(response)
        }
}
else
{
    print("没有找到文件")
}

输出结果为:

上传进度: 0.01685915418681258
上传进度: 0.5394929339780026
上传进度: 0.6743661674725031
上传进度: 0.9441126344615044
上传进度: 1.0

10、网络可达性

监听移动网络和 WiFi 网络接口的主机和地址的可达性变化。

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

manager?.startListening
{ status in
    print("网络状态发生改变: \(status)")
}

输出结果为:

网络状态发生改变: reachable(Alamofire.NetworkReachabilityManager.NetworkReachabilityStatus.ConnectionType.ethernetOrWiFi)

三、玩转Alamofire

1、Session

a、Session.default

AlamofireSession 在职责上大致等同于它维护的 URLSession 实例:它提供 API 来生成各种 Request 子类,这些子类封装了不同的 URLSessionTask 子类,以及封装应用于实例生成的所有 Request 的各种配置。Session 提供了一个 default 单例实例,并且 AF 实际上就是 Session.default。因此,以下两个语句是等效的:

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

let session = Session.default
session.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}

b、创建自定义的 Session 实例

使用以下便利初始化器,并将结果存储在整个应用程序使用的单个实例中。此初始化器允许自定义 Session 的所有行为。

let session = Session.init(...)

public convenience init
(
    configuration: URLSessionConfiguration = URLSessionConfiguration.af.default,
    delegate: SessionDelegate = SessionDelegate(),
    rootQueue: DispatchQueue = DispatchQueue(label: "org.alamofire.session.rootQueue"),
    startRequestsImmediately: Bool = true,
    requestQueue: DispatchQueue? = nil,
    serializationQueue: DispatchQueue? = nil,
    interceptor: RequestInterceptor? = nil,
    serverTrustManager: ServerTrustManager? = nil,
    redirectHandler: RedirectHandler? = nil,
    cachedResponseHandler: CachedResponseHandler? = nil,
    eventMonitors: [EventMonitor] = []
)

c、使用 URLSessionConfiguration 创建 Session

要自定义底层 URLSession 的行为,可以提供自定义的 URLSessionConfiguration 实例。建议从改变URLSessionConfiguration.af.default 实例开始,因为它添加了 Alamofire 提供的默认 Accept-EncodingAccept-LanguageUser-Agent headers

let configuration = URLSessionConfiguration.af.default
configuration.allowsCellularAccess = false

let customConfigurationSession = Session(configuration: configuration)
customConfigurationSession.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}

d、SessionDelegate

SessionDelegate 实例封装了对各种 URLSessionDelegate 和相关协议回调的所有处理。默认情况下,Session 将在添加至少一个响应 handler 后立即对 Request 调用 resume()。将 startRequestsImmediately 设置为 false 需要手动调用所有请求的 resume() 方法。

let session = Session(startRequestsImmediately: false)
session.request("https://httpbin.org/get").resume().responseJSON
{ response in
    debugPrint(response)
}

输出结果为:

[Request]: GET https://httpbin.org/get
    [Headers]: None
    [Body]: None
[Response]: None
[Network Duration]: None
[Serialization Duration]: 0.00031406700145453215s
[Result]: failure(Alamofire.AFError.sessionDeinitialized)

e、Session 的 DispatchQueue

默认情况下,Session 实例对所有异步工作使用单个 DispatchQueue。这包括 URLSessiondelegate OperationQueueunderlyingQueue,用于所有 URLRequest 创建、所有响应序列化工作以及所有内部 SessionRequest 状态的改变。如果性能分析显示瓶颈在于 URLRequest 的创建或响应序列化,则可以为 Session 的每个工作区域提供单独的 DispatchQueue。提供的任何自定义 rootQueue 都必须是串行队列,但 requestQueueserializationQueue 可以是串行或并行队列。通常建议使用串行队列,除非性能分析显示工作被延迟,在这种情况下,使队列并行可能有助于提高整体性能。

let rootQueue = DispatchQueue(label: "com.app.session.rootQueue")
let requestQueue = DispatchQueue(label: "com.app.session.requestQueue")
let serializationQueue = DispatchQueue(label: "com.app.session.serializationQueue")

let session = Session(
    rootQueue: rootQueue,
    requestQueue: requestQueue,
    serializationQueue: serializationQueue
)

f、添加其他信息
❶ 添加 RequestInterceptor

AlamofireRequestInterceptor 协议(RequestAdapter & RequestRetrier)提供了重要而强大的请求自适应和重试功能。

let policy = RetryPolicy()
let session = Session(interceptor: policy)
❷ 添加 ServerTrustManager

AlamofireServerTrustManager 类封装了域名和遵循 ServerTrustEvaluating 协议的类型实例之间的映射,这提供了定制 Session 处理 TLS 安全性的能力。这包括使用证书和公钥固定以及证书吊销检查。

let manager = ServerTrustManager(evaluators: ["httpbin.org": PinnedCertificatesTrustEvaluator()])
let managerSession = Session(serverTrustManager: manager)
managerSession.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}
❸ 添加 RedirectHandler

AlamofireRedirectHandler 协议定制了 HTTP 重定向响应的处理。

let redirector = Redirector(behavior: .follow)
let redirectorSession = Session(redirectHandler: redirector)
redirectorSession.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}
❹ 添加 CachedResponseHandler

AlamofireCachedResponseHandler 协议定制了响应的缓存,可以在 SessionRequest 层级使用。

let cacher = ResponseCacher(behavior: .cache)
let cacherSession = Session(cachedResponseHandler: cacher)
cacherSession.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}
❺ 添加 EventMonitor

AlamofireEventMonitor 协议提供了对 Alamofire 内部事件的强大洞察力。它可以用来提供日志和其他基于事件的特性。

let monitor = ClosureEventMonitor()
monitor.requestDidCompleteTaskWithError =
{ (request, task, error) in
    debugPrint(request)
}
let monitorSession = Session(eventMonitors: [monitor])
monitorSession.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}

g、从 URLSession 创建实例

除了前面提到的便利初始化器之外,还可以直接从 URLSession 初始化 Session。但是,在使用这个初始化器时需要记住几个要求,因此建议使用便利初始化器。其中包括:

  • Alamofire 不支持为在后台使用而配置的 URLSession。初始化 Session 时,这将导致运行时错误。
  • 必须创建 SessionDelegate 实例并将其作为 URLSessiondelegate,以及传递给 Session 的初始化器。
  • 必须将自定义 OperationQueue 作为 URLSessiondelegateQueue。此队列必须是串行队列,它必须具有备用 DispatchQueue,并且必须将该 DispatchQueue 作为其 rootQueue 传递给 Session
let rootQueue = DispatchQueue(label: "org.alamofire.customQueue")
let queue = OperationQueue()
queue.maxConcurrentOperationCount = 1
queue.underlyingQueue = rootQueue
let delegate = SessionDelegate()
let configuration = URLSessionConfiguration.af.default
let urlSession = URLSession(configuration: configuration,
                            delegate: delegate,
                            delegateQueue: queue)
let session = Session(session: urlSession, delegate: delegate, rootQueue: rootQueue)

2、Request

a、创建请求
URLConvertible协议

可以使用遵循 URLConvertible 协议的类型来构造 URL,然后使用 URL 在内部构造 URL 请求。默认情况下,StringURLURLComponents遵循了URLConvertible协议,允许将它们中的任何一个作为 URL 参数传递给 requestuploaddownload方法。

let urlString = "https://httpbin.org/get"
AF.request(urlString)

let url = URL(string: urlString)!
AF.request(url)

let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
AF.request(urlComponents)
URLRequestConvertible协议

遵循 URLRequestConvertible 协议的类型可用于构造 URLRequest。默认情况下,URLRequest 遵循 URLRequestConvertible,允许将其直接传递到 requestuploaddownload 方法中。

let postUrl = URL(string: "https://httpbin.org/post")!
var urlRequest = URLRequest(url: postUrl)
urlRequest.method = .post

let parameters = ["foo": "bar"]

do
{
    urlRequest.httpBody = try JSONEncoder().encode(parameters)
}
catch
{
    // Handle error.
    print("出错了")
}

urlRequest.headers.add(.contentType("application/json"))

AF.request(urlRequest)
    .responseJSON
    { response in
        debugPrint(response)
    }

输出结果为:

[Result]: success({
    args =     {
    };
    data = "{\"foo\":\"bar\"}";
    files =     {
    };
    form =     {
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        "Content-Length" = 13;
        "Content-Type" = "application/json";
        Host = "httpbin.org";
        "User-Agent" = "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1";
        "X-Amzn-Trace-Id" = "Root=1-600fbced-3ebfeeb20ee086fa017998b4";
    };
    json =     {
        foo = bar;
    };
    origin = "218.104.139.115";
    url = "https://httpbin.org/post";
})

b、请求管道

一旦使用 Request 子类的初始参数或 URLRequestConvertible 创建了它,它就会通过组成 Alamofire 请求管道的一系列步骤进行传递。

请求步骤
  1. 初始参数(如 HTTP 方法、headers 和参数)被封装到内部 URLRequestConvertible 值中。
  2. URLRequestConvertible 值调用 asURLRequest(),创建第一个 URLRequest 值。此值将传递给 Request 并存储在 requests 中。
  3. 如果 SessionRequestAdapterRequestInterceptor,则使用先前创建的 URLRequest 调用它们。然后将调整后的 URLRequest 传递给 Request 并存储在 requests 中。
  4. Session 调用 Request 创建的 URLSessionTask,以基于 URLRequest 执行网络请求。
  5. 完成 URLSessionTask 并收集 URLSessionTaskMetrics(日志)后,Request 将执行其 Validator
  6. 请求执行已附加的任何响应 handlers,如 responseDecodable
触发重试

在这些步骤中的任何一个,都可以通过创建或接收的 Error 值来表示失败,然后将错误值传递给关联的 Request。例如,除了步骤 1 和 4 之外,上面的所有其他步骤都可以创建一个Error,然后传递给响应 handlers 或可供重试。一旦将错误传递给 RequestRequest 将尝试运行与 SessionRequest 关联的任何 RequestRetrier。如果任何 RequestRetrier 选择重试该 Request,则将再次运行完整的管道。RequestRetrier也会产生 Error,但这些错误不会触发重试。

失败原因
  • 参数封装不能失败。
  • 调用 asURLRequest() 时,任何 URLRequestConvertible 值都可能创建错误。这允许初始验证各种 URLRequest 属性或参数编码失败。
  • RequestAdapter 在自适应过程中可能会失败,可能是由于缺少授权 token
  • URLSessionTask 创建不能失败。
  • URLSessionTask 可能由于各种原因带有错误地完成,包括网络可用性和取消。这些 Error 值将传递回给 Request
  • 响应 handlers 可以产生任何错误,通常是由于无效响应或其他分析错误。

c、请求种类

Alamofire 执行的每个请求都由特定的类、DataRequestUploadRequestDownloadRequest 封装。这些类中的每一个都封装了每种类型请求所特有的功能,但是 DataRequestDownloadRequest 继承自一个公共的父类 RequestUploadRequest 继承自 DataRequest)。Request 实例从不直接创建,而是通过各种 request方法之一从会话 Session 中自动生成。

DataRequest

DataRequestRequest 的一个子类,它封装了 URLSessionDataTask,将服务器响应下载到存储在内存中的 Data 中。因此,必须认识到,超大下载量可能会对系统性能产生不利影响。对于这些类型的下载,建议使用 DownloadRequest 将数据保存到磁盘。

除了 Request 提供的属性之外,DataRequest 还有一些属性。其中包括 data(这是服务器响应的累积 Data)和 convertible(这是创建 DataRequest 时使用的 URLRequestConvertible,其中包含创建实例的原始参数)。

默认情况下,DataRequest 不验证响应。相反,必须向其中添加对 validate() 的调用,以验证各种属性是否有效。添加 validate() 确保响应状态代码在 200..<300 范围内,并且响应的 Content-Type 与请求的 Accept 匹配。通过传递 Validation 闭包可以进一步定制验证。

UploadRequest

UploadRequestDataRequest 的一个子类,它封装 URLSessionUploadTask、将 Data、磁盘上的文件或 InputStream 上传到远程服务器。

除了 DataRequest 提供的属性外,UploadRequest 还有一些属性。其中包括一个 FileManager 实例,用于在上传文件时自定义对磁盘的访问,以及 uploadupload 封装了用于描述请求的 URLRequestConvertible 值和确定要执行的上传类型的Uploadable 值。

DownloadRequest

DownloadRequestRequest 的一个具体子类,它封装了 URLSessionDownloadTask,将响应数据下载到磁盘。DownloadRequest 除了由 Request 提供的属性外,还有一些属性。其中包括取消 DownloadRequest 时生成的数据 resumeData(可用于以后继续下载)和 fileURL(下载完成后下载文件对应的 URL)。

除了支持 Request 提供的 cancel() 方法外,DownloadRequest 还包括了其他两种取消方法。

// 可以选择在取消时设置 resumeData 属性
cancel(producingResumeData shouldProduceResumeData: Bool)

// 将生成的恢复数据提供给传递进来的闭包
cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?) -> Void)

d、请求状态

尽管 Request 不封装任何特定类型的请求,但它包含 Alamofire 执行的所有请求所共有的状态和功能,表示 Request 生命周期中的主要事件。请求在创建后以 .initialized 状态启动。通过调用适当的生命周期方法,可以挂起、恢复和取消 Request

public enum State 
{
    case initialized
    case resumed
    case suspended
    case cancelled
    case finished
}
  • resume(): 恢复或启动请求的网络流量。如果 startRequestsImmediatelytrue,则在将响应 handlers 添加到 Request 后自动调用此函数。
  • suspend(): 挂起或暂停请求及其网络流量。此状态下的 Request 可以继续,但只有 DownloadRequest 才能继续传输数据。其他 Request 将重新开始。
  • cancel(): 取消请求。一旦进入此状态,就无法恢复或挂起 Request。调用 cancel() 时,将使用 AFError.explicitlyCancelled 实例设置请求的 error 属性。
  • finished(): 如果一个 Request 被恢复并且在以后没有被取消,那么它将在所有响应验证器和响应序列化器运行之后到达 .finished 状态。但是,如果在请求达到 .finished 状态后将其他响应序列化器添加到该请求,则它将转换回 .resumed 状态并再次执行网络请求。

e、请求进度

为了跟踪请求的进度,Request 提供了 uploadProgressdownloadProgress 属性以及基于闭包的 uploadProgressdownloadProgress 方法。与所有基于闭包的 Request APIs 一样,进度 APIs 可以与其他方法链接到 Request 之后,但是应该在添加任何响应 handlers(如 responseJSON)之前添加到请求中。但并不是所有的 Request 子类都能够准确地报告它们的进度,有些可能有其他依赖项来报告它们的进度。对于下载进度,只有一个要求,服务器响应必须包含 Content-Length header

AF.request("https://httpbin.org/get")
    .uploadProgress
    { progress in
        print("上传进度: \(progress.fractionCompleted)")
    }
    .downloadProgress
    { progress in
        print("下载进度: \(progress.fractionCompleted)")
    }
    .responseJSON
    { response in
        debugPrint(response)
    }

输出结果为:

下载进度: 1.0

[Result]: success({
    args =     {
    };
    headers =     {
        Accept = "*/*";
        "Accept-Encoding" = "br;q=1.0, gzip;q=0.9, deflate;q=0.8";
        "Accept-Language" = "en;q=1.0";
        Host = "httpbin.org";
        "User-Agent" = "UseAlamofire/1.0 (com.xiejiapei.UseAlamofire; build:1; iOS 14.3.0) Alamofire/5.4.1";
        "X-Amzn-Trace-Id" = "Root=1-600f8c8f-1550b7515a19e35b7f534f0e";
    };
    origin = "218.104.139.115";
    url = "https://httpbin.org/get";
})
对于上传进度,可以通过以下方式确定进度
  • 通过作为上传 body 提供给 UploadRequestData 对象的长度。
  • 通过作为 UploadRequest 的上传 body 提供的磁盘上文件的长度。
  • 通过根据请求的 Content-Length header 的值(如果已手动设置)。

f、调整和重试请求
重定向

AlamofireRedirectHandler 协议提供了对 Request 的重定向处理的控制和定制。除了每个 Session RedirectHandler 之外,每个 Request 都可以被赋予属于自己的 RedirectHandler,并且这个 handler 将重写 Session 提供的任何 RedirectHandler

let redirector = Redirector(behavior: .follow)
AF.request("https://httpbin.org/get")
    .redirect(using: redirector)
    .responseDecodable(of: String.self)
    { response in
        debugPrint(response)
    }
重试

AlamofireRequestInterceptor 协议由 RequestAdapterRequestRetrier 协议组成。在身份验证系统中,向每个 Request 添加一个常用的 headers,并在授权过期时重试 RequestAlamofire 还包含一个内置的 RetryPolicy 类型,当由于各种常见的网络错误而导致请求失败时,可以轻松重试。

RequestAdapter 协议允许在通过网络发出之前检查和修改 Session 执行的每个 URLRequest。适配器的一个常见的用途,是在身份验证后面为请求添加 Authorization headerRequestRetrier 协议允许重试在执行时遇到错误的请求。


3、Security

在与服务器和 web 服务通信时使用安全的 HTTPS 连接是保护敏感数据的重要步骤。默认情况下,Alamofire 接收与 URLSession 相同的自动 TLS 证书和证书链验证。虽然这保证了证书链的有效性,但并不能防止中间人(MITM)攻击或其他潜在的漏洞。为了减轻 MITM 攻击,处理敏感客户数据或财务信息的应用程序应使用 AlamofireServerTrustEvaluating 协议提供的证书或公钥固定。

a、评估服务器信任
ServerTrustEvaluting 协议
  • DefaultTrustEvaluator:使用默认服务器信任评估,同时允许您控制是否验证质询提供的主机。
  • RevocationTrustEvaluator:检查接收到的证书的状态以确保它没有被吊销。这通常不会在每个请求上执行,因为它需要网络请求开销。
  • PinnedCertificatesTrustEvaluator:使用提供的证书验证服务器信任。如果某个固定证书与某个服务器证书匹配,则认为服务器信任有效。此评估器还可以接受自签名证书。
  • PublicKeysTrustEvaluator:使用提供的公钥验证服务器信任。如果某个固定公钥与某个服务器证书公钥匹配,则认为服务器信任有效。
  • CompositeTrustEvaluator:评估一个 ServerTrustEvaluating 值数组,只有在所有数组中值都成功时才成功。此类型可用于组合,例如,RevocationTrustEvaluatorPinnedCertificatesTrustEvaluator
  • DisabledEvaluator:此评估器应仅在调试方案中使用,因为它禁用所有求值,而这些求值又将始终认为任何服务器信任都是有效的。此评估器不应在生产环境中使用!
// 此方法提供从底层 URLSession 接收的 SecTrust 值和主机 String,并提供执行各种评估的机会
func evaluate(_ trust: SecTrust, forHost host: String) throws
ServerTrustManager
  • cert.example.com:将始终在启用默认和主机验证的情况下使用证书固定
  • keys.example.com:将始终在启用默认和主机验证的情况下使用公钥固定
let evaluators: [String: ServerTrustEvaluating] = 
[
    // 默认情况下,包含在 app bundle 的证书会自动固定。
    "cert.example.com": PinnedCertificatesTrustEvalutor(),
    // 默认情况下,包含在 app bundle 的来自证书的公钥会被自动使用。
    "keys.example.com": PublicKeysTrustEvalutor(),
]

let manager = ServerTrustManager(evaluators: serverTrustPolicies)

b、Logging

EventMonitor 协议的最大用途是实现相关事件的日志记录。

final class Logger: EventMonitor
{
    let queue = DispatchQueue(label: "xiejiapei")

    // Event called when any type of Request is resumed.
    func requestDidResume(_ request: Request)
    {
        print("Resuming: \(request)")
    }

    // Event called whenever a DataRequest has parsed a response.
    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>)
    {
        debugPrint("Finished: \(response)")
    }
}

let logger = Logger()
let session = Session(eventMonitors: [logger])
session.request("https://httpbin.org/get").responseJSON
{ response in
    debugPrint(response)
}

输出结果为:

[Request]: GET https://httpbin.org/get
    [Headers]: None
    [Body]: None
[Response]: None
[Network Duration]: None
[Serialization Duration]: 4.879705375060439e-05s
[Result]: failure(Alamofire.AFError.sessionDeinitialized)
Resuming: GET https://httpbin.org/get
"Finished: failure(Alamofire.AFError.sessionDeinitialized)"

Demo

Demo在我的Github上,欢迎下载。
UseFrameworkDemo

参考文献

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容