Alamofire源码解读系列(一)之概述和使用

尽管Alamofire的github文档已经做了很详细的说明,我还是想重新梳理一遍它的各种用法,以及这些方法的一些设计思想

前言

因为之前写过一个AFNetworking的源码解读,所以就已经比较了解iOS平台的网络框架是怎么一回事了。AlamofireAFNetworking有很多相同的地方,然而,这些相同点在swift和oc两种不同语言的实现情况下,给人的感觉是完全不同的。

我们看源码的目的有两个:一是了解代码的实现原理,另一个是学习swift的一些高级用法。

下边的这个表格就是我打算解读的顺序,一共17个文件,其中DispatchQueue+Alamofire.swift就不作为单独的一篇来解释了,会在使用到它的地方做一个说明,这一篇文章的主要目的就是解释Alamofire如何使用,因此一共就需要17篇文章来完成这一系列的源码解读。

文件名 描述
1.AFError.swift 对错误的封装,包含了Alamofire中所有可能出现的错误,使用enum实现,很有意思
2.Notifications.swift swift中通知的用法,这个跟oc的有区别
3.ParameterEncoding.swift 参数编码,有些情况需要把参数编码到URL中,包含了转义相关的知识
4.Result.swift 对请求结果的封装
5.TaskDelegate.swift 任务代理
6.NetworkReachabilityManager.swift 网络状态管理
7.ServerTrustPolicy.swift 安全策略管理
8.Response.swift 服务器返回的数据的封装
9.ResponseSerialization.swift 响应序列化管理
10.MultipartFormData.swift 多表单数据处理
11.Timeline.swift 新增的内容,与请求相关的一些时间属性
12.Request.swift 最核心的请求类
13.Validation.swift 对服务器响应的验证
14.SessionDelegate.swift 会话代理
15.SessionManager.swift 会话管理,核心内容
16.Alamofire.swift 支持的基本接口

Alamofire的基本用法

1.最简单的请求

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

这是一个最简单的请求,这个请求即不需要参数,也不需要接收数据。接下来我们翻看Alamofire这个文件,发现并没有Alamofire这个类,那么为什么能够像Alamofire.requeset()这么使用呢?

其实当一个文件作为一个模块被导入的话,通过文件名就能访问到模块内部的数据,比如说通过cocopods导入的框架,就有这样的特性。如果把Alamofire.swift直接拖进工程中,Alamofire.requeset()就会报错,但是我们去掉Alamofire,直接用request()就可以了。

2.Response处理

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

在Alamofire中,对请求的封装有以下几种类型:

  • Request
  • DataRequest
  • DownloadRequest
  • UploadRequest
  • StreamRequest

这几种类型,按照名字我们就能很容易的知道他们的用途是什么,其中StreamRequest在iOS9.0之后才被引入。

request(...)方法返回Request本身或者其子类,那么responseJson就应该是Request本身或者其子类的一个函数,该函数的最后一个参数是一个闭包。这里先不能解释太多,到了后边会详细解释。

Alamofire对于response提供了5种处理方式:

// Response Handler - Unserialized Response
func response(
    queue: DispatchQueue?,
    completionHandler: @escaping (DefaultDataResponse) -> Void)
    -> Self

// Response Data Handler - Serialized into Data
func responseData(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Data>) -> Void)
    -> Self

// Response String Handler - Serialized into String
func responseString(
    queue: DispatchQueue?,
    encoding: String.Encoding?,
    completionHandler: @escaping (DataResponse<String>) -> Void)
    -> Self

// Response JSON Handler - Serialized into Any
func responseJSON(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void)
    -> Self

// Response PropertyList (plist) Handler - Serialized into Any
func responsePropertyList(
    queue: DispatchQueue?,
    completionHandler: @escaping (DataResponse<Any>) -> Void))
    -> Self

我们把这五种归纳一下:

  • response 直接返回HTTPResponse,未序列化
  • responseData 序列化为Data
  • responseJSON 序列化为JSON
  • responseString 序列化为字符串
  • responsePropertyList 序列化为Any

不管被序列成哪一个,结果都会通过闭包的参数response返回,如果是被序列化的数据,就通过resonse中的result.value来获取数据。

源码中response闭包函数的返回值是Self,也就是Request,这就让我们能够使用链式访问来做一些很有意思的事情,比如:

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

