首页 > 解决方案 > 缓存 Firebase 视频网址时,URLSession 移动缓慢

问题描述

我最初问了这个问题,得到了答案,并在@LeoDabus 的评论中说:

NSData(contentsOf: url) 这并不意味着与非本地资源 url 一起使用

他建议我使用URLSession我做过的,但反应很慢。我想知道我做错了什么。如果这有什么不同的话,视频是 2mb。

在会话的完成处理程序中,我尝试更新主队列上的返回数据,但在执行此操作时出现滚动故障。使用DispatchQueue.global().async没有滚动故障,但似乎需要更长的返回时间

// all of this occurs inside my data model

var cachedURL: URL?

let videoUrl = dict["videoUrl"] as? String ?? "" // eg. "https://firebasestorage.googleapis.com/v0/b/myApp.appspot.com/o/abcd%277920FHqFBkl7D6j%2F-MC65EFG_qT0KZbdtFhU%2F48127-8C29-4666-96C9-E95BE178B268.mp4?alt=media&token=bf85dcd1-8cee-428e-87bc-91800b7316de"
guard let url = URL(string: videoUrl) else { return }

useURLSessionToCacheVideo(url)


func useURLSessionToCacheVideo(_ url: URL) {
    
    let lastPathComponent = url.lastPathComponent
    let cachesDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
    let file = cachesDir.appendingPathComponent(lastPathComponent)
    
    if FileManager.default.fileExists(atPath: file.path) {

        self.cachedURL = file
        print("url already exists in cache")
        return
    }
    
    URLSession.shared.dataTask(with: url, completionHandler: { (data, response, error) in
        
        if let error = error { return }
        
        if let response = response as? HTTPURLResponse {
            guard response.statusCode == 200 else {
                return
            }
        }
        
        guard let data = data else {
            return
        }
        
        DispatchQueue.global().async { // main queue caused a hiccup while scrolling a cv
            do {
                try data.write(to: file, options: .atomic)
                DispatchQueue.main.async { [weak self] in
                    self?.cachedURL = file
                }
            } catch {
                print("couldn't cache video file")
            }
        }
        
    }).resume()
}

标签: iosswiftnsurlsession

解决方案


您应该从会话的后台线程写入文件:

func useURLSessionToCacheVideo(_ url: URL) {
    let lastPathComponent = url.lastPathComponent

    let fileURL = try! FileManager.default
        .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent(lastPathComponent)

    if FileManager.default.fileExists(atPath: fileURL.path) {
        self.cachedURL = fileURL
        print("url already exists in cache")
        return
    }

    URLSession.shared.dataTask(with: url) { data, response, error in
        guard
            error == nil,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode,
            let data = data
        else {
            return
        }

        do {
            try data.write(to: fileURL, options: .atomic)
            DispatchQueue.main.async { [weak self] in
                self?.cachedURL = fileURL
            }
        } catch {
            print("couldn't cache video file")
        }
    }.resume()
}

这也接受任何 2xx HTTP 响应代码。


话虽如此,我建议使用下载任务,它可以减少峰值内存使用量并将数据写入文件中:

func useURLSessionToCacheVideo(_ url: URL) {
    let lastPathComponent = url.lastPathComponent

    let fileURL = try! FileManager.default
        .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent(lastPathComponent)

    if FileManager.default.fileExists(atPath: fileURL.path) {
        self.cachedURL = fileURL
        print("url already exists in cache")
        return
    }

    URLSession.shared.downloadTask(with: url) { location, response, error in
        guard
            error == nil,
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode,
            let location = location
        else {
            return
        }

        do {
            try FileManager.default.moveItem(at: location, to: fileURL)
            DispatchQueue.main.async { [weak self] in
                self?.cachedURL = fileURL
            }
        } catch {
            print("couldn't cache video file")
        }
    }.resume()
}

就个人而言,我不会让这个例程cachedURL自行更新,而是使用完成处理程序模式:

enum CacheError: Error {
    case failure(URL?, URLResponse?)
}

func useURLSessionToCacheVideo(_ url: URL, completion: @escaping (Result<URL, Error>) -> Void) {
    let lastPathComponent = url.lastPathComponent

    let fileURL = try! FileManager.default
        .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent(lastPathComponent)

    if FileManager.default.fileExists(atPath: fileURL.path) {
        completion(.success(fileURL))
        return
    }

    URLSession.shared.downloadTask(with: url) { location, response, error in
        if let error = error {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
            return
        }

        guard
            let httpResponse = response as? HTTPURLResponse,
            200 ..< 300 ~= httpResponse.statusCode,
            let temporaryLocation = location
        else {
            DispatchQueue.main.async {
                completion(.failure(CacheError.failure(location, response)))
            }
            return
        }

        do {
            try FileManager.default.moveItem(at: temporaryLocation, to: fileURL)
            DispatchQueue.main.async {
                completion(.success(fileURL))
            }
        } catch {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }.resume()
}

并这样称呼它:

useURLSessionToCacheVideo(url) { result in
    switch result {
    case .failure(let error):
        print(error)

    case .success(let cachedURL):
        self.cachedURL = cachedURL
    }
}

这样,调用者负责更新cachedURL,它现在知道何时完成(以防您想更新 UI 以反映下载的成功或失败),并且您的网络层不会与模型结构纠缠在一起呼叫者。


推荐阅读