在 UITableViewCell 中下载图像:数据与 URLSession?

Downloading images in UITableViewCell: Data vs URLSession?

我有一个 UITableView,我需要它的每个单元格从提供的 URL 下载图像并显示它。这看起来是一个很常见的场景,实际上我发现了几个与此相关的帖子和​​问题,但我仍然不清楚应该采用哪种最佳方法。

第一个,我有一个使用 Data(contentsOf:)。这是我 UITableViewCell:

中的代码
class MyCell: UITableViewCell {

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var imageView: UIImageView!

var model: Model? {
    willSet {
        activityIndicator.startAnimating()
        configureImage(showImage: false, showActivity: true)
    }
    didSet {
        guard let imageSmallUrlStr = model?.imageSmallUrl, let imageUrl = URL(string: imageSmallUrlStr) else {
            activityIndicator.stopAnimating()
            configureImage(showImage: false, showActivity: false)
            return
        }

        DispatchQueue.global(qos: .userInitiated).async {
            var image: UIImage?
            let imageData = try? Data(contentsOf: imageUrl)
            if let imageData = imageData {
                image = UIImage(data: imageData)
            }

            DispatchQueue.main.async {
                self.imageView.image = image
                self.configureImage(showImage: true, showActivity: false)
            }
        }
}

override func awakeFromNib() {
    super.awakeFromNib()
    configureImage(showImage: false, showActivity: false)
}

override func prepareForReuse() {
    super.prepareForReuse()
    model = nil
}

// Other methods
}

这种方法看起来非常直接和快速,至少当我在模拟器上测试它时 运行是这样。不过,我对此有一些疑问:

  1. 使用Data(contentsOf:)HTTPURL下载图片真的合适吗?它可以工作,但也许更适合table 获取您拥有的文件或其他类型的东西,而且它不是网络的最佳解决方案。
  2. 我想在全局异步队列中执行该操作是正确的做法,对吧?
  3. 如果图像 URL 已更新并且我需要下载其他图像,是否可以使用此方法取消该图像下载?另外,当单元格从 table 出列时,我应该取消下载还是自动完成?
  4. 此外,我对此不确定:由于多个单元格将同时显示在 table 中,因此这些单元格需要同时下载和显示它们的图像。也可能发生这样的情况,当图像下载完成时,相应的单元格已从 table 中出列,因为用户已滚动。这种方法是否确保多个单元同时下载图像,并且每个下载的图像都正确设置到其对应的单元?

现在,这是我的另一种方法,使用 URLSession:

class MyCell: UITableViewCell {

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
@IBOutlet weak var imageView: UIImageView!

var imageTask: URLSessionDownloadTask?

var model: Model? {
    willSet {
        activityIndicator.startAnimating()
        configureImage(showImage: false, showActivity: true)
    }
    didSet {
        imageTask?.cancel()

        guard let imageSmallUrlStr = model?.imageSmallUrl, let imageUrl = URL(string: imageSmallUrlStr) else {
            activityIndicator.stopAnimating()
            configureImage(showImage: false, showActivity: false)
            return
        }

        imageTask = NetworkManager.sharedInstance.getImageInBackground(imageUrl: imageUrl, completion: { [weak self] (image, error) in
            DispatchQueue.main.async {
                guard error == nil else {
                    self?.activityIndicator.stopAnimating()
                    self?.configureImage(showImage: false, showActivity: false)
                    return
                }

                self?.imageView.image = image
                self?.activityIndicator.stopAnimating()
                self?.configureImage(showImage: true, showActivity: false)
            }
        })
    }
}

// Other methods

}

使用这种方法,我可以手动取消任务,但是当我 运行 模拟器上的应用程序时,它看起来更慢。事实上,我在尝试下载许多图像时遇到了一些 "cancelled" 错误。但是这样我就可以处理后台下载了。

关于此方法的问题:

  1. 如何确保下载的图片显示在相应的单元格中?与前面描述的问题相同:该单元可能在那一刻从 table 中出队。

这个问题与两种方法有关:

  1. 我不想将图像保存到文件中,但如果单元格再次显示我不想再次下载,而我已经下载了。缓存它们的最佳方式应该是什么?

您永远不应该使用 NSData 的 contentsOfURL 方法来检索非本地数据,原因有两个:

  • 文档明确表示不支持这样做。
  • 名称解析和检索同步进行,阻塞当前线程。

在你的例子中,在并发队列上做,最后一个并没有那么糟糕,但如果你获取了足够的资源,我怀疑它会导致相当数量的开销。

您的 NSURLSession 方法看起来较慢的一个原因是我 认为您可能同时获取所有资源(非常努力地访问服务器)你的另一种方法。更改 NSURLSession 中的并发限制可能会稍微提高其性能,但只有在您知道服务器可以处理它时才这样做。

回答其他问题:

  • 您可以通过多种方式将图像绑定到特定的单元格,但最常见的方法是使用一个管理所有请求的单例,并且在该单例中:

    • 保留一个将数据任务映射到唯一标识符的字典。将其设置为单元格的可访问性标识符,并使用内置查找方法查找具有该可访问性标识符的单元格。如果它不再存在,则丢弃该响应。
    • 保留一个将数据任务映射到块的字典。当请求完成时,运行 块。让块保持对单元格的强引用。

    在任何一种情况下,将 URL 存储在单元格上的 属性 中,并确保在设置图像之前它与请求的原始 URL 匹配,以便如果单元格得到重用,您不会用过时请求中的错误图像覆盖其现有图像。

  • NSURL缓存是你的朋友。创建一个合理大小的磁盘缓存,它应该注意避免不必要的网络请求(假设你的服务器正确支持缓存)。