You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:27:37