NSURLSession的waitsForConnectivity标识在OperationQueue运行DataTask时被忽略
问题描述
我自定义了一个实现Operation协议的类MyURLRequest,内部会创建并配置URLSession,相关代码如下:
public init(shouldWaitForConnectivity: Bool, timeoutForResource: Double?) { baseUrl = URL(string: Self.relevantServerUrl + "api/") self.shouldWaitForConnectivity = shouldWaitForConnectivity self.timeoutForResource = timeoutForResource super.init() localURLSession = URLSession(configuration: localConfig, delegate: self, delegateQueue: nil) } public var localConfig: URLSessionConfiguration { let res = URLSessionConfiguration.default res.allowsCellularAccess = true if let shouldWaitForConnectivity = shouldWaitForConnectivity { res.waitsForConnectivity = shouldWaitForConnectivity if let timeoutForResource = timeoutForResource { res.timeoutIntervalForResource = timeoutForResource } } return res }
后续会创建URLRequest、dataTask,再放到OperationQueue上运行,Operation的相关方法实现如下:
override open func start() { if isCancelled { isFinished = true return } startDate = Date() sessionTask?.resume() localURLSession.finishTasksAndInvalidate() } override open func cancel() { super.cancel() sessionTask?.cancel() }
MyURLRequest同时实现了URLSessionDataDelegate和URLSessionTaskDelegate协议,作为自身创建的URLSession的代理。
当前问题:waitsForConnectivity配置失效,构造方法中将其设为true后运行时被忽略。关闭网络场景下,请求会立即结束返回-1009错误,urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)回调立即触发,func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask)完全不会被调用。
已确认waitsForConnectivity配置正确:在didCompleteWithError回调中查看task对应的配置,该值确实为true。
不通过OperationQueue发起请求时功能完全符合预期,推测问题和OperationQueue的使用有关。
问题更新1
初步判断问题根源是Operation在请求完成前被提前释放,尝试用DispatchGroup做同步,代码如下,没有生效,请求依旧不会等待网络连通:
override open func start() { if isCancelled { isFinished = true return } startDate = Date() dispatchGroup.enter() sessionTask?.resume() dispatchGroup.wait() localURLSession.finishTasksAndInvalidate() }
其中leave()方法在URLSessionDelegate的回调中调用。
问题更新2
didCompleteWithError回调中收到的错误信息如下:
Error Domain=NSURLErrorDomain Code=-1009 "" UserInfo={_kCFStreamErrorCodeKey=50, NSUnderlyingError=0x7fc319112de0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <6388AD46-8497-40DF-8768-44FEBB84A8EC>.<1>, _NSURLErrorRelatedURLSessionTaskErrorKey=( "LocalDataTask <6388AD46-8497-40DF-8768-44FEBB84A8EC>.<1>", "LocalDataTask <26BCBD73-FC8B-4A48-8EA2-1172ABB8093C>.<1>" ), NSLocalizedDescription=., NSErrorFailingURLStringKey=}
根因分析
- 使用了默认同步Operation:默认
Operation是同步执行的,start方法执行完成后会被自动标记为isFinished = true,随即被OperationQueue释放,URLSession的delegate也跟着被销毁,还在等待网络的任务失去代理后直接返回无网络错误。 - 方法调用时机错误:在
start方法刚启动任务就调用了localURLSession.finishTasksAndInvalidate(),虽然该方法会等待现有任务完成,但session代理被提前释放后,等待逻辑会直接终止。 - DispatchGroup用法导致死锁:如果URLSession的代理回调队列和当前被
wait()阻塞的队列是同一个,回调永远不会执行,leave()永远不会被调用;即使队列不同,阻塞Operation执行线程的做法也不符合异步Operation的设计规范。
解决方案
1. 实现异步Operation
自定义Operation要支持异步执行,必须重写状态属性,手动触发KVO通知:
private var _isExecuting: Bool = false override open var isExecuting: Bool { get { _isExecuting } set { willChangeValue(forKey: "isExecuting") _isExecuting = newValue didChangeValue(forKey: "isExecuting") } } private var _isFinished: Bool = false override open var isFinished: Bool { get { _isFinished } set { willChangeValue(forKey: "isFinished") _isFinished = newValue didChangeValue(forKey: "isFinished") } } override open var isAsynchronous: Bool { return true }
2. 调整start方法逻辑
移除start中的finishTasksAndInvalidate调用,仅做任务启动和状态更新:
override open func start() { if isCancelled { isFinished = true return } // 标记Operation开始执行 isExecuting = true startDate = Date() sessionTask?.resume() }
3. 请求结束后更新状态并销毁session
在urlSession(_:task:didCompleteWithError:)回调中统一处理收尾逻辑:
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { // 先处理业务逻辑 defer { // 销毁session localURLSession.finishTasksAndInvalidate() // 标记Operation执行结束 isExecuting = false isFinished = true } // 其余错误处理逻辑 }
4. 调整cancel方法逻辑
取消任务时同步更新Operation状态:
override open func cancel() { super.cancel() sessionTask?.cancel() localURLSession.invalidateAndCancel() isExecuting = false isFinished = true }
修改完成后在无网络场景下测试,taskIsWaitingForConnectivity回调会正常触发,网络恢复后请求会自动执行,不会提前返回-1009错误。
内容的提问来源于stack exchange,提问作者Fedor

