Swift Combine:CurrentValueSubject发送值与完成事件异常排查
问题根源分析
你的问题出在两个核心点上:
- 复用已终止的
CurrentValueSubject实例:cacheSubject是SSNetworkManager的成员变量,当你第一次处理缓存请求时调用cacheSubject.send(completion: .finished),这个Subject就会进入终止状态。后续所有基于它创建的Publisher,只会直接发送完成事件,再也无法传递任何数据。 - 事件发送与订阅的时机不匹配:你先把
cancelable赋值为cacheSubject.eraseToAnyPublisher(),之后才发送数据和完成事件。如果调用方的订阅操作在这之后执行,即便CurrentValueSubject保留了最新值,只要Subject已经终止,订阅者也只能收到完成通知。
修复方案
我们需要为每个缓存请求创建独立的Subject实例,彻底避免复用已终止的Subject。推荐使用PassthroughSubject(缓存场景不需要初始值,它更贴合需求),以下是修改后的完整代码:
public class SSNetworkManager { public static let shared = SSNetworkManager() private var cache: [String: CachedData] = [:] // 移除全局的cacheSubject,改为按需创建独立实例 @discardableResult public func makeServiceCall<D: Decodable>(forRequest request: SSNetworkRequest<D>, onMainThread: Bool = true) -> AnyPublisher<D, Error>? { guard let urlRequest = request.urlRequest else { return nil } var cancelable: AnyPublisher<Data, Error> if let url = urlRequest.url?.absoluteString, let cachedData = cache[url], cachedData.isValid { // 为当前请求创建专属的PassthroughSubject let cacheSubject = PassthroughSubject<Data, Error>() cancelable = cacheSubject.eraseToAnyPublisher() // 依次发送缓存数据和完成事件 cacheSubject.send(cachedData.data) cacheSubject.send(completion: .finished) } else { cancelable = URLSession.shared.dataTaskPublisher(for: urlRequest) .tryMap {[weak self] (data, response) -> Data in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw SSNetworkError(httpCode: (response as? HTTPURLResponse)?.statusCode ?? 0, data: data) } if request.shouldCacheNow, let url = urlRequest.url?.absoluteString { self?.cache[url] = CachedData(data: data, expirationTime: request.cacheExpirationTime) } return data } .eraseToAnyPublisher() } // 统一处理解码和线程切换逻辑,提升可读性 let decodedPublisher = cancelable .decode(type: D.self, decoder: JSONDecoder()) .eraseToAnyPublisher() return onMainThread ? decodedPublisher.receive(on: RunLoop.main).eraseToAnyPublisher() : decodedPublisher } } fileprivate struct CachedData { let data: Data let expirationTime: Date var isValid: Bool { return Date().compare(expirationTime) != .orderedDescending } }
关键改动说明
- 取消全局Subject复用:每个缓存请求都创建新的
PassthroughSubject,确保每个请求的事件流完全独立,不会被之前的请求终止操作影响。 - 替换为
PassthroughSubject:缓存场景下我们有明确的待发送数据,不需要CurrentValueSubject的初始值特性,用PassthroughSubject更简洁,也避免了初始空Data的干扰。 - 优化代码结构:把解码和线程切换逻辑统一提取,让核心的缓存/网络请求逻辑更清晰。
这样修改后,调用方就能同时收到缓存数据和完成通知,且所有请求的事件流不会互相干扰。
内容的提问来源于stack exchange,提问作者Son Nguyen
相关产品推荐
相关产品推荐

