Alamofire之request补充

1,303 阅读7分钟

上一篇 Alamofire之request中部分内容写得不是很清晰,所以在这一篇中进行补充。

一、下标法

在调用 Alamofire.request 时,最后会来到 SessionManager.request 如下代码:

那么小伙伴是否主要到红框中的代码呢?

这种语法叫下标法。一般用于数组和字典中,那么 delegate 作为 SessionDelegate 的实例,是如何拥有这种能力的呢?

这其实是 Swift 语法的一种。如果重写对象的 subscript 方法,就可以使用下标法。在 SessionDelegate 中,我们可以看到如下代码:

所以,可以通过 SessionDelegate[task] 获取到对应的 request

那么为什么Alamofire要通过 SessionDelegatetask 获取其 request 呢?

我们知道,SessionDelegate 是面向开发者的协议集合,其内部实现了所有和URLSession有关的Delegate。但是真正处理任务的是 task 对应 request 的 delegate DownloadTaskDelegateDataTaskDelegateUploadTaskDelegate等来具体处理回调。

举个栗子🌰:比如下载成功之后,需要将下载的文件移动到指定的目录,这时会回调 SessionDelegate实现的 func urlSession( _ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) 方法。那么一定就是 SessionDelegate 来处理吗?答案是否定的。请看代码:

这里很明显,分为了两步

  • 如果开发者已经给 downloadTaskDidFinishDownloadingToURL 赋值了,那么就回调这个闭包。
  • 如果没有闭包,则通过 self[downloadTask]?.delegate 获取到对应task的delegate,让delegate去处理回调。

通过这种方式,实现 SessionDelegate 的任务分发功能。让处理具体事务的 delegate 去处理对应的事务,避免了其内部逻辑混乱。

二、RequestAdapter

我们在回到 SessionManager.request 中。

小伙伴看到这个 adapter 是不是又觉得有点懵?😳

这个 adapter 能干什么呢?我们先看看这个属性的是个什么鬼👻? 然后我们就看到了 open var adapter: RequestAdapter?

而且很奇怪的是也没有初始化,那我们再看看 RequestAdapter 是什么。

这是一个协议。这个协议可以做什么呢?既然Alamofire没有实现,那么必然需要我们自己来实现。

我们来举个栗子🌰: 我们创建一个类 BOAdapter 并继承自 RequestAdapter 协议。 协议要求必须实现一个 adapt 方法。

class BOAdapter: RequestAdapter {
    func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
        
        return urlRequest
    }
}

adapt 方法传入一个 URLRequest 并返回一个 URLRequest。我们先不对其做任何处理,直接返回这个 urlRequest,然后再调试一下,这个 urlReqeust 到底是什么。

我们该如何使用这个 BOAdapter 呢?因为 adapterSessionManager 的属性,所以我们可以这样:

let urlBD = "https://www.baidu.com"

// 使用实现的 adapter
Alamofire.SessionManager.default.adapter = BOAdapter()
Alamofire.request(urlBD, method: .get, parameters: ["user": "bo"]).response { (response) in
    print(response)
}

运行,调试,可以知道,urlRequest 就是我们请求发起的 urlRequest。我们能拿到这个 request,那么我们就可以做很多事情了,比如:

1、给request添加公共参数

func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
    
    var request = urlRequest
    
    // 设置参数
    request.setValue("token", forHTTPHeaderField: "token")
    
    return urlRequest
}

2、重定向

func adapt(_ urlRequest: URLRequest) throws -> URLRequest {
    
    // 如果是百度地址开头,则重定向到QQ地址
    if let url = urlRequest.url?.absoluteString, url.hasPrefix("https://www.baidu.com") {
        
        let request = URLRequest(url: URL(string: "https://www.qq.com")!)
        
        return request
    }
    
    return urlRequest
}

还有其他的一些用法,这里就不一一举例了。

三、validate

在网络请求返回结果之后,一般我们还会对请求结果做一次验证。

let urlBD = "https://www.baidu.com"

Alamofire.request(urlBD, method: .get, parameters: ["user": "bo"])
    .response { (response) in
        print(response)
    }.validate { (request, response, data) -> Request.ValidationResult in
        
        guard let _ = data else {
            
            return .failure(NSError(domain: "NO Response", code: 10086, userInfo: nil))
        }
        
        guard response.statusCode == 200 else {
            
            return .failure(NSError(domain: "Error", code: response.statusCode, userInfo: nil))
        }
        
        return .success
}

因为这一步需要服务器配合,所以请小伙伴自行验证。

四、RequestRetrier

在网络请求结束后,会回调 func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) 方法。 在这个方法中,我们会看到下面这段代码:

通过注释,我们知道这段代码的作用是:如果发生错误并设置了 retrier,则异步询问 retrier 是否应重试请求。否则,通过通知 delegate 来完成任务。

那么这个 retrier 又该如何设置呢?查看这个属性 var retrier: RequestRetrier? 并没有初始化,所以,也需要我们自己来设置。