上边的代码就使用了链式访问,当收到服务器的数据后,先处理responseString再处理responseJSON。那么内部是如何实现类似这种有顺序的访问的呢?

答案就是使用队列,任务按照顺序依次放入到队列中,就实现了上边的功能,这里关于队列在Alamofire中是如何使用的,会在接下来的文章中给出更详细的解答。我在这里先给出一个粗略的说明:

  1. TaskDelegate中有一个属性queue,下边就是这个queue的初始化,这样的写法也是通过闭包来实现赋值的,值得注意的是operationQueue的isSuspended被赋值为true,这样做的目的就是,当一系列的operation被添加到队列中后,不会立刻执行,直到isSuspended等于false时才会。

       self.queue = {
                 let operationQueue = OperationQueue()
     
                 operationQueue.maxConcurrentOperationCount = 1
                 operationQueue.isSuspended = true
                 operationQueue.qualityOfService = .utility
     
                 return operationQueue
             }()
    
  2. 调用.responseString后放生了什么?其实,很简单,就是给queue添加了一个操作

     delegate.queue.addOperation {
                 /// 这里就调用了responseSerializer保存的系列化函数,函数调用后会得到result
                 let result = responseSerializer.serializeResponse(
                     self.request,
                     self.response,
                     self.delegate.data,
                     self.delegate.error
                 )
     
                 /// 这里一定要记得,DataResponse是一个结构体,是专门为了纯存储数据的,这里是调用了结构体的初始化方法创建了一个新的DataResponse实例
                 var dataResponse = DataResponse<T.SerializedObject>(
                     request: self.request,
                     response: self.response,
                     data: self.delegate.data,
                     result: result,
                     timeline: self.timeline
                 )
     
                 dataResponse.add(self.delegate.metrics)
     
                 (queue ?? DispatchQueue.main).async { completionHandler(dataResponse) }
             }
    
  3. 当然还有其他的一些操作,比方说上传完成后要删除临时文件等等,但归根到底,这里用的就是队列相关的知识

Alamofire中,默认的响应会放在主线程,那么我们该如何自定义响应线程呢?

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

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

这主要得益于swift函数的参数可以设置默认值,有默认值得函数参数可以忽略。

3.验证

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

上边的这些代码看上去很简单,其实包含了一个复杂的过程。validate(statusCode: 200..<300)validate(contentType: ["application/json"])都返回的是Self,只有这样才能够保证链式的调用。那么这两个验证的结果要如何来获取呢?

我们先看一个方法:

  @discardableResult
    public func validate<S: Sequence>(statusCode acceptableStatusCodes: S) -> Self where S.Iterator.Element == Int {
        return validate { [unowned self] _, response, _ in
            return self.validate(statusCode: acceptableStatusCodes, response: response)
        }
    }

这个方法就是validate(statusCode: 200..<300)的内部实现函数,可以看出来,在函数中调用了一个函数得到的返回值,那么这个被调用的函数validate只接受一个参数,这个参数也是一个函数。我们姑且称这个函数为函数1. 接下来要看看validate函数的实现细节:

 @discardableResult
    public func validate(_ validation: @escaping Validation) -> Self {
        let validationExecution: () -> Void = { [unowned self] in
            if
                let response = self.response,
                self.delegate.error == nil,
                case let .failure(error) = validation(self.request, response, self.delegate.data)
            {
                self.delegate.error = error
            }
        }

        validations.append(validationExecution)

        return self
    }

可以看出,函数内部调用了它的参数,这个参数也就是在上边传递过来的函数1。这个可能比较绕,不太好理解。这个会在ResponseSerialization.swift那篇文章中进行详细解释的。

虽然我们可能通过下边的方法来判断是不是验证成功:

switch response.result {
        case .success:
            print("Validation Successful")
        case .failure(let error):
            print(error)
        }

我们仍然可以通过result访问到序列化后的数据

switch response.result {
        case .success(data):
            print("Validation Successful data:\(data)")
        case .failure(let error):
            print(error)
        }

如果使用自动验证的话,它会验证200..<300的状态吗和发请求时提供的可接受的ContentType类型。

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

4.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提供了上边的HTTPMethod,至于每个方法的使用详情,请参考我写的这篇文章。那么在请求中是这么使用的:

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)

