【问题标题】:How to enable WKURLSchemeHandler to do work off main thread?如何启用 WKURLSchemeHandler 来完成主线程的工作?
【发布时间】:2021-09-17 00:41:07
【问题描述】:

我正在尝试让 WKURLSchemeHandler 在 WebView 使用自定义 url 方案时提供视频文件。我意识到didReceive(data) 可以被多次调用,所以我想出了如何分块加载我的视频文件并将其发回。

问题是所有这些工作都是在主线程上完成的。我找不到如何在后台线程上成功完成此操作的示例。我能找到的所有 WKURLSchemeHandler 示例,包括 WWDC 演示视频here(接近视频结尾)都非常基础。他们都没有展示如何处理大文件,更不用说如何从主线程中推出工作了。

如果我只是将所有内容都包装在 DispatchQueue.global(qos: .background).async {...} 中,那么我的应用程序崩溃 b/c WebView 会抛出一个非托管异常,错误为 this task has already been stopped

任何人都知道如何成功地做到这一点?

【问题讨论】:

    标签: wkwebview wkurlschemehandler


    【解决方案1】:

    我终于明白了。我无法相信这是多么困难。难怪苹果没有发布任何关于此的样本。这是我的代码:

    // This is based on "Customized Loading in WKWebView" WWDC video (near the end of the
    // video) at https://developer.apple.com/videos/play/wwdc2017/220 and A LOT of trial
    // and error to figure out how to push work to background thread.
    //
    // To better understand how WKURLSchemeTask (and internally WebURLSchemeTask) works
    // you can refer to the source code of WebURLSchemeTask at
    // https://github.com/WebKit/WebKit/blob/main/Source/WebKit/UIProcess/WebURLSchemeTask.cpp
    //
    // Looking at that source code you can see that a call to any of the internals of
    // WebURLSchemeTask (which is made through WKURLSchemeTask) is expected to be on the
    // main thread, as you can see by the ASSERT(RunLoop::isMain()) statements at the
    // beginning of pretty much every function and property getters. I'm not sure why Apple
    // has decided to do these on the main thread since that would result in a blocked UI
    // thread if we need to return large responses/files. At the very least they should have
    // allowed for calls to come back on any thread and internally pass them to the main
    // thread so that developers wouldn't have to write thread-synchronization code over and
    // over every time they want to use WKURLSchemeHandler.
    //
    // The solution to pushing things off main thread is rather cumbersome. We need to call
    // into DispatchQueue.global(qos: .background).async {...} but also manually ensure that
    // everything is synchronized between the main and bg thread. We also manually need to
    // keep track of the stopped tasks b/c a WKURLSchemeTask does not have any properties that
    // we could query to see if it has stopped. If we respond to a WKURLSchemeTask that has
    // stopped then an unmanaged exception is thrown which Swift cannot catch and the entire
    // app will crash.
    public class MyURLSchemeHandler: NSObject, WKURLSchemeHandler {
        private var stoppedTaskURLs: [URLRequest] = []
    
        public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
            let request = urlSchemeTask.request
            guard let requestUrl = request.url else { return }
            
            DispatchQueue.global(qos: .background).async { [weak self] in
                guard let strongSelf = self, requestUrl.scheme == "my-video-url-scheme" else {
                    return
                }
    
                let filePath = requestUrl.absoluteString
                if let fileHandle = FileHandle(forReadingAtPath: filePath) {
                    // video files can be very large in size, so read them in chuncks.
                    let chunkSize = 1024 * 1024 // 1Mb
                    let response = URLResponse(url: requestUrl,
                                               mimeType: "video/mp4",
                                               expectedContentLength: chunkSize,
                                               textEncodingName: nil)
                    strongSelf.postResponse(to: urlSchemeTask, response: response)
                    var data = fileHandle.readData(ofLength: chunkSize) // get the first chunk
                    while (!data.isEmpty && !strongSelf.hasTaskStopped(urlSchemeTask)) {
                        strongSelf.postResponse(to: urlSchemeTask, data: data)
                        data = fileHandle.readData(ofLength: chunkSize) // get the next chunk
                    }
                    fileHandle.closeFile()
                    strongSelf.postFinished(to: urlSchemeTask)
                } else {
                    strongSelf.postFailed(
                        to: urlSchemeTask,
                        error: NSError(domain: "Failed to fetch resource",
                                       code: 0,
                                       userInfo: nil))
                }
                
                // remove the task from the list of stopped tasks (if it is there)
                // since we're done with it anyway
                strongSelf.stoppedTaskURLs = strongSelf.stoppedTaskURLs.filter{$0 != request}
            }
        }
        
        public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
            if (!self.hasTaskStopped(urlSchemeTask)) {
                self.stoppedTaskURLs.append(urlSchemeTask.request)
            }
        }
        
        private func hasTaskStopped(_ urlSchemeTask: WKURLSchemeTask) -> Bool {
            return self.stoppedTaskURLs.contains{$0 == urlSchemeTask.request}
        }
        
        private func postResponse(to urlSchemeTask: WKURLSchemeTask,  response: URLResponse) {
            post(to: urlSchemeTask, action: {urlSchemeTask.didReceive(response)})
        }
        
        private func postResponse(to urlSchemeTask: WKURLSchemeTask,  data: Data) {
            post(to: urlSchemeTask, action: {urlSchemeTask.didReceive(data)})
        }
        
        private func postFinished(to urlSchemeTask: WKURLSchemeTask) {
            post(to: urlSchemeTask, action: {urlSchemeTask.didFinish()})
        }
        
        private func postFailed(to urlSchemeTask: WKURLSchemeTask, error: NSError) {
            post(to: urlSchemeTask, action: {urlSchemeTask.didFailWithError(error)})
        }
        
        private func post(to urlSchemeTask: WKURLSchemeTask, action: @escaping () -> Void) {
            let group = DispatchGroup()
            group.enter()
            DispatchQueue.main.async { [weak self] in
                if (self?.hasTaskStopped(urlSchemeTask) == false) {
                    action()
                }
                group.leave()
            }
            group.wait()
        }
    }
    

    【讨论】:

    • 我遇到了同样的障碍,我尝试了您的解决方案,但它也不起作用。
    • 对我来说很好。什么对你不起作用。你遇到了什么问题?
    • 我注意到 iOS 15 上视频内容的 WKURLSchemeTask 的一些非常奇怪的行为(iOS 按字节范围请求资源,并且之前枚举读取文件块的代码未按预期工作) ,并根据您上面描述的概念实现一些解决方案,但是以并发样式使用 WKURLSchemeTask 的操作会发出异常“此任务已停止”,即使使用您的方法也是如此。
    • 嗯....我以前没有遇到过这种情况。需要注意的一点是,当您在响应中设置expectedContentLength 时,请确保您发回的数据大小相同(即字节数组容量应该相同,但字节数组中的数据可以小于或等于该容量)。我在expectedContentLength != data buffer length 的位置出现逻辑错误,它导致方案处理程序出错并将this task has already been stopped 返回给我。我不确定这对你来说是否是同样的问题,因为我没有看到你的代码。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-09-13
    • 1970-01-01
    • 2013-10-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多