Combine Swift中Mock DataService Publisher出现EXC_BAD_ACCESS崩溃求助
问题根源分析
崩溃的核心原因是Mock的Publisher是同步立即发送值的,而真实API的Publisher是异步的,两者执行时序的差异导致了悬垂引用:
- ViewModel初始化过程中,访问
lazy var nasaPublisher时才会创建完整的Publisher链; $dataString的初始值会立即触发flatMap逻辑,此时ViewModel还未完全完成初始化;- 你在
flatMap中使用了[unowned self],当Mock的Just同步发送值时,unowned self可能指向未完全初始化或已失效的内存地址,从而触发EXC_BAD_ACCESS。
真实API的dataTaskPublisher是异步执行的,等它发送值时ViewModel已经完成初始化,因此不会出现问题。
解决方案
方案1:替换unowned self为weak self(推荐)
unowned是无主引用,当引用对象不存在时会直接崩溃;而weak是弱引用,会自动变为nil,安全性更高。修改flatMap的捕获列表:
private lazy var nasaPublisher: AnyPublisher<String, Never> = { $dataString .flatMap { [weak self] string in guard let self = self else { return Empty().eraseToAnyPublisher() } return self.dataService.getNasaData() } .eraseToAnyPublisher() }()
方案2:让Mock Publisher模拟异步行为
如果需要保留unowned self,可以让Mock的Publisher延迟发送值,模拟真实API的异步特性,避免在初始化过程中同步访问self:
class MockDataService: DataServiceProtocol { func getNasaData() -> AnyPublisher<String, Never> { return Just("Test string") .delay(for: 0.01, scheduler: DispatchQueue.main) // 轻微延迟确保ViewModel初始化完成 .eraseToAnyPublisher() } }
方案3:调整初始化逻辑
避免在ViewModel的init中直接执行assign,将订阅逻辑移到外部(比如View的onAppear方法中),确保ViewModel完全初始化后再启动Publisher链:
// ViewModel修改:移除init中的assign,添加订阅方法 class ViewModel: ObservableObject { @Published var dataString: String = "" let dataService: DataServiceProtocol init(dataService: DataServiceProtocol) { self.dataService = dataService } func subscribe() { nasaPublisher.assign(to: &$dataString) } private lazy var nasaPublisher: AnyPublisher<String, Never> = { $dataString .flatMap { [unowned self] string in self.dataService.getNasaData() } .eraseToAnyPublisher() }() } // 在View中调用订阅 .onAppear { viewModel.subscribe() }
额外注意点
- 你的
DataService中map块存在未闭合的do语句(注释掉了catch分支),虽然不影响当前崩溃,但建议修复语法问题; - 使用
assign(to:)时需注意内存泄漏风险,虽然@Published的assign会自动管理订阅生命周期,但结合lazy属性时仍需确保引用关系安全。
内容的提问来源于stack exchange,提问作者Francojamesfan
相关产品推荐
相关产品推荐