5.Parameter Encoding

Alamofire支持三种参数编码方式:URLJSONPropertyList。也可以通过实现ParameterEncoding协议来自定义编码方式。

我们先看URL编码:

URLEncoding是对URL编码的封装,通过一个enum提供3种编码方式:

 public enum Destination {
        case methodDependent, queryString, httpBody
    }
  • methodDependent 表示根据HTTPMethod来判断如何编码,.get, .head, .delete情况下会把参数编入URL之中
  • queryString 表示把参数编入URL之中
  • httpBody 表示把参数编入httpBody之中

当然这些东西现不在这里做过多的解释了,在开发中也用的不多,详细的解释会放到后边ParameterEncoding.swift这一片文章之中。

JSON

我们把参数以JSON的方式编码,如果在开发中用到了,需要在请求的header中设置

ContentTypeapplication/json

let parameters: Parameters = [
    "foo": [1,2,3],
    "bar": [
        "baz": "qux"
    ]
]

// Both calls are equivalent
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding.default)
Alamofire.request("https://httpbin.org/post", method: .post, parameters: parameters, encoding: JSONEncoding(options: []))

// HTTP body: {"foo": [1, 2, 3], "bar": {"baz": "qux"}}

PropertyList

这个跟JSON很像,如果在开发中用到了,需要在请求的header中设置

ContentTypeapplication/x-plist

如果我们要自定义参数编码,那该怎么办呢?下边是Alamofire的一个例子:

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

该例子中的JSONStringArrayEncoding实现了ParameterEncoding协议,实现了协议中的方法,这是一个典型的自定义编码方式,在开发中这么使用:

Alamofire.request("https://xxxxx", method: .get, parameters: nil, encoding: JSONStringArrayEncoding(array: ["abc", "ddd"]), headers: nil)

当然我们也可以把ParameterEncoding当做一个API来使用:

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)

6.请求头

客户端每发起一次HTTP请求,请求头信息是必不可少的。这也是同服务器交流的一种手段,在实际的开发中,也肯定会遇到需要自定义请求头的需求,那么我们就看看,在Alamofire中如何设置请求头:

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

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

很简单,在request(...)函数中,存在headers这么一个参数,我们只要传入提前写好的字典就行了。当然,使用URLSessionConfiguration来配置全局的属性更加有优势,因为上边的方法只是针对某一个请求的,如果有很多的请求都需要添加请求头,那么就应该使用URLSessionConfiguration来配置了。

需要说明的是,Alamofire为每一个请求都设置了默认的请求头,我们简单介绍一下:

  • Accept-Encoding 表示可接受的编码方式,值为:gzip;q=1.0, compress;q=0.5
  • Accept-Language 表示可接受的语言,这个在后边的文章中会详细说明
  • User-Agent 表示用户代理信息,比如:iOS Example/1.0 (com.alamofire.iOS-Example; build:1; iOS 10.0.0) Alamofire/4.0.0

默认的情况下,我们通过SessionManager.default来创建SessionManager:

   open static let `default`: SessionManager = {
        let configuration = URLSessionConfiguration.default
        configuration.httpAdditionalHeaders = SessionManager.defaultHTTPHeaders

        return SessionManager(configuration: configuration)
    }()

如果我们想自定义Accept-Encoding Accept-Language User-Agent,那该怎么办呢? 答案就是使用下边的这个方法:

  public init(
        configuration: URLSessionConfiguration = URLSessionConfiguration.default,
        delegate: SessionDelegate = SessionDelegate(),
        serverTrustPolicyManager: ServerTrustPolicyManager? = nil)
    {
        self.delegate = delegate
        self.session = URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)

        commonInit(serverTrustPolicyManager: serverTrustPolicyManager)
    }

通过configuration来设置自定义的请求头,但需要注意的是,通过这个初始化方法创建的SessionManager不在是一个单利了,要想继续使用单利,可能需要自己继承SessionManager,然后手动实现单利。

7.HTTP 基本认证

