Swift中释放订阅引用为何无法立即取消订阅?
我想了解为何在Swift中释放订阅的引用有时无法取消订阅,为此构建了一个示例:在Adder这个StateObject中定义Combine管道实现两数求和,视图通过TextField将有效值传入Adder的termA和termB发布变量。点击“Compute Result”按钮时,订阅管道输出到sink,显示结果后将订阅引用设为nil以取消订阅,但订阅仍会响应一次后续的输入变化。我并非询问如何修改实现方式,只是想知道此场景下该问题的原因,怀疑可能与弱引用或订阅释放时机有关。
示例代码如下:
import Combine import SwiftUI // Simple Addition Pipeline private class Adder: ObservableObject { @Published var termA: Int = 0 @Published var termB: Int = 0 @Published var result: Int = 0 init() { $termA .combineLatest($termB) .map { $0.0 + $0.1 } .assign(to: &$result) } } struct SingleUsePipeline: View { @StateObject private var adder = Adder() @State private var termA = "0" @State private var termB = "0" @State private var result = "" @State private var adderConnection: AnyCancellable? var body: some View { VStack { HStack { TextField("Term A", text: $termA) .textFieldStyle(.roundedBorder) .onChange(of: termA) { val in let a = Int(val) if let a { adder.termA = a } } Text("+") TextField("Term B", text: $termB) .textFieldStyle(.roundedBorder) .onChange(of: termB) { val in let b = Int(val) if let b { adder.termB = b } } } Button("Compute Result") { adderConnection = adder.$result .sink { total in result = "\(total)" adderConnection = nil } } Text(result) } .padding() } }
原因分析
这个现象的核心原因是Combine订阅的取消时机与@Published的发送逻辑存在异步调度延迟,具体拆解为两点:
sink闭包内取消订阅的时机滞后
当你在sink闭包中设置adderConnection = nil时,订阅的取消操作并非立即生效。@Published的事件发送是在当前RunLoop周期内完成的,而订阅的取消需要等到当前事件处理完成后,才会真正从发布者的订阅集合中移除。@Published的“值缓存”特性@Published会缓存当前的最新值,新订阅创建时会立即发送一次缓存值。当你修改termA/termB触发result更新时,如果之前的订阅取消操作还未完全完成(比如还没从发布者的订阅列表中移除),这次result的更新事件就会被已经标记要取消但尚未完成取消的订阅捕获,导致额外的一次响应。
更具体的执行流程:
- 点击按钮创建订阅时,
adder.$result立即发送当前缓存的result值,触发sink闭包,你在闭包内将adderConnection设为nil,标记订阅要取消。 - 但订阅的取消逻辑是异步执行的(Combine的Cancellable取消是线程安全的,但并非同步阻塞)。
- 后续修改TextField的值触发
termA/termB更新,进而触发result更新时,若订阅取消操作还未完成,这次更新事件就会被尚未完全取消的订阅捕获,导致多一次响应。
另外,@StateObject的生命周期由SwiftUI稳定管理,Adder实例不存在弱引用问题——你的代码中通过AnyCancellable强引用持有订阅、置nil取消的逻辑是正确的,问题根源在Combine内部的事件调度和订阅取消的异步性上。
内容的提问来源于stack exchange,提问作者Curious Jorge

