Alamofire - 理解RequestInterceptor的设计与实现

2,497 阅读8分钟

上篇,我们梳理了Alamofire的工作流程。今天我们继续研究,这次主要梳理RequestInterceptor(拦截器)的相关内容。

RequestInterceptor是一个协议,它本身没有任何要求,只是遵循了和两个协议:

public protocol RequestInterceptor: RequestAdapter, RequestRetrier {}

也就是说,要想实现一个拦截器,满足RequestAdapterRequestRetrier即可。下面分别瞅瞅这俩各有什么要求。

RequestAdapter

RequestAdapter是一个请求适配器。对于一个请求,我们可以通过Adapter来决定如何操作该请求。具体定义如下:

public protocol RequestAdapter {
    /// 通过urlRequest和session两个入参,来决定如何处理该请求。处理完的结果通过completion进行回调。例如:
    /// 1. 我们修改该请求,然后回调 completion(.success(someRequest))。这样框架可以继续处理返回的Request
    /// 2. 判断某些必要条件不成立,决定将该请求失败掉,可以回调 completion(.failure(someError))
    /// 由于回调是通过closure进行的,这里也可以做异步操作
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void)

    /// 和上一个方法类似,这里我们得到的入参更丰富了,多了requestID (RequestAdapterState是个结构体,包含了session和requestID)
    func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void)
}

如果我们将一个http请求事务划分为两个阶段:请求发送之前收到响应之后,那么这里的RequestAdapter就作用在第一阶段的。RequestRetrier是否作用于第二阶段呢?一起往下看。

RequestRetrier

public protocol RequestRetrier {
    /// 在请求出错后,通过该方法决定是否需要重试。我们可以有4种处理方式(来自RetryResult枚举)
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void)
}

可以看到这里确实是作用在收到响应之后,只不过限制了在失败的场景。

这里可能的处理方式有4种:

public enum RetryResult {
    /// 立即重试
    case retry
    /// 在指定时间之后重试
    case retryWithDelay(TimeInterval)
    /// 无需重试
    case doNotRetry
    /// 无需重试并报错
    case doNotRetryWithError(Error)
}

我只需将具体的处理方式通过completion回调回去即可实现对应的效果。

好了,静态的部分暂时分析这么多,下面瞅瞅RequestInterceptor是如何运行的。

RequestInterceptor是如何工作的

为了观察RequestInterceptor的工作方式,我新建了一个SignRequestInterceptor,用来完成对请求的签名,将签名通过请求头传递:

RequestInterceptor的单元测试有点复杂,对于理解RequestInterceptor是个负担,所有这里使用我们自己写的例子。

class SignRequestInterceptor: RequestInterceptor {
    // MARK: - RequestAdapter
    
    func adapt(_ urlRequest: URLRequest, using state: RequestAdapterState, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        let request = sign(request: urlRequest)
        completion(.success(request))
    }
    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        let request = sign(request: urlRequest)
        completion(.success(request))
    }
    // MARK: - RequestRetrier
    
    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        completion(.retry)
    }
    // MARK: -
    
    /// 模拟签名请求,使用url作为签名内容,便于观察
    private func sign(request: URLRequest) -> URLRequest {
        guard let urlString = request.url?.absoluteString else {
            return request
        }
        var retRequest = request
        retRequest.headers.add(name: "X-SIGN", value: urlString)
        return retRequest
    }
}

有了自己的RequestInterceptor后,我们有两种方式去使用它:

  1. Session级别,在生成自己的Session时同时并配置Interceptorlet session = Session(interceptor: SignRequestInterceptor())。这种方式配置的,会作用于该Session创建的每一个Request上。

  2. Request级别,在构建请求实例时,传入:AF.request("https://httpbin.org/post", interceptor: SignRequestInterceptor())。这种方式配置的只作用于当前Request

RequestAdapter 工作流程

我们在这里打上断点:

func sign(request: URLRequest) -> URLRequest { ... }

发起请求后可以看到如下调用栈:

截屏2021-11-17 下午10.45.33.png

这里是我们上篇讲到的最后的请求配置阶段,在该阶段,我们的Interceptor得以调用。如何处置就看大家的想象了。

RequestRetrier 工作流程

同样,我们在下面的方法打上断点:

func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) { ... }

然后将网络断掉,模拟网络错误的情况。对应的调用栈如下:

request-fail.png

可追溯到的调用起点是系统的回调方法: SessionDelegate.urlSession(_:task:didCompleteWithError:)。在分析具体实现之前,我们先了解下这里引入的几个新面孔:

  1. SessionDelegate:实现了众多URLSessionDelegate,衔接系统框架和Alamofire。它包含了几个重要的属性:
open class SessionDelegate: NSObject {
    /// 文件管理器,主要负责下载请求的文件操作
    private let fileManager: FileManager
    /// 依赖项,多数的操作都依赖该属性,参考 SessionStateProvider
    weak var stateProvider: SessionStateProvider?
    /// 事件监听器,负责通知各种事件
    var eventMonitor: EventMonitor?
}
  1. SessionStateProvider:为了不直接使用Session对象,这里使用SessionStateProviderSessionSessionDelegate隔离开。
protocol SessionStateProvider: AnyObject {
    /// https证书校验器
    var serverTrustManager: ServerTrustManager? { get }
    /// 重定向处理器
    var redirectHandler: RedirectHandler? { get }
    /// 缓存处理器
    var cachedResponseHandler: CachedResponseHandler? { get }
    /// task到request的映射
    func request(for task: URLSessionTask) -> Request?
    /// 统计信息的报告
    func didGatherMetricsForTask(_ task: URLSessionTask)
    /// 任务完成报告
    func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void)
    /// task级别的证书映射
    func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential?
    /// 通知请求取消
    func cancelRequestsForSessionInvalidation(with error: Error?)
}

下面是Session对于该协议的实现:

extension Session: SessionStateProvider {
    /// 通过task获取request,直接从requestTaskMap类字典结构中取值
    func request(for task: URLSessionTask) -> Request? {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        return requestTaskMap[task]
    }
    /// 在task完成之后,在判断收集到统计信息后,直接回调completion。否则使用waitingCompletions进行收集
    func didCompleteTask(_ task: URLSessionTask, completion: @escaping () -> Void) {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        // 只有在统计信息已经收集才会返回true
        let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterCompletingTask(task)
        if didDisassociate {
            completion()
        } else {
            waitingCompletions[task] = completion
        }
    }
    /// 收集到统计信息,判断task完成后,调用waitingCompletions记录的回调
    func didGatherMetricsForTask(_ task: URLSessionTask) {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        // 只有在task完成后才会返回true
        let didDisassociate = requestTaskMap.disassociateIfNecessaryAfterGatheringMetricsForTask(task)
        if didDisassociate {
            waitingCompletions[task]?()
            waitingCompletions[task] = nil
        }
    }
    /// 获取Request级别的认证信息
    func credential(for task: URLSessionTask, in protectionSpace: URLProtectionSpace) -> URLCredential? {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        return requestTaskMap[task]?.credential ??
            session.configuration.urlCredentialStorage?.defaultCredential(for: protectionSpace)
    }
    /// Session失效时,将请求都失效掉
    func cancelRequestsForSessionInvalidation(with error: Error?) {
        dispatchPrecondition(condition: .onQueue(rootQueue))
        requestTaskMap.requests.forEach { $0.finish(error: AFError.sessionInvalidated(error: error)) }
    }
}
  1. EventMonitor: 事件监听器。这也是一个协议,遵循该协议的可以成为事件监听器,可监听到URLSession一系列代理事件和Request生命周期内的各种事件。监听器的所有事件都有默认的实现,在对应的扩展中。同时,Alamofire也提供了多个实现:

    • CompositeEventMonitor监听器的混合器,可以将多个监听器通过该类合并在一起。
    • ClosureEventMonitor闭包监听器,将EventMonitor的各个方法通过闭包回调。
    • NSLoggingEventMonitor日志监听器,负责输出日志到控制台。
    • AlamofireNotifications通知监听器,负责将对应事件以通知的形式发出,这里只实现了部分监听方法。
  2. RequestDelegate: 和SessionStateProvider类似,Request通过该协议和Session通信

public protocol RequestDelegate: AnyObject {
    /// 获取Session的配置,用于生成cURL命令
    var sessionConfiguration: URLSessionConfiguration { get }
    /// 是否应该立即发起请求。默认true。在request.responseXXX的时候会根据这个参数觉得是否resume这个request
    var startImmediately: Bool { get }
    /// 执行清理操作。如:下载完成后移除下载文件
    func cleanup(after request: Request)
    /// request出错了,请求对于错误的处理方式
    func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void)
    /// 对于出错的request,触发重试
    func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?)
}