在Alamofire中有三种使用基本认证的方法:

  • 在request(...)和response之间,拼接authenticate(user: user, password: password)

      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)
          }
    
  • 手动生成headers,Request.authorizationHeader(user: user, password: password)返回一个元组(key: String, value: String)?

      let user = "user"
      let password = "password"
      
      var headers: HTTPHeaders = [:]
      
      if let authorizationHeader = Request.authorizationHeader(user: user, password: password) {
          headers[authorizationHeader.key] = authorizationHeader.value
      }
      
      Alamofire.request("https://httpbin.org/basic-auth/user/password", headers: headers)
          .responseJSON { response in
              debugPrint(response)
          }
    
  • 使用URLCredential

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

8.下载文件

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

即使APP在后台,download也是支持的。

需要注意的是,Alamofire.download返回的是DownloadRequest,它的response的类型是DownloadResponse,这里边包含temporaryURLdestinationURL这两个属性,也就是说,如果我们没有指定Destination,那么文件就默认下载到temporaryURL,通过他也可以访问到文件。

要想自定义指定的目标路径,我们需要创建一个DownloadFileDestination的闭包,我们先看看这个闭包的原型:

public typealias DownloadFileDestination = (
    _ temporaryURL: URL,
    _ response: HTTPURLResponse)
    -> (destinationURL: URL, options: DownloadOptions)

可以看出,该函数有两个参数,temporaryURL和response,要求返回一个元组,包含目标路径和选型,我们在看看这个DownloadOptions:

  • createIntermediateDirectories 表示会根据路径来创建中间的文件夹
  • removePreviousFile 表示会移除指定路径上之前的文件

这里指的注意的是DownloadOptions使用掩码来实现的,这就说明可以同时选中这两个选项 我们来看个例子:

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

另外一种用法就是使用Alamofire建议的路径,我们先看一个例子:

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

再来看看suggestedDownloadDestination函数的实现:

 open class func suggestedDownloadDestination(
        for directory: FileManager.SearchPathDirectory = .documentDirectory,
        in domain: FileManager.SearchPathDomainMask = .userDomainMask)
        -> DownloadFileDestination
    {
        return { temporaryURL, response in
            let directoryURLs = FileManager.default.urls(for: directory, in: domain)

            if !directoryURLs.isEmpty {
                return (directoryURLs[0].appendingPathComponent(response.suggestedFilename!), [])
            }

            return (temporaryURL, [])
        }
    }

可以看出来,suggestedDownloadDestination需要指定directory和domain,当然他们也都有默认值,文件名则采用的是response.suggestedFilename!

说道下载,就不得不提下载进度,我们来看看Alamofire是怎么用下载进度的:

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

大概说一下监听进度的基本原理,详细的实现方法会在后续的文章中提供,当下载文件开始之后,就会有一个数据写入的代理方法被调用,就是在这个方法中处理进度的。我们看看这个进度函数:

@discardableResult
open func downloadProgress(queue: DispatchQueue = DispatchQueue.main, closure: @escaping ProgressHandler) -> Self {
    dataDelegate.progressHandler = (closure, queue)
    return self
}

可以看出来除了一个闭包参数意外还有另外一个参数,就是队列,作用就是指定闭包在那个队列中被调用,我们在开发中,这么使用:

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

还有一种特殊的情况,就是恢复下载数据,当一个下载任务因为一些原因被取消或者中断后,后返回一个resumeData,我们可以使用这个resumeData重新发起一个请求,具体使用方法如下:

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

9.上传文件

在开发中,当需要上传的数据很小的时候,我们往往通过JSON或者URL把参数上传到服务器,但是遇到数据量比较大的情况,在Alamofire中就要采用upload的方式上传数据。

假设我们有一张图片要上传:

let imageData = UIPNGRepresentation(image)!

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

或者这样上传:

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

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

在Alamofire中处理上传数据的方式有以下几种:

  • Data
  • fileURL
  • inputStream
  • MultipartFormData

前三种用起来比较简单,我们接下来讲讲MultipartFormData的使用方法:

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

这段代码需要注意的有几个地方。

  • 数据是通过 multipartFormData.append拼接起来的,append需要两个参数,其中一个参数是获取数据的方式,另一个是数据名称,这个名称一定要给,主要用于给多表单数据的Content-Disposition中的name字段赋值。这个在后续的文章中也会给出详细解释。
  • encodingCompletion并不是上传成功后的回调函数,而是所有要上传的数据编码后的回调。那么我们需要对编码结果做出判断,这样做的好处就是,如果数据编码失败了,就没必要发送数据给服务器。
  • encodingResult的结果,如果是成功的,那么它会返回一个UploadRequest,我们就通过这个UploadRequest绑定response事件。

