使用URLSession.dataTaskPublisher请求数据时频繁出现cancel日志且请求偶发成功的问题求助
从你的日志和代码来看,核心问题是订阅的生命周期被意外终止——也就是AnyCancellable(即你的sub变量)被提前释放,导致URLSession的请求被取消,所以你看到了receive cancel的日志。偶尔成功的情况,是因为网络请求速度足够快,在订阅被取消前就完成了数据返回。
具体原因拆解
你代码里的sub是Networker类的存储属性,看起来是被持有,但如果你的DataSource实例是局部变量(比如在某个方法里临时创建),那么当方法执行结束后,DataSource会被销毁,它持有的Networker也会被销毁,sub作为Networker的属性自然也会被释放。而AnyCancellable的特性就是:当它被释放时,会自动调用cancel()取消订阅,这就导致了请求被中途取消,出现receive cancel日志。
另外,你的代码里还有一个小bug:在Networker.fetchUrl方法里,你用guard let url = URL(string: "https://apple.com") else { return }直接覆盖了传入的url参数,这会导致不管你在DataSource.fetch里传入什么URL,实际请求的都是苹果官网,不过这不是导致请求取消的原因,但建议修正。
解决方案
1. 确保持有DataSource/Networker的实例
如果是在SwiftUI中使用,需要用@StateObject(用于初始化视图的对象)或@ObservedObject(用于父视图传递的对象)来持有DataSource,确保它不会被提前销毁:
struct ContentView: View { // 用@StateObject强持有DataSource实例 @StateObject private var dataSource = DataSource() var body: some View { Button("Fetch Data") { dataSource.fetch() } } }
如果是在UIKit中,需要把DataSource作为ViewController的属性持有:
class ViewController: UIViewController { private let dataSource = DataSource() override func viewDidLoad() { super.viewDidLoad() dataSource.fetch() } }
2. 修正URL覆盖的bug
把Networker.fetchUrl里的多余URL创建代码删掉,直接使用传入的参数:
func fetchUrl(url: URL) { // 移除这行覆盖参数的代码 // guard let url = URL(string: "https://apple.com") else { return } pub = URLSession.shared.dataTaskPublisher(for: url) .receive(on: DispatchQueue.main) .tryMap() { data, response in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) } return data } .print("Test2") .eraseToAnyPublisher() sub = pub?.sink( receiveCompletion: { completion in switch completion { case .finished: print("请求完成") case .failure(let error): // 建议不要用fatalError,改用日志输出,避免程序崩溃 print("请求失败:\(error.localizedDescription)") } }, receiveValue: { print("收到数据:\($0)") } ) }
3. 优化错误处理
你当前在receiveCompletion的failure分支用了fatalError,这会导致程序直接崩溃,建议换成日志输出,这样即使请求失败,程序也能继续运行,方便你排查问题。
额外建议
- 可以考虑把
sub的类型改为AnyCancellable?而不是Cancellable?,这样更明确,也避免潜在的类型问题。 - 如果需要管理多个订阅,可以用
Set<AnyCancellable>来存储,这样更方便统一管理:
class Networker: NSObject, ObservableObject { private var cancellables = Set<AnyCancellable>() // 移除单独的sub变量 func fetchUrl(url: URL) { URLSession.shared.dataTaskPublisher(for: url) .receive(on: DispatchQueue.main) .tryMap() { data, response in guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw URLError(.badServerResponse) } return data } .print("Test2") .sink( receiveCompletion: { completion in switch completion { case .finished: print("请求完成") case .failure(let error): print("请求失败:\(error.localizedDescription)") } }, receiveValue: { print("收到数据:\($0)") } ) .store(in: &cancellables) // 把订阅存入Set,自动管理生命周期 } }
这样就不需要单独维护sub变量,Set会自动持有所有订阅,直到Networker实例被销毁。
内容的提问来源于stack exchange,提问作者mixiu

