如何实现WKURLSchemeHandler在非主线程执行相关处理任务?
崩溃核心原因
WKURLSchemeTask 协议规定所有回调方法(didReceive(_ response:), didReceive(_ data:), didFinish(), didFailWithError(_ error:))必须在主线程触发,直接把全部逻辑放到后台队列执行,回调也在后台触发时就会出现异常。另外WebView页面跳转、销毁时会主动调用stop()方法终止当前scheme任务,若后台分块逻辑还在运行,继续调用已停止任务的方法就会抛出this task has already been stopped错误。
可行实现方案
1. 自定义专属处理队列
不要使用系统全局后台队列,自行创建串行队列处理文件读取、分块等耗时逻辑,方便控制任务的执行和中断:
private let processQueue = DispatchQueue(label: "com.yourapp.scheme.videoProcess", qos: .userInitiated)
2. 持有运行中任务、增加有效性校验
在自定义WKURLSchemeHandler中维护当前运行的任务列表,每次执行分块逻辑、触发回调前都校验任务是否还在运行,WebView主动停止任务时及时从列表中移除,避免调用已终止任务的方法。
完整核心实现参考:
class VideoSchemeHandler: NSObject, WKURLSchemeHandler { // 存储当前正在运行的scheme任务 private var runningTasks: [URLRequest: WKURLSchemeTask] = [:] // 自定义处理队列处理耗时逻辑 private let processQueue = DispatchQueue(label: "com.yourapp.scheme.videoProcess", qos: .userInitiated) func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { // 先持有当前任务,避免提前释放 runningTasks[urlSchemeTask.request] = urlSchemeTask processQueue.async { [weak self] in guard let self = self else { return } // 前置校验:任务是否还在运行 guard self.runningTasks.keys.contains(urlSchemeTask.request) else { return } // 本地视频文件读取逻辑 guard let videoPath = Bundle.main.path(forResource: "test", ofType: "mp4"), let fileHandle = FileHandle(forReadingAtPath: videoPath) else { DispatchQueue.main.async { guard self.runningTasks.keys.contains(urlSchemeTask.request) else { return } urlSchemeTask.didFailWithError(NSError(domain: NSURLErrorDomain, code: NSURLErrorFileDoesNotExist)) self.runningTasks.removeValue(forKey: urlSchemeTask.request) } return } let fileSize = fileHandle.seekToEndOfFile() // 构造响应头,视频场景需要额外支持Range请求,按需处理206状态码 let response = HTTPURLResponse( url: urlSchemeTask.request.url!, statusCode: 200, httpVersion: "HTTP/1.1", headerFields: [ "Content-Type": "video/mp4", "Content-Length": "\(fileSize)", "Accept-Ranges": "bytes" ] )! // 响应必须在主线程发送 DispatchQueue.main.async { guard self.runningTasks.keys.contains(urlSchemeTask.request) else { return } urlSchemeTask.didReceive(response) } // 分块读取逻辑,单次读取大小可按需调整,示例为1MB let chunkSize = 1024 * 1024 fileHandle.seek(toFileOffset: 0) while true { // 每次读取前都校验任务是否已停止 guard self.runningTasks.keys.contains(urlSchemeTask.request) else { try? fileHandle.close() return } let chunkData = fileHandle.readData(ofLength: chunkSize) guard chunkData.count > 0 else { break } // 分块数据必须在主线程发送 DispatchQueue.main.async { guard self.runningTasks.keys.contains(urlSchemeTask.request) else { return } urlSchemeTask.didReceive(chunkData) } } try? fileHandle.close() // 完成回调必须在主线程发送 DispatchQueue.main.async { guard self.runningTasks.keys.contains(urlSchemeTask.request) else { return } urlSchemeTask.didFinish() self.runningTasks.removeValue(forKey: urlSchemeTask.request) } } } func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { // WebView主动停止任务时直接移除,后台逻辑会自动中断 runningTasks.removeValue(forKey: urlSchemeTask.request) } }
注意事项
- 视频加载场景必须支持Range范围请求,需要解析请求头的
Range字段,返回对应区间的文件数据和206状态码,否则视频拖动进度、预加载功能会异常。 - 处理队列QoS不要使用优先级过低的
.background,视频加载属于用户可感知操作,使用.userInitiated或.utility能避免系统调度优先级太低导致加载卡顿。 - 所有对
WKURLSchemeTask方法的调用必须先校验任务是否还在运行列表中,禁止对已停止的任务执行任何回调操作。
内容的提问来源于stack exchange,提问作者Meisam Seyed Aliroteh
相关产品推荐
相关产品推荐

