基于闭包回调的异步编程模型在Swift项目中长期占据主导地位,但一旦网络请求之间有依赖关系,或者需要在多个回调之间传递数据,代码的嵌套层级便会迅速加深。阅读这类代码时,开发者需要在不同闭包之间来回跳跃,追踪状态变量的变化,很容易遗漏某个分支的错误处理分支。而Swift Concurrency中的async/await语法允许开发者以同步代码的直观顺序编写异步逻辑,编译器负责处理状态挂起与恢复,让网络层的逻辑表达回归线性思维。

旧有闭包回调网络层的复杂度来源
先看一段典型的闭包回调网络层代码。假设需要请求用户信息,然后根据用户权限再去请求对应的资源列表,最后加载首张资源的图片。在没有引入任何并发框架时,这段逻辑会写成链式的闭包嵌套,每一层闭包捕获前一层的结果,同时还要处理每一层可能产生的错误。如果某个环节忘记调用完成回调,或者错误路径写得不完整,整个请求链条就会无声无息地挂起。
func fetchUserProfile(completion: @escaping (Result<User, Error>) -> Void) {
let url = URL(string: "https://api.ippipp.com/user")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.emptyData))
return
}
do {
let user = try JSONDecoder().decode(User.self, from: data)
completion(.success(user))
} catch {
completion(.failure(error))
}
}.resume()
}
func fetchResourceList(userID: String, completion: @escaping (Result<[Resource], Error>) -> Void) {
let url = URL(string: "https://api.ippipp.com/resources?user=\(userID)")!
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data else {
completion(.failure(NetworkError.emptyData))
return
}
do {
let resources = try JSONDecoder().decode([Resource].self, from: data)
completion(.success(resources))
} catch {
completion(.failure(error))
}
}.resume()
}
func loadFirstImage(from resource: Resource, completion: @escaping (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: resource.imageURL) { data, response, error in
if let error = error {
completion(.failure(error))
return
}
guard let data = data, let image = UIImage(data: data) else {
completion(.failure(NetworkError.invalidImage))
return
}
completion(.success(image))
}.resume()
}
// 调用处层层嵌套
func loadAvatar(completion: @escaping (Result<UIImage, Error>) -> Void) {
fetchUserProfile { result in
switch result {
case .success(let user):
fetchResourceList(userID: user.id) { result in
switch result {
case .success(let resources):
guard let first = resources.first else {
completion(.failure(NetworkError.noResources))
return
}
loadFirstImage(from: first) { result in
completion(result)
}
case .failure(let error):
completion(.failure(error))
}
}
case .failure(let error):
completion(.failure(error))
}
}
}
从上面的示例可以看出,闭包回调版本的三个主要复杂度来源是:嵌套层级深导致缩进越来越夸张;错误处理重复且容易遗漏;每增加一层依赖关系都要手工传递一次completion闭包。这种代码在业务逻辑简单时尚可维护,一旦加入超时控制、请求取消、并发限制等额外能力,复杂度会进一步失控。
更深层的问题是线程安全。闭包回调中的代码默认在URLSession的代理队列上执行,如果需要在主线程刷新UI,开发者必须手动调用DispatchQueue.main.async。随着业务代码增多,线程跳转的逻辑会散布在各个回调中,排查线程相关崩溃时往往需要逐个检查调用栈。
async/await如何简化异步编程
Swift Concurrency引入的async/await语法把异步函数变成了一种普通的函数声明,只是增加一个async关键字来标记该函数可能发生挂起。调用方使用await关键字等待异步操作完成,在这一期间当前任务会被挂起但不阻塞所在线程,底层调度器可以继续执行其他任务。从代码阅读角度来说,async/await版本几乎和同步代码没有区别,开发者可以按照从上到下的顺序理解整个流程。
func fetchUserProfile() async throws -> User {
let url = URL(string: "https://api.ippipp.com/user")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
func fetchResourceList(userID: String) async throws -> [Resource] {
let url = URL(string: "https://api.ippipp.com/resources?user=\(userID)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Resource].self, from: data)
}
func loadFirstImage(from resource: Resource) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(from: resource.imageURL)
guard let image = UIImage(data: data) else {
throw NetworkError.invalidImage
}
return image
}
// 调用处变成顺序结构
func loadAvatar() async throws -> UIImage {
let user = try await fetchUserProfile()
let resources = try await fetchResourceList(userID: user.id)
guard let first = resources.first else {
throw NetworkError.noResources
}
return try await loadFirstImage(from: first)
}
对比两个版本可以直观感受到差异:原来需要多层嵌套的错误处理分支被简化为单层的try await表达式,任意一层抛出的错误都会自动沿着函数调用栈向上传播,不需要手工逐层传递completion。更重要的是,异步函数的调用者和被调用者之间的契约变得清晰起来——函数签名直接告诉调用者这是一个可能抛错的异步操作,返回值是具体的数据类型。
Swift Concurrency还提供了结构化并发(Structured Concurrency)的底层支撑。使用Task来创建子任务,取消操作可以通过检查Task.isCancelled来实现,系统会帮助管理任务的层级关系,避免无意义的资源浪费。这些能力在闭包回调版本中都需要借助第三方库或自行设计状态机才能实现。
重构实战:完整改造旧有网络层
现在把整个网络层进行系统性改造。首先设计一个通用的网络请求协议,把JSON解析和错误处理统一封装起来,避免每个接口重复编写样板代码。
protocol APIClientProtocol {
func send<T: Decodable>(_ request: URLRequest) async throws -> T
}
enum NetworkError: Error {
case invalidResponse
case emptyData
case invalidImage
case serverError(statusCode: Int)
}
struct APIClient: APIClientProtocol {
let session: URLSession
let decoder: JSONDecoder
init(session: URLSession = .shared, decoder: JSONDecoder = JSONDecoder()) {
self.session = session
self.decoder = decoder
}
func send<T: Decodable>(_ request: URLRequest) async throws -> T {
let (data, response) = try await session.data(for: request)
guard let httpResponse = response as? HTTPURLResponse else {
throw NetworkError.invalidResponse
}
guard (200..<300).contains(httpResponse.statusCode) else {
throw NetworkError.serverError(statusCode: httpResponse.statusCode)
}
guard !data.isEmpty else {
throw NetworkError.emptyData
}
return try decoder.decode(T.self, from: data)
}
}
上述封装把URLSession.data(for:)的结果统一做了状态码校验和数据解析,业务层只需要定义请求体和响应模型。比如获取用户信息的API可以这样写:
struct User: Decodable {
let id: String
let name: String
}
extension APIClient {
func fetchUserProfile() async throws -> User {
var request = URLRequest(url: URL(string: "https://api.ippipp.com/user")!)
request.httpMethod = "GET"
request.setValue("application/json", forHTTPHeaderField: "Accept")
return try await send(request)
}
}
如果项目中还残留着大量旧有的闭包回调接口,无法一次性全部替换,可以使用withCheckedThrowingContinuation作为桥接层,把回调风格的API包装成async函数。这种方式特别适合渐进式重构阶段,让新旧代码共存。
extension APIClient {
func legacyFetchUserProfile(completion: @escaping (Result<User, Error>) -> Void) {
// 模拟旧接口的异步回调
DispatchQueue.global().async {
let user = User(id: "123", name: "Alice")
completion(.success(user))
}
}
func fetchUserProfileAsync() async throws -> User {
try await withCheckedThrowingContinuation { continuation in
legacyFetchUserProfile { result in
switch result {
case .success(let user):
continuation.resume(returning: user)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}
}
这种桥接方式要格外注意一个细节:continuation只能被resume一次,如果回调可能在不同条件下被触发多次,需要额外加保护逻辑。另外,如果旧接口永远不会回调,withCheckedThrowingContinuation会在调试模式下触发运行时警告,帮助开发者及时发现遗漏的resume调用。
重构过程中的关键细节与陷阱
线程和主线程隔离是重构中很容易踩坑的地方。async/await本身不保证调用后的代码一定在哪个线程执行。如果自定义的URLSession配置了委托队列,那么数据返回后的续体可能在该委托队列上运行。在需要更新UI的位置,应该使用@MainActor标注对应的视图模型方法,或者在调用处包装一层MainActor.run。对于大部分直接使用URLSession.shared的场景,系统会在后台线程处理网络请求,续体执行线程不固定,所以显式标记主线程隔离仍然是必要的安全措施。
@MainActor
final class ProfileViewModel: ObservableObject {
@Published var avatar: UIImage?
@Published var isLoading = false
private let apiClient: APIClient
init(apiClient: APIClient) {
self.apiClient = apiClient
}
func loadAvatar() async {
isLoading = true
defer { isLoading = false }
do {
let user = try await apiClient.fetchUserProfile()
let resources = try await apiClient.fetchResourceList(userID: user.id)
guard let first = resources.first else { return }
avatar = try await apiClient.loadFirstImage(from: first)
} catch {
// 统一的错误处理逻辑
}
}
}
取消支持也需要在重构时一并考虑。URLSession的data(for:)方法本身支持Task取消,当Task被取消时,网络请求会自动终止并抛出CancellationError。但使用withCheckedThrowingContinuation桥接旧闭包接口时,如果旧回调内部不在取消时主动调用resume,Task会被永久挂起。一种简单做法是在桥接函数中注册取消回调,在取消时抛出一个自定义错误来结束Task。
func bridgeWithCancellation(completion: @escaping (@escaping (Result<User, Error>) -> Void) -> Void) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
var isFinished = false
completion { result in
guard !isFinished else { return }
isFinished = true
switch result {
case .success(let value):
continuation.resume(returning: value)
case .failure(let error):
continuation.resume(throwing: error)
}
}
// 如果Task被取消,继续等待可能会导致资源泄漏
// 可以在任务中使用Task.checkCancellation()定期检查
}
}
最后需要关注的是Swift版本兼容性。如果项目最低支持系统低于iOS 13或macOS 10.15,Swift Concurrency部分语法无法直接使用。通常的做法是把网络层核心代码放在一个Swift 5.5+的文件中,然后通过iOS 13+的系统版本判断来决定调用路径。对于无法升级旧系统支持的项目,可以考虑保留旧闭包接口,或者使用iOS 15提供的async版本API结合availability判断来优雅降级。
总结
使用async/await重构网络层的核心收益在于把异步逻辑的线性顺序还原出来,让代码的阅读者不再需要手工追踪闭包之间的状态传递。错误处理的统一传播机制也消除了重复的switch case分支,开发者能够将更多精力放在业务逻辑的边界条件上。桥接工具withCheckedThrowingContinuation的存在保证了渐进式重构的可行性,网络层的改造不必一步到位,而是可以按照模块逐个替换。对于大型项目而言,这种平滑的迁移路径往往是决定新技术是否能够落地的关键因素。
值得注意的是,async/await并不是银弹。在并发请求数量较高、或者需要精细控制请求优先级时,还是要结合TaskGroup和AsyncStream等更进阶的并发原语来设计。但作为Swift Concurrency的入门第一步,将闭包回调换成async/await已经能显著降低网络层代码的复杂性,值得在项目中尽快实践推进。
Swift Concurrencyasync/await网络层重构修改时间:2026-08-27 15:08:04