iOS网络层工程范式迁移
引言
生活并非直线前进,而是在一次又一次的循环中向前。随着项目的更迭与技术栈的变化, 又一次站在了这个熟悉的网络层设计问题前——这已经是第几次,我大抵也记不太清了。有趣的是,问题几乎未曾改变,但在不同的技术背景与开发阶段下,循环的终点却彼此并不矛盾。为什么网络层总在被重构? 我反复更换的,究竟是网络请求库,还是对“网络层”这一概念本身的理解?当开发生产力不断提高,网络层的意义,是否也在随之发生变化?
回答这些问题,或许需要暂时抽离当下的技术语境,逆拨时间的指针,回到网络层第一次成为“工程问题”的年代。
网络层的第一次工程化
在 iOS 开发的早期,我们并没有一套足够简洁且稳定的 API 来完成一次 HTTP 请求。开发者需要直接面对 NSURLConnection 及其 delegate 回调,手动处理线程切换、状态维护、错误分支与生命周期管理。网络请求往往散落在各个业务代码中,高度依赖个人经验以及项目本身的成熟程度。
也正是在这一阶段,网络层开始显露出最初的工程问题: 如何将网络请求从零散的实现中抽离出来,使其具备一定的模块化与可复用性。
秩序的建立
AFNetworking 并不是第一个网络封装,但它是第一个被大规模接受为工程基础设施的解决方案。它所做的事情,在今天看来并不复杂:
- 将零散的 delegate 回调收敛为 block;
- 将请求调度交由 operation queue 统一管理;
- 提供一致的序列化、状态与网络可达性支持。
AFNetworking 并没有试图重新定义网络的抽象边界,它解决的是一个更朴素的问题:如何统一 HTTP 请求的工程流程。从这个角度看,它的历史地位并不来自于 API 的优雅,而在于完成了一件更基础的事情——让网络请求第一次脱离“个人技巧”,进入“团队工程”的范畴。
当网络请求不再是一种高风险操作,而成为可以被稳定复用的工程能力之后,新的问题也随之浮现: 在秩序建立之后,网络层是否还需要继续演进?
语言变了,其他什么都没变
随着 Swift 的出现,iOS 开发迎来了第一次明显的语言层级跃迁。更安全的类型系统、更清晰的控制流,以及对错误处理与并发模型的重新思考,都让开发者对“代码结构”本身产生了新的期待。然而,在 iOS 的发展历程中,语言层面的变化往往具有明显的滞后性。
在相当长的一段时间里,Swift 项目仍然沿用着 AFNetworking 所建立的工程范式:请求依然被视作一次次独立的操作,生命周期、错误分支与调度逻辑分散在各处。Alamofire 也正是在这样的背景下出现的——在它的早期阶段,它所承担的角色并不是重新定义网络层,而是提供一个 Swift 版本的 AFNetworking。
这并非缺乏野心,而是现实所迫。Swift 在早期版本中经历了频繁而剧烈的 API 变动,从语言特性到标准库,再到与 Foundation 的桥接关系,都在不断重写之中。这种不稳定性,对于试图迁移或学习新语言的开发者而言,无异于一盆又一盆的冷水。
也正因如此,在那个 Swift 生态尚未成熟的阶段,稳定性本身,才是最重要的工程价值。
范式二次迁移
随着 Swift 生态逐渐稳定,网络层的关注点也开始发生转移:它不再只是关心“是否能够稳定工作”,而是进一步走向“流程是否足够清晰、是否具备工程可控性”。Alamofire 正是在这一时期不断演进,并主动吸收了来自其他语言与平台的工程经验。它逐渐不再只是一个请求封装库,而是开始将一次网络请求视作一个具有明确生命周期的工程流程。
Session、Request、Interceptor、Retrier 等概念的引入,使得请求在进入与离开网络层时,拥有了可插拔、可观察、可扩展的处理节点。网络请求不再是一次孤立的调用,而成为一条可以被组合与调度的处理管线。也正是在这一阶段,Alamofire 从一套工程工具,演变为 iOS 网络开发中的事实标准。当人们谈论网络层时,几乎不需要再解释背景——Alamofire 本身,已经成为了那个时代 iOS 网络工程的默认前提。
再次抽象
随着 RxSwift、Combine 等声明式与函数式思想逐渐在 iOS 生态中流行,开发者开始尝试以更“描述性”的方式来组织代码结构。相比命令式地拼装请求参数,人们更希望先回答一个问题:系统中究竟存在哪些 API,它们的形态是什么?
正是在这样的背景下,Moya 在已经趋于完善的网络层实现之上,引入了一套基于 Swift 类型系统的 DSL(Domain-Specific Language)。通过 enum、case 与 protocol 的组合,Moya 尝试将网络接口本身建模为一种结构化、可推导的描述,而不再仅仅是零散的请求构造逻辑。
这种 DSL 的价值,并不在于提升网络请求的执行能力——这部分早已由 Alamofire 等底层实现所解决——而在于为接口组织、Mock 与测试提供了一种更具约束性的表达方式。网络层之上,第一次出现了“以接口为中心”的组织视角。
然而,抽象的上移并不意味着复杂度的消解。相反,当流程、接口与描述层不断叠加,网络层开始承载越来越多本不属于它的概念与责任。这种堆叠式的演进,也悄然将网络层推向了一个临界点——下一次变化,或许不再来自抽象本身。
控制权回归
真正的分水岭,并不是某一个网络库的发布,而是语言本身发生了改变。
随着 Swift Concurrency 的引入,异步执行、取消、错误传播与生命周期管理被统一纳入语言语义之中。在 Swift Concurrency 之前,即使不依赖任何第三方库,一个最基础的网络请求,也需要显式处理回调、线程、错误、生命周期等问题:
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
self.state = .error(error)
}
return
}
guard
let data = data,
let response = response as? HTTPURLResponse,
200..<300 ~= response.statusCode
else {
DispatchQueue.main.async {
self.state = .error(NetworkError.invalidResponse)
}
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
DispatchQueue.main.async {
self.state = .loaded(user)
}
} catch {
DispatchQueue.main.async {
self.state = .error(error)
}
}
}.resume()
在 Swift Concurrency 语境下,同样的请求可以被表达为
let request = URLRequest(url: url)
do {
let (data, response) = try await URLSession.shared.data(for: request)
guard
let response = response as? HTTPURLResponse,
200..<300 ~= response.statusCode
else {
throw NetworkError.invalidResponse
}
let user = try JSONDecoder().decode(User.self, from: data)
state = .loaded(user)
} catch {
state = .error(error)
}
从表面上看,变化只是几十行代码变成了几行。但真正发生转变的是:
- 执行顺序由语言控制,而非回调嵌套
- 错误传播通过 throw 统一语义
- 取消由
Task作为语言级能力管理 - 生命周期首次成为编译器可理解的结构
旧世界的终章
面对这一轮范式转变,Alamofire 并未停滞不前。相反,它持续尝试适配新的语言能力,引入 async/await 接口,与 Swift Concurrency 接轨。然而,作为一个诞生于前并发时代的工程体系,Alamofire 不可避免地背负着历史结构的惯性:其核心模型并非以 Sendable 为前提构建,内部仍存在大量可变状态;部分 API 在新的并发语境下显得不够“语言化”,更像是一层面向过渡期的兼容包装。这并非能力不足,而是时代断层所留下的工程痕迹。值得注意的是,Alamofire 并未因此止步。只要它仍在持续演进,仍在尝试靠近语言本身的表达边界,那么它的故事就尚未结束。甚至可以设想,在未来某一个关键版本节点,当工程结构真正与语言范式完成对齐,也许 6.x 之后的 Alamofire,仍存在“王者归来”的可能。
相比之下,Moya 则呈现出另一种截然不同的轨迹。它的核心价值建立在一套以 Endpoint 为中心的 DSL 之上。然而,随着 Swift 5 之后类型系统、协议与泛型能力的成熟,以及 Swift Concurrency 的到来,这套 DSL 却并未随语言一同演化。长期缺乏对 async/await 的原生支持,使得 Moya 在新的并发模型下逐渐失去立足点;而对既有 DSL 结构的高度依赖,也限制了它吸收语言层新能力的空间。当抽象停止生长,它便不再是助力,而开始成为负担。时至今日,Moya 已逐渐淡出主流讨论视野,这并非因为它曾经的设计不够优秀,而是因为它未能继续回应时代的变化。
这也引出了一个并不新,却常被忽略的事实:
再优秀的库,如果缺乏持续维护与演进,都无法抵御语言与时代本身的变化。
于是,一个新的问题被摆在面前:在语言接管流程之后,网络层还应该承担什么?
重新思考
当网络请求的传输与调度被语言层大幅简化之后,人们开始重新审视网络层本身的价值与边界。关注点不再是如何完整地接管请求生命周期,而是转向更基础的问题:如何让网络能力足够可组装、可测试、可替换,以及可观测。在这一语境下,网络层反而开始变“薄”——不再试图包揽流程,而是清晰地暴露能力边界。
从这个角度看,Moya 所代表的 API 描述 DSL 思路本身并未过时。过时的并不是“用 DSL 描述接口”这件事,而是围绕这一思路所构建的 API 形态,未能随着语言能力的演进而持续调整。如果回到 Moya 的源码与早期讨论中,会发现一个颇具时代感的事实:Moya 从一开始,便将自己定义为 Alamofire 的二次封装。这一定位在当时是合理且务实的——它使得 Moya 能够迅速建立在成熟执行层之上,专注解决接口组织的问题。
但也正是这一自我定位,在无形中为它设定了边界。当语言开始原生承担异步流程与生命周期管理之后,一个以“封装某个执行框架”为前提的 DSL,便很难继续向语言层靠近。这并非设计失误,而是时代变化所带来的结构性结果。许多早期的讨论与 issue,已经敏锐地意识到了这一张力,只是在当时的语境下,尚不足以推动一次彻底的转向。更别说之后的 Swift Concurrency 了。
当网络层变薄之后
当异步流程、错误传播与生命周期管理被语言原生承担之后,网络层本身所剩下的事情,其实变得异常简单。
在今天的语境下,一次网络请求不再需要被层层包装,也不需要被过度建模。它可以被拆解为几个极其清晰、彼此解耦的步骤:
- 通过某种协议定义,构建一个
URLRequest - 通过某种传输机制(Transport)发出请求
- 接收响应数据或者错误
- 将结果解码为某种业务所需的模型
这里并不存在一个“中心调度者”,也不存在隐式的流程控制。每一步都只是一个普通的函数调用,每一个内容都一被配置或者可插拔,每一个节点都拥有明确的输入与输出,并且都可以自然地抛出错误。
仅此而已,这就足够了。
但到这里, 许多读者估计会感到非常一位, 聊了这么久, 最后的结论就仅仅是这个? 对的, 结果就是这个, 有些简单、有些朴素。 但这个就是当前语境的最优解, 也是你回望过去得到的最优解。你可以使用任何形式的 API 来构建 URLRequest:无论是链式调用,还是拦截器式的配置;你也可以选择官方的 URLSession 作为传输实现,或者继续使用 Alamofire。这些选择本身并不重要,因为它们都是可替换的实现细节。
真正重要的,并不是你是否使用了某一个具体的网络库,而是你的系统是否允许它被替换。当网络层足够薄、足够中立时,底层实现的变化不再具有破坏性:它不会牵动业务结构,也不会迫使上层逻辑随之重写。在这样的前提下,讨论“是否还需要使用 Alamofire”,就不再是一场立场之争,而只是一次技术选择;系统也不会因为某个 API 的演进或退场,而整体失衡。
After Moya
在这条演进路径上,很难绕开 Moya。对我而言,它并不仅仅是一个网络库,更像是一次提前到来的启蒙。在刚入行的阶段,我第一次从网络层的视角,真正意识到“接口组织”“抽象边界”与“工程结构”这些概念的存在。那些一次又一次出现的架构图与层层抽象,并非晦涩,而是单纯地超出了当时的经验边界。
随着 Swift 语言范式的演变进化,Moya 所依赖的那套抽象语境不再能对的上实际语境。面对上百个的 issue 与尚未完整的Feature,这些内容已并非一两次重构所能解决的问题。也许在这个时代,之后的世界没有承载Moya的船只了。
伤感虽伤感, 但是日子总归都得向前, 项目中依然需要适应全新时代语言的网络层架构, 我开始重新审视那些曾经影响过自己的设计内容, 重新放回到当下的语言与工程语境之中。在这样的背景下,我为新的尝试取了一个名字——Moira。 它并非为了替代什么,而只是希望将那些曾经成立、并仍然有价值的思想,继续传递下去。
又一次的循环
回到最初的问题,为什么网络层总在被重构?
或许所谓的“重构”,从来不是推翻,而是一次次回到原点后的再出发。
在语言不断演进、经验不断累积的过程中,我们反复校准网络层的责任边界,也在反复确认:什么值得被保留,什么应该被交还。
于是循环继续向前——
每一次回望,都让设计变得更简单一些;
每一次重写,都是让设计更贴近当下的语境。
也许我们此刻所做的事情,
与几十年前那些在车库里反复试错的人,并无本质上的不同。