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

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

前言

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

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

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

文件名描述
1.AFError.swift对错误的封装,包含了Alamofire中所有可能出现的错误,使用enum实现,很有意思
2.Notifications.swiftswift中通知的用法,这个跟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
  • responseString 序列化为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的使用技巧,真正能够提高代码水平的源码解读,我会尽量完成。

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

转载于:https://www.cnblogs.com/machao/p/6430525.html

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
回答: 在PyTorch源码中,有几个关键的类和函数需要解读。首先是torch.utils.data.Sampler类,它负责提供一种遍历数据集所有元素索引的方式。它的len()方法在计算数据集长度时是必要的。\[1\]接下来是torch.utils.data.DataLoader类,它是PyTorch数据加载的核心。它可以加载数据集,并支持多种参数设置,如batch size、shuffle、num_workers等。\[2\]此外,还有一个vocab类,它用于统计解释变量中涉及到的单词的出现频率,并为每个单词分配一个整数作为该单词的整数表示。@classmethod是一个装饰器,用于定义类方法。\[3\]在源码中还有一些其他的类和函数,但这些是其中的一部分。 #### 引用[.reference_title] - *1* *2* [PyTorch 源码解读之 torch.utils.data:解析数据处理全流程(非常好,一篇足够)](https://blog.csdn.net/Highlight_Jin/article/details/126206958)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [Transformer实现以及Pytorch源码解读(一)-数据输入篇](https://blog.csdn.net/weixin_41806489/article/details/128380667)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值