再就是在上传文件的时候监听进度了,使用方法:

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

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

10.统计度量

Alamofire提供了一个叫TimeLine的新特性,通过这个特性,我们能够观察跟请求相关的一些时间属性,使用方法如下:

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

打印结果如下:

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

在ios10中,苹果引入了URLSessionTaskMetrics ,这个APIs能够提供很多跟请求响应相关的信息,在Alamofire中通过response.metrics来访问这个属性:

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

在使用的时候,一定要做版本检测:

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

11.打印请求

在开发中,经常做的一件事就是调试接口,如果有一种方案,能够很容易的打印请求相关的参数,那么就再好不过了。Alamofire中的Request实现了CustomStringConvertibleCustomDebugStringConvertible协议,因此我们就可以通过下边的方法来打印请求信息:

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

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

打印调试模式下的信息:

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

结果如下:

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

Alamofire的高级用法

1.Session Manager

Alamofire有一些高级的使用方法,最外层的方法都是通过Alamofire.request来访问的,其内部是通过Alamofire.SessionManagerURLSessionConfiguration来实现的,因此我们可以通过修改这些属性,来灵活的使用Request。

先看下边的两种请求方式,他们的作用是一样的:

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

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

通过URLSessionConfiguration我们能够很灵活的修改网络配置参数,比如超时时间等等,下边我们就使用URLSessionConfiguration来创建SessionManager

使用Default Configuration创建SessionManage

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

使用Background Configuration创建SessionManage

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

使用Ephemeral Configuration创建SessionManage

let configuration = URLSessionConfiguration.ephemeral
let sessionManager = Alamofire.SessionManager(configuration: 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)

对于AuthorizationContent-Type不建议通过Configuration来配置,建议使用Alamofire.request APIs中的headers来配置。

2.Session Delegate

在开发中,会有很多自定义代理事件的需求,Alamofire中提供了很多的闭包来解决这个问题,比如:

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

我们有两种方法来修改Alamofire中默认的代理事件,一种是重写这些代理函数:

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
}

上边的函数中,我们重新定义了重定向的函数。还有一种方法是继承代理后,重写父类的方法:

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

3.Request

request,download, upload stream这四个方法的返回值分别为DataRequest, DownloadRequest, UploadRequest StreamRequest,并且他们都继承自Request.这四个子类有一些方法,比如:authenticate, validate, responseJSON uploadProgress,这些方法的返回值又都是Self,这么做的目的是为了实现链式访问。

每一个请求都可以被暂停,恢复,和取消,分别使用下边的方法:

  • suspend() 暂停
  • resume() 恢复, 在SessionManager中有一个属性:startRequestsImmediately。他控制这请求是不是立刻发起,默认的值为true。
  • cancel() 取消 同时该请求的每一个监听对象都会受到一个错误回调

4.路由请求

Alamofire支持通过URLConvertibleURLRequestConvertible这两个协议来实现路由设计模式,路由的概念就是中转站的意思,在Alamofire中,String, URL, URLComponents实现了URLConvertible协议。因此我们才能够这么用:

let urlString = "https://httpbin.org/post"
Alamofire.request(urlString, method: .post)

let url = URL(string: urlString)!
Alamofire.request(url, method: .post)

let urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: true)!
Alamofire.request(urlComponents, method: .post)

当然我们也可以根据实际开发需求,来自定义符合我们需求的路由。在Alamofire的官方演示中,是这么使用的:

extension User: URLConvertible {
    static let baseURLString = "https://example.com"

    func asURL() throws -> URL {
        let urlString = User.baseURLString + "/users/\(username)/"
        return try urlString.asURL()
    }
}

上边的代码让User实现了URLConvertible协议,因此我们就可以直接使用下边的方式发起请求:

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

URLRequestConvertible的用法也很神奇,我们直接看例子:

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

Router实现了URLRequestConvertible协议,因此我们就能够使用下边的这种方式请求数据:

Alamofire.request(Router.search(query: "foo bar", page: 1)) // https://example.com/search?q=foo%20bar&offset=50

上边的Router就实现了根据query和page来生成一个request的过程。大家仔细回味下上边封装的Router,很有意思。

