WWDC Quick Look 💓 By SwiftGGTeam
Meet AsyncSequence

Meet AsyncSequence

观看原视频

Highlight

AsyncSequence 是 Swift 5.5 引入的核心协议,它是同步 Sequence 的异步等价物。就像 for item in sequence 遍历同步序列一样,for await item in asyncSequence 可以遍历异步产生的元素。

核心内容

async/await 之前,处理一系列异步值需要回调、代理模式或 Combine Publisher。这些方式各有各的复杂性:回调嵌套导致”回调地狱”,代理分散在多个方法中,Combine 的学习曲线又比较陡。

AsyncSequence 用最朴素的 for-await-in 循环解决了这个问题。它让异步序列的处理看起来和同步序列几乎一样。编译器在背后会把 for-await-in 转换成对 makeAsyncIterator() 的调用,然后反复 await iterator.next()

系统框架中大量 API 已经提供了 AsyncSequence 接口。URL.lines 可以逐行异步读取文本文件。URLSessionbytes(from:) 返回 AsyncBytesNotificationCenternotifications(named:) 返回通知序列。FileHandle 也支持了异步读取。

Session 还介绍了 AsyncStreamAsyncThrowingStream,这是把传统的 callback/delegate 模式转换为 AsyncSequence 的桥梁。你可以用 AsyncStream 把 Timer、CLLocationManager 的 delegate 回调包装成 AsyncSequence

详细内容

遍历 AsyncSequence

04:28

for await quake in quakes {
    if quake.magnitude > 3 {
        displaySignificantEarthquake(quake)
    }
}

关键点:

  • for await 是 Swift 5.5 的新语法,用于遍历 AsyncSequence
  • 如果序列可能抛出错误,使用 for try await
  • breakcontinuefor-await-in 中正常工作
for try await quake in quakeDownload {
    if quake.location == nil {
        break
    }
    if quake.magnitude > 3 {
        displaySignificantEarthquake(quake)
    }
}

编译器如何处理异步迭代

03:52

// 同步迭代
var iterator = quakes.makeIterator()
while let quake = iterator.next() {
    if quake.magnitude > 3 {
        displaySignificantEarthquake(quake)
    }
}

// 异步迭代
var iterator = quakes.makeAsyncIterator()
while let quake = await iterator.next() {
    if quake.magnitude > 3 {
        displaySignificantEarthquake(quake)
    }
}

关键点:

  • 编译器把 for-in 转换成 while let + makeIterator() + next()
  • 编译器把 for await-in 转换成 while let await + makeAsyncIterator() + next()
  • 每次迭代都可能挂起,等待下一个值到达

并发迭代

07:15

let iteration1 = Task {
    for await quake in quakes {
        displayEarthquake(quake)
    }
}

let iteration2 = Task {
    do {
        for try await quake in quakeDownload {
            updateMap(with: quake)
        }
    } catch {
        handleError(error)
    }
}

// 后续可以取消
iteration1.cancel()
iteration2.cancel()

关键点:

  • for-await-in 放在 Task 闭包中可以并发执行多个迭代
  • Task 提供了 cancel() 方法,可以从外部取消迭代
  • 对于可能无限运行的序列(如通知流),这种方式可以管理生命周期

从 FileHandle 读取字节

07:56

for try await line in FileHandle.standardInput.bytes.lines {
    processLine(line)
}

关键点:

  • FileHandle.bytes 返回 AsyncSequence<UInt8>
  • .lines 扩展把字节流转换成字符串流,按行分割
  • 适合处理标准输入、管道数据等流式场景

从 URL 读取文件

08:16

let url = URL(fileURLWithPath: "/tmp/somefile.txt")
for try await line in url.lines {
    print(line)
}

关键点:

  • URL.lines 是 Swift 5.5 新增的便利属性
  • 自动处理文件打开、关闭和错误
  • 逐行读取,不会一次性把整个文件加载到内存

从 URLSession 获取字节流

08:49

let (bytes, response) = try await URLSession.shared.bytes(from: url)

guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw MyNetworkingError.invalidServerResponse
}

