WKURLSchemeHandler任务取消引发NSInternalInconsistencyException崩溃求助
解决WKURLSchemeHandler任务取消时的崩溃问题
在使用WKURLSchemeHandler处理任务取消时,调用webView(_:stop:)并打印“Stopping task ...”后,偶尔会出现try Task.checkCancellation()未抛出异常,或后续代码仍执行的情况,导致调用urlSchemeTask.didReceive、didFinish或didFailWithError时触发崩溃:
Stopping task <WKURLSchemeTaskImpl: 0x7fd445c209c0> *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'This task has already been stopped'
示例实现
import WebKit class AsyncURLSchemeHandler: NSObject, WKURLSchemeHandler { private var pendingTasks = [ObjectIdentifier: TaskItem]() func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { guard let task = pendingTasks.removeValue(forKey: urlSchemeTask.id) else { return } print("Stopping task \(urlSchemeTask)") task.stop() } func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { let task = Task { [weak self] in var request = urlSchemeTask.request // Do some mutation on the request do { try Task.checkCancellation() // Conditionally get a URLSession let session: URLSession // Fire off the request let (data, response) = try await session.data(for: request) await Task.yield() try Task.checkCancellation() // Report back to the scheme task // Either of these !! may crash in this implementation urlSchemeTask.didReceive(response) // !! urlSchemeTask.didReceive(data) // !! urlSchemeTask.didFinish() // !! } catch is CancellationError { // Do not call didFailWithError, didFinish, or didReceive in this case print("Task for WKURLSchemeTask \(urlSchemeTask) has been cancelled") } catch { if !Task.isCancelled { // !! This can crash, too urlSchemeTask.didFailWithError(error) } } self?.pendingTasks.removeValue(forKey: urlSchemeTask.id) } pendingTasks[urlSchemeTask.id] = .init(urlSchemeTask: urlSchemeTask, task: task) } } private extension WKURLSchemeTask { var id: ObjectIdentifier { ObjectIdentifier(self) } } private struct TaskItem { enum Error: Swift.Error { case manualCancellation } let urlSchemeTask: WKURLSchemeTask let task: Task<Void, Never> /// Should be called when urlSchemeTask has been stopped by the system /// Calling anything on the urlSchemeTask afterwards would result in an exception func stop() { task.cancel() } /// Should be called when the urlSchemeTask should be stopped manually func cancel() { task.cancel() urlSchemeTask.didFailWithError(Error.manualCancellation) } }
解决方案
1. 增加任务状态的原子性跟踪
Task.cancel()是异步操作,无法保证Task.checkCancellation()立即生效。给TaskItem添加原子性的状态标记,在调用urlSchemeTask的方法前先检查状态:
private struct TaskItem { enum Error: Swift.Error { case manualCancellation } let urlSchemeTask: WKURLSchemeTask let task: Task<Void, Never> private(set) var isStopped = false private let queue = DispatchQueue(label: "com.example.taskitem.state") func stop() { queue.sync { isStopped = true task.cancel() } } func isTaskActive() -> Bool { queue.sync { !isStopped } } }
2. 同步控制任务操作与状态检查
在异步任务的关键节点,除了调用Task.checkCancellation(),还需检查TaskItem的状态,避免在任务已停止后调用urlSchemeTask的方法:
func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { let taskItem = TaskItem(urlSchemeTask: urlSchemeTask, task: Task { [weak self, weak taskItem] in guard let taskItem = taskItem else { return } var request = urlSchemeTask.request // Do some mutation on the request do { try Task.checkCancellation() guard taskItem.isTaskActive() else { throw CancellationError() } // Conditionally get a URLSession let session: URLSession = URLSession.shared // Fire off the request let (data, response) = try await session.data(for: request) try Task.checkCancellation() guard taskItem.isTaskActive() else { throw CancellationError() } // Report back to the scheme task if taskItem.isTaskActive() { urlSchemeTask.didReceive(response) urlSchemeTask.didReceive(data) urlSchemeTask.didFinish() } } catch is CancellationError { print("Task for WKURLSchemeTask \(urlSchemeTask) has been cancelled") } catch { if taskItem.isTaskActive() && !Task.isCancelled { urlSchemeTask.didFailWithError(error) } } self?.pendingTasks.removeValue(forKey: urlSchemeTask.id) }) pendingTasks[urlSchemeTask.id] = taskItem }
3. 确保任务状态的线程安全
所有涉及TaskItem状态修改和读取的操作,都通过串行队列同步执行,避免并发导致的状态不一致问题。
内容的提问来源于stack exchange,提问作者fxvdh
相关产品推荐
相关产品推荐

