Skip to content
Gaoxuefeng's Blog
Go back

Alamofire 源码里的设计课

读 Alamofire 源码的起因是项目里的网络层写得不好。Singleton、Cookie 注入写了三处、重试逻辑散落在各个 ViewModel 里。想重构,但不知道该往哪个方向走。与其自己想,不如看看 44k star 的库怎么做的。

以下是读完之后记下来的东西。不是 API 使用教程,是设计层面的收获。

先理解它在包装什么

Alamofire 包装的是 URLSession。不理解原生 API 的痛点,就不理解 Alamofire 每一层抽象在解决什么。

URLSession 的三层架构:Configuration(配置行为)→ Session(管理连接池)→ Task(执行请求)。三种使用方式:Completion Handler、async/await、Delegate。

原生写法长这样:

var request = URLRequest(url: URL(string: "https://api.example.com/user")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
request.httpBody = try? JSONEncoder().encode(params)

URLSession.shared.dataTask(with: request) { data, response, error in
    guard error == nil,
          let response = response as? HTTPURLResponse,
          (200...299).contains(response.statusCode),
          let data else { return }
    DispatchQueue.main.async {
        let user = try? JSONDecoder().decode(User.self, from: data)
    }
}.resume()

Alamofire 写法:

AF.request("https://api.example.com/user", method: .post,
           parameters: params, encoder: JSONParameterEncoder.default)
    .validate()
    .responseDecodable(of: User.self) { response in }

行数差异不是重点。重点是原生 API 的九个结构性问题:每次手写 URLRequest 样板代码、错误处理分散在三个地方(网络/HTTP/解码)、回调在后台队列忘切主线程就 crash、没有内建重试、没有统一的 Header 注入点、Task 默认 suspended 忘记 resume 就发不出去、一个 URLSession 只有一个 delegate 多请求要自己路由。

Alamofire 的每一层抽象都精确对应一个痛点,不多不少。

36 个文件的全景

Source/
├── Core/        (18 个文件) 核心引擎
│   ├── Session.swift           请求工厂与调度中心
│   ├── Request.swift           所有请求的基类,状态机 + 可变状态
│   ├── DataRequest.swift       内存数据请求
│   ├── DownloadRequest.swift   文件下载
│   ├── UploadRequest.swift     上传(继承 DataRequest)
│   ├── SessionDelegate.swift   URLSession 回调桥接
│   ├── Protected.swift         线程安全泛型容器
│   ├── RequestTaskMap.swift    Request ↔ Task 双向映射
│   ├── AFError.swift           统一错误类型
│   └── ...
├── Features/    (18 个文件) 可插拔功能
│   ├── EventMonitor.swift      事件观察协议
│   ├── RequestInterceptor.swift 请求拦截器
│   ├── ResponseSerialization.swift 响应序列化
│   ├── RetryPolicy.swift       重试策略
│   ├── Concurrency.swift       async/await 适配
│   └── ...
└── Extensions/  (6 个文件) 标准库扩展

Core 和 Features 的分界很清楚:Core 是引擎,Features 是可插拔的行为。这个分层本身就值得学。

Protected — 线程安全的正确做法

这是我读到的第一个让我停下来反复看的设计。

Alamofire 没有给每个变量各加一把锁。它把所有可变状态收进一个 struct(MutableState),然后用一个泛型容器 Protected<Value> 包住,一把锁保护所有东西。

struct MutableState {
    var state: State = .initialized
    var requests: [URLRequest] = []
    var tasks: [URLSessionTask] = []
    var metrics: [URLSessionTaskMetrics] = []
    var retryCount = 0
    var error: AFError?
    var responseSerializers: [...]
    // ... 十几个字段,一把锁
}

let mutableState: Protected<MutableState>

分散加锁(N 把锁)的问题是:死锁风险、性能差、状态不一致。集中到一个 struct 里用一把锁,原子性天然保证。

有意思的是 Protected 的实现细节。它没有用 OSAllocatedUnfairLock.withLock,而是自己定义了 Lock 协议,手动 lock() / unlock()

private func around<T>(_ closure: () throws -> T) rethrows -> T {
    lock.lock(); defer { lock.unlock() }
    return try closure()
}

原因是 withLock 要求闭包是 @Sendable,但 Protected<Value> 是泛型容器,Value 没有 Sendable 约束(MutableState 里有 URLSessionTask 这种非 Sendable 类型)。用 withLock 会编译报错。

不是老式写法,是唯一正确的做法。

整个类标记 @unchecked Sendable——锁已经保证了线程安全,但编译器推断不出来,需要手动告诉它。内部的 value 属性用 nonisolated(unsafe) 标记——Swift 6 严格模式下,Sendable 类型的 var 必须证明线程安全,这个标记表示”我自己用锁保证”。

Request 状态机

每个请求有 5 个状态:

initialized → resumed ⇄ suspended → cancelled/finished

cancelledfinished 是终态,不能转出。resumedsuspended 可以互相切换。状态转换通过 canTransitionTo 方法穷举所有合法路径,非法转换直接 return。

一个 Request 可能产生多个 URLSessionTask——重试时老 task 废弃,创建新 task。所以 tasksmetrics 都是数组:

场景tasks 数组
正常请求[task0]
重试 1 次[task0, task1]
重试 2 次[task0, task1, task2]

日常使用取 task(= last),调试分析取 firstTask,完整历史取 tasks

retryOrFinish — 决策分离的枢纽

这是整个库最精妙的方法,四五十行代码。

func retryOrFinish(error: AFError?) {
    guard !isCancelled, let error, let delegate else { finish(); return }
    
    delegate.retryResult(for: self, dueTo: error) { retryResult in
        switch retryResult {
        case .doNotRetry:           self.finish()
        case .doNotRetryWithError:  self.finish(error: retryError)
        case .retry, .retryWithDelay: delegate.retryRequest(self, ...)
        }
    }
}

guard 的三个条件各有语义:已取消不重试、error 为 nil 说明成功直接 finish、delegate 是 weak 如果 Session 已释放就降级。

关键设计:Request 自己不判断该不该重试。它只知道”出错了”,把决策权完全交给 delegate(Session)→ interceptor(你的业务代码)。四个不同的失败路径(URLRequest 构造失败、Adapter 失败、网络错误、响应验证失败)全部汇聚到这一个方法。

Request 只负责问和执行,不包含任何重试策略。策略在 interceptor 里,想换随时换。

拦截器的三个槽位

public protocol RequestAdapter   { func adapt(...) }
public protocol RequestRetrier   { func retry(...) }
public typealias RequestInterceptor = RequestAdapter & RequestRetrier

内置的 Interceptor 组合器提供三个数组:

参数语义
adapters: [RequestAdapter]只修改请求(注入 Header、Cookie)
retriers: [RequestRetrier]只处理重试
interceptors: [RequestInterceptor]两者都做

拦截管道是 Session 级先执行、Request 级后执行的双层结构。adapt 按顺序串行(前一个的输出是后一个的输入),retry 则是第一个返回 .retry 的 retrier 胜出。

实际项目中最常用的场景是认证拦截器。多个请求同时 401 时,只刷新一次凭证,其他请求排队等待。实现要素是 Protected<AuthState> 状态机 + pending 回调队列,attemptToTransitionTo(.authenticating) 保证原子性——只有第一个请求成功转换并执行刷新。

序列化流水线

用户可以链式添加多个序列化器:

request
    .responseDecodable(of: User.self) { ... }  // serializers[0]
    .responseString { ... }                     // serializers[1]

执行机制用了一个巧妙的 trick:completions.count 既是”已完成数”也是”下一个要执行的索引”。不需要单独维护 currentIndex 变量。

第 1 轮: completions.count = 0 → 执行 serializers[0]
第 2 轮: completions.count = 1 → 执行 serializers[1]
第 3 轮: completions.count = 2 ≥ serializers.count → 执行所有 completions → cleanup

重试时 completions 清空(游标归零),但 serializers 不清——重试后从头重新序列化。

还有一个细节:序列化器的实际执行放在锁外。write 闭包里只读取状态、准备好要做的事(返回一个闭包),锁释放后再执行。避免持锁期间做耗时的 JSON 解码。

链式 API 的编译器实现

@discardableResult
public func cancel() -> Self { ... return self }

Self(大写)在子类中解析为子类类型:DataRequest 调完 cancel() 返回的仍然是 DataRequest,不会退化成 Request。编译器为子类自动生成 thunk 函数,内部用 unchecked_ref_cast 转换类型,零运行时开销。

为什么用 return self 而不是构造新实例?因为构造新实例需要 required init,Request 的初始化参数很复杂,强制子类实现没有意义。return self 就够了。

EventMonitor — 无侵入的观测

30 多个方法的协议,全部有默认空实现。CompositeEventMonitor 把多个监控者组合成一个,Session 级和 Request 级的事件统一分发。

实际项目中用它做了两件事:一是网络活跃指示器(requestDidResume 时 +1,requestDidFinish 时 -1),二是请求 Metrics 采集。业务代码完全不知道这些监控存在。

双队列分层

underlyingQueue    → 所有内部状态变更(串行)
serializationQueue → 响应序列化(串行,target 到 underlyingQueue)
用户指定队列        → 回调执行(默认 .main)

Public API(cancel/suspend/resume)可以在任何队列调用——内部用 mutableState.write {} 保证线程安全,状态变更后 async 回 underlyingQueue 执行副作用。Internal Event API 开头都有 dispatchPrecondition(condition: .onQueue(underlyingQueue)),Debug 模式下检查,Release 优化掉。

HTTPMethod 为什么是 struct 不是 enum

public struct HTTPMethod: RawRepresentable, Equatable, Hashable, Sendable {
    public static let get = HTTPMethod(rawValue: "GET")
    public static let post = HTTPMethod(rawValue: "POST")
    // ...
}

enum 不能扩展 case。如果有人需要 PATCH 或自定义方法,enum 就死了。struct + RawRepresentable + 静态常量,用起来跟 enum 一样(.get.post),但用户可以 HTTPMethod(rawValue: "CUSTOM")

同样的 pattern 在 HTTPHeaders.Name 里也用了。

读完之后做了什么

基于这些设计重构了项目的网络层。几个直接的产出:

最大的收获不是某个具体 pattern,是「集中式可变状态 + 单锁」这个思路。之前总觉得每个变量各加一把锁才”安全”,读完 Alamofire 才明白那是最不安全的做法。


Share this post on:

Previous Post
AutoSnippet 开发中的思考与洞察
Next Post
AutoSnippet 心路历程