在看看下边的这个封装:

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

上边的代码把对User的操作进行了封装,因此我们在操作User的时候,不需要跟底层的数据打交道,按照这种设计写出的代码也更简洁和具有可读性。

Alamofire.request(Router.readUser("mattt")) // GET https://example.com/users/mattt

5.请求的适配和重试

Alampfire提供了RequestAdapterRequestRetrier这两个协议来进行请求适配和重试的。

RequestAdapter协议允许开发者改变request,这在实际应用中,会有很多实用场景,比如给请求中添加某个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
    }
}

当AccessTokenAdapter成为某个SessionManager的适配者之后,SessionManager的每一个请求都会被这个AccessTokenAdapter适配一遍。具体的代码实现逻辑会在后续的章节中给出。那么到这里,我们已经掌握了好几种添加headers得到方法了。AccessTokenAdapter的使用方法:

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

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

关于RequestAdapterRequestRetrier的综合运用,Alamofire给出了一个一个这样的例子:

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

我们把上边的代码拆解成以下的使用场景:

  • 客户端发送的每一个请求都要包含一个token,这个token很可能会过期,过期的token不能使用,因此通过adapt方法把token添加到请求的header中
  • 当使用现有的token请求失败后,如果是token过期导致的请求失败,那么就通过should方法重新申请一个新的token

使用方法:

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

6.自定义响应序列者

关于Alamofire中自定义序列响应者。Alamofire已经为我们提供了Data,JSON,strings和property lists的解析。为了演示自定义的功能,我们要完成一下两件事:

  • 为Alamofire扩展一个XML的解析
  • 直接把服务器返回的数据解析成对象,比方说User

为Alamofire扩展一个XML的解析

在做任何事情事前,都应该先设计好错误处理方案:

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

XML解析:

extension DataRequest {
    static func xmlResponseSerializer() -> DataResponseSerializer<ONOXMLDocument> {
        return DataResponseSerializer { request, response, data, error in
            // Pass through any underlying URLSession error to the .network case.
            guard error == nil else { return .failure(BackendError.network(error: error!)) }

            // Use Alamofire's existing data serializer to extract the data, passing the error as nil, as it has
            // already been handled.
            let result = Request.serializeResponseData(response: response, data: data, error: nil)

            guard case let .success(validData) = result else {
                return .failure(BackendError.dataSerialization(error: result.error! as! AFError))
            }

            do {
                let xml = try ONOXMLDocument(data: validData)
                return .success(xml)
            } catch {
                return .failure(BackendError.xmlSerialization(error: error))
            }
        }
    }

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

可以看出,这个解析是在DataRequest基础上进行扩展的,当然也可以在DownloadRequest上扩展,xmlResponseSerializer函数的返回值是一个函数,这种处理方式在Alamofire中经常出现,完全可以把函数当成一种数据来对待。response函数会把这个闭包函数加入到task代理的队列中,在请求完成后会被调用,总之,这是一系列的过程,我会在后续的文章中详细说明。

- 直接把服务器返回的数据解析成对象,比方说User

在开发中,能够直接把服务器返回的数据转换成对象还是很有价值的。接下来我们看看用代码是如何实现的:

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

ResponseObjectSerializable这个协议是关键,这个协议提供了一个初始化方法,方法的参数有两个,一个是服务器返回的响应,另一个是被转化后的数据,着这个例子中使用的是JSON。也就是说对象一定要实现这个协议,在这个协议方法中拿到这两个参数,然后给自己的属性赋值就可以了 。

User的代码:

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

Alamofire的文档中还掩饰了一个系列成[User]的例子,由于篇幅的原因,在这里就不解释了。

7.安全

Alamofire中关于安全策略的使用,会放到后边的文章中介绍。

8.网络状态监控

主要用于实时监控当前的网络情况

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

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

manager?.startListening()

有一下几点值得注意:

  • 不要用该监控来决定是不是发送请求,应该直接发送
  • 当网络恢复之后,尝试重新发送请求
  • 状态吗可以用来查看网络问题的原因

总结

以上就是本篇的所有内容,知识大概的讲解了Alamofire的使用技巧,真正能够提高代码水平的源码解读,我会尽量完成。

如果有任何错误之处,欢迎提出,多谢了。

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

推荐阅读更多精彩内容