下面是Session对于该协议的实现:

extension Session: RequestDelegate {
    /// 直接返回session(URLSession)的配置
    public var sessionConfiguration: URLSessionConfiguration {
        session.configuration
    }
    /// 直接返回session(Session)的属性startRequestsImmediately
    public var startImmediately: Bool { startRequestsImmediately }
    /// 清理时,将该Request从活动的请求记录中删除
    public func cleanup(after request: Request) {
        activeRequests.remove(request)
    }
    
    /// 决定如何处理已经出错的请求
    /// 1. 未能获取到请求重试器:直接回调不再重试
    /// 2. 获取到请求重试器:根据请求重试器的结果处理:
    /// a: 重试器返回了错误:回调包装后的错误(AFError)
    /// b: 其他:直接回调
    public func retryResult(for request: Request, dueTo error: AFError, completion: @escaping (RetryResult) -> Void) {
        guard let retrier = retrier(for: request) else {
            rootQueue.async { completion(.doNotRetry) }
            return
        }
        // 这里会调用我们的重试器
        retrier.retry(request, for: self, dueTo: error) { retryResult in
            self.rootQueue.async {
                guard let retryResultError = retryResult.error else { completion(retryResult); return }

                let retryError = AFError.requestRetryFailed(retryError: retryResultError, originalError: error)
                completion(.doNotRetryWithError(retryError))
            }
        }
    }
    /// 重试一个request。
    public func retryRequest(_ request: Request, withDelay timeDelay: TimeInterval?) {
        rootQueue.async {
            let retry: () -> Void = {
                // 取消的request不再重试
                guard !request.isCancelled else { return }
                // 准备阶段:记录重试次数、重置各种进度
                request.prepareForRetry()
                // 请求的配置阶段
                self.perform(request)
            }
            // 若存在延迟,通过gcd执行;否则直接触发重试
            if let retryDelay = timeDelay {
                self.rootQueue.after(retryDelay) { retry() }
            } else {
                retry()
            }
        }
    }
}

接下来的工作就简单了。RequestRetrier流程其实就是以上个各种方法的使用:

  1. SessionDelegate.urlSession(_:task:didCompleteWithError:)接收到系统回调。
  2. sessionDelegate通过stateProvider回调Session.didCompleteTask(_:completion:)告知Session任务完成了。此时Session会根据具体状态决定是否从requestTaskMap记录中删除task。
  3. sessionDelegate回调Request.didCompleteTask(_:with:)。此时Request会对响应进行验证,之后进入下一步的重试判断阶段。
  4. Request.retryOrFinish(error:)若没有错误产生,直接进入完成阶段。否则进入下一步的重试。
  5. Request会调用delegate(Session).retryResult(for:dueTo:completion:)获得是否有重试的结果,若需要重试,会调用delegate(Session).retryRequest(_:withDelay:)进行重试。我们实现的SignRequestInterceptor也正是在Session.retryResult(for:dueTo:completion:)方法中获得被调用的机会。

大致流程就是这些,大家可以先对各个参与者有个大致印象,然后跟着流程细看。总体还是比较清晰的。

Alamofire提供的RequestInterceptor(s)

框架内部也实现了一些常用的拦截器,如下:

  1. open class Adapter: RequestInterceptor { ... }:提供闭包风格的请求适配器。
  2. open class Retrier: RequestInterceptor { ... }:提供闭包风格的请求重试器。
  3. open class Interceptor: RequestInterceptor { ... }:拦截器的混合器,可以将多个拦截器包装起来。
  4. public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator { ... }:提供授权功能。
  5. open class RetryPolicy: RequestInterceptor { ... }:提供更丰富的重试条件控制,如允许重试的次数,允许重试的请求方法,每次重试过后下次重试的间隔等等等。

AuthenticationInterceptorRetryPolicy简直不要太强!💯,下面会有专门的文章来分析它们,关注期待吧。🤣

总结

今天我们主要梳理里拦截器的工作流程。可以看到,它就是在一个请求事务的前后,给了我们相应的响应机会,其实内容并不复杂。

另外,对应的代码分析,我放在了GitHub上(分支:risk),希望对你有所帮助。