for try await byte in bytes {
    processByte(byte)
}

关键点:

  • bytes(from:) 返回 (AsyncBytes, URLResponse) 元组
  • AsyncBytesAsyncSequence<UInt8>
  • 逐字节处理,适合流式解析或大文件下载

等待特定通知

09:12

let center = NotificationCenter.default
let notification = await center.notifications(named: .NSPersistentStoreRemoteChange).first {
    $0.userInfo[NSStoreUUIDKey] == storeUUID
}

关键点:

  • notifications(named:) 返回 AsyncSequence<Notification>
  • 结合 first(where:) 可以等待第一个满足条件的通知
  • 这比实现 delegate 回调或添加 observer 更简洁

使用 AsyncStream

11:10

class QuakeMonitor {
    var quakeHandler: (Quake) -> Void
    func startMonitoring()
    func stopMonitoring()
}

let quakes = AsyncStream(Quake.self) { continuation in
    let monitor = QuakeMonitor()
    monitor.quakeHandler = { quake in
        continuation.yield(quake)
    }
    continuation.onTermination = { @Sendable _ in
        monitor.stopMonitoring()
    }
    monitor.startMonitoring()
}

let significantQuakes = quakes.filter { quake in
    quake.magnitude > 3
}

for await quake in significantQuakes {
    displaySignificantEarthquake(quake)
}

关键点:

  • AsyncStream 构造函数接受一个闭包,提供 continuation 用于产生值
  • continuation.yield() 向序列投递一个值
  • continuation.onTermination 在序列被取消时调用,用于清理资源
  • AsyncStream 可以和 filtermap 等操作符组合使用

AsyncStream 处理错误

如果需要抛出错误,使用 AsyncThrowingStream

let events = AsyncThrowingStream(Event.self) { continuation in
    let monitor = EventMonitor()
    monitor.eventHandler = { result in
        switch result {
        case .success(let event):
            continuation.yield(event)
        case .failure(let error):
            continuation.finish(throwing: error)
        }
    }
    monitor.start()
}

关键点:

  • AsyncThrowingStreamcontinuation.finish(throwing:) 用于报告错误
  • 迭代时需要使用 for try await
  • 错误会终止序列,后续 next() 调用返回 nil

核心启发

1. 用 URL.lines 处理大文件

如果你的 App 需要处理大型文本文件(日志、CSV、JSON Lines),不要一次性读入内存。用 url.lines 逐行处理,内存占用恒定。

实现思路:替换 String(contentsOfFile:)for try await line in url.lines。在循环内逐行解析,提取所需信息后丢弃。

2. 用 AsyncStream 包装 Timer 回调

传统的 Timer.scheduledTimer 需要在 target 方法中处理,分散了逻辑。用 AsyncStream 包装后,可以在一个循环中处理所有定时事件。

实现思路:创建 AsyncStream,在 Timer 回调中调用 continuation.yield(())。然后用 for await _ in timerStream 处理定时任务。

3. 用 AsyncStream 包装 NotificationCenter 回调

某些通知需要特定条件触发,传统方式需要先 add observer 再在回调中判断条件。用 notifications(named:)first(where:) 可以直接等待满足条件的通知。

实现思路:替换 addObserver(forName:object:queue:using:)await center.notifications(named:).first { 条件 }。代码更简洁,意图更明确。

4. 用 AsyncStream 包装网络分页加载

网络 API 的分页加载通常需要递归调用下一页。用 AsyncStream 可以把分页序列变成线性迭代。

实现思路:创建 AsyncStream,在异步闭包中加载第一页,通过 continuation.yield 返回每条数据,然后递归加载下一页。调用方只需 for await item in paginatedStream

5. 用 AsyncStream 包装 CoreLocation 回调

CLLocationManager 的 delegate 回调分散在多个方法中。用 AsyncStream 可以统一处理位置更新。

实现思路:为每个需要的事件创建 AsyncStream(如 didUpdateLocationsdidFailWithError),在 delegate 方法中调用对应的 continuation.yield

关联 Session

评论

GitHub Issues · utterances