再查看 RequestRetrier:

这是一个协议。并且需要我们自己来实现。

举个栗子🌰: 我们自定义一个 BORetrier 继承自 RequestRetrier协议。协议要求实现 should 方法。

should 方法有四个参数,前三个参数都比较好理解。但是第四个参数是一个闭包,这又是什么呢?我们查看其源码:

通过注释我们知道:这个一个闭包,并在 RequestRetrier 决定是否一个 request 应该被重试的时候执行。

说明我们可以通过这个闭包,控制是否重试当前的请求。

class BORetrier: RequestRetrier {
    
    var count: Int = 0
    
    func should(_ manager: SessionManager, retry request: Request, with error: Error, completion: @escaping RequestRetryCompletion) {
        
        print("manager = \(manager)")
        print("request = \(request)")
        print("error = \(error)")
        
        // 最多重试3次
        if count < 3 {
            completion(true, 1)
            count += 1
        } else {
            completion(false, 0)
        }
    }
}

在我们自定义的 BORetrier 中,我们规定,重试次数不超过3次。

我们可以如下面的代码这样使用 BORetrier

let urlBD = "https://www.xxxxxx.com"

Alamofire.SessionManager.default.retrier = BORetrier()
Alamofire.request(urlBD, method: .get, parameters: ["user": "bo"])
    .response { (response) in
        print(response)
    }.validate { (request, response, data) -> Request.ValidationResult in
        
        guard let _ = data else {
            
            return .failure(NSError(domain: "NO Response", code: 10086, userInfo: nil))
        }
        
        guard response.statusCode == 200 else {
            
            return .failure(NSError(domain: "Error", code: response.statusCode, userInfo: nil))
        }
        
        return .success
}

运行,因为地址 www.xxxxxx.com 无法访问,所以请求返回错误。但是因为有重试机制,所以会在重试3次后再提示错误。

当然,这里也不一定使用次数限制,小伙伴还可以使用其他方式控制是否重试。

五、queue

还是在 SessionManager.request 方法中,不知道小伙伴们是否有注意到这个地方:

我们知道一般调用的是 task.resume(),而这里的 request.resume()又是怎么回事呢?我们一起来分析一下。

原来是里面获取了一个 task,再调用 task.resume()

同时我们还看到这样一句代码:delegate.queue.isSuspended = false。在没有任务时,让delegate的队列 queue 恢复。咦❓这是个什么操作呢?这里的队列有什么作用呢?

我们先来查看一下源码,看看这个 queue 到底是什么。

我们看到这个 queueOperationQueue 的实例。并且在下方 TaskDelegate 的初始化方法我们可以找到 queue 的初始化。

maxConcurrentOperationCount = 1 表明 queue 是一个串行队列。isSuspended = true 表明这个队列默认是挂起的。

那么这个串行队列到底能做什么呢?这就和网络请求的 Timeline 有关了。

小伙伴都知道,在response 方法中,打印 response 会打印出请求结果,在其中会有这样一段字符串 Timeline: { "Request Start Time": 588156668.544, "Initial Response Time": 588156668.835, "Request Completed Time": 588156668.838, "Serialization Completed Time": 588156668.838, "Latency": 0.291 secs, "Request Duration": 0.294 secs, "Serialization Duration": 0.000 secs, "Total Duration": 0.295 secs } 这就是网络请求的 Timeline,标识出了网络请求的起始时间等时间点。那么,这是如何实现的呢?

1、网络请求发起时间

resume 方法中,会给 startTime 赋值当前时间戳,这就是网络请求的发起时间。

2、网络请求结束时间

在初始化 DataRequest 时,会向 queue 队列中添加一个任务,获取当前的时间戳赋值给 endTime,这就是网络请求结束时间。

但是因为当前队列默认为挂起状态,所以不会执行里面的任务。那么这个任务在何时执行呢?

在网络请求完成回调 didCompleteWithError 方法时会恢复 queue 队列。

这时就会执行 queue 队列内的任务,完成 endTime 的赋值。

3、初始化Response时间

在网络请求开始返回数据时,会设置 initialResponseTime 为当前时间戳,这个时间就是初始化Response的时间。

4、序列化结束时间

在调用 response 方法时,会向 queue 队列中添加一个任务。因为当前未使用自定义的序列化方法,所以直接返回请求回来的数据,并返回 self.timeline

因为 timeline 是一个计算属性。

所以在调用 self.timeline 时,会初始化 Timeline 对象,并将当前时间戳作为参数 serializationCompletedTime 的值传递给 Timeline 对象。

这个 serializationCompletedTime 就是序列化结束的时间。同时这个任务也是在队列恢复时执行。

5、请求总时长

totalDuration = serializationCompletedTime - requestStartTime 即是当次网络请求的总时长。

其余时间点则需要小伙伴自行探索咯。^_^

附Timeline时序图


以上则是本篇的补充内容。若有不足之处,请评论指正。