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

如何实现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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.04 00:48:05