在 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
}
这种方法看起来非常直接和快速,至少当我在模拟器上测试它时 运行是这样。不过,我对此有一些疑问:
- 使用
Data(contentsOf:)
从HTTP
URL下载图片真的合适吗?它可以工作,但也许更适合table 获取您拥有的文件或其他类型的东西,而且它不是网络的最佳解决方案。
- 我想在全局异步队列中执行该操作是正确的做法,对吧?
- 如果图像 URL 已更新并且我需要下载其他图像,是否可以使用此方法取消该图像下载?另外,当单元格从 table 出列时,我应该取消下载还是自动完成?
- 此外,我对此不确定:由于多个单元格将同时显示在 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" 错误。但是这样我就可以处理后台下载了。
关于此方法的问题:
- 如何确保下载的图片显示在相应的单元格中?与前面描述的问题相同:该单元可能在那一刻从 table 中出队。
这个问题与两种方法有关:
- 我不想将图像保存到文件中,但如果单元格再次显示我不想再次下载,而我已经下载了。缓存它们的最佳方式应该是什么?
您永远不应该使用 NSData 的 contentsOfURL 方法来检索非本地数据,原因有两个:
- 文档明确表示不支持这样做。
- 名称解析和检索同步进行,阻塞当前线程。
在你的例子中,在并发队列上做,最后一个并没有那么糟糕,但如果你获取了足够的资源,我怀疑它会导致相当数量的开销。
您的 NSURLSession 方法看起来较慢的一个原因是我 认为您可能同时获取所有资源(非常努力地访问服务器)你的另一种方法。更改 NSURLSession 中的并发限制可能会稍微提高其性能,但只有在您知道服务器可以处理它时才这样做。
回答其他问题:
您可以通过多种方式将图像绑定到特定的单元格,但最常见的方法是使用一个管理所有请求的单例,并且在该单例中:
- 保留一个将数据任务映射到唯一标识符的字典。将其设置为单元格的可访问性标识符,并使用内置查找方法查找具有该可访问性标识符的单元格。如果它不再存在,则丢弃该响应。
- 保留一个将数据任务映射到块的字典。当请求完成时,运行 块。让块保持对单元格的强引用。
在任何一种情况下,将 URL 存储在单元格上的 属性 中,并确保在设置图像之前它与请求的原始 URL 匹配,以便如果单元格得到重用,您不会用过时请求中的错误图像覆盖其现有图像。
- NSURL缓存是你的朋友。创建一个合理大小的磁盘缓存,它应该注意避免不必要的网络请求(假设你的服务器正确支持缓存)。
我有一个 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
}
这种方法看起来非常直接和快速,至少当我在模拟器上测试它时 运行是这样。不过,我对此有一些疑问:
- 使用
Data(contentsOf:)
从HTTP
URL下载图片真的合适吗?它可以工作,但也许更适合table 获取您拥有的文件或其他类型的东西,而且它不是网络的最佳解决方案。 - 我想在全局异步队列中执行该操作是正确的做法,对吧?
- 如果图像 URL 已更新并且我需要下载其他图像,是否可以使用此方法取消该图像下载?另外,当单元格从 table 出列时,我应该取消下载还是自动完成?
- 此外,我对此不确定:由于多个单元格将同时显示在 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" 错误。但是这样我就可以处理后台下载了。
关于此方法的问题:
- 如何确保下载的图片显示在相应的单元格中?与前面描述的问题相同:该单元可能在那一刻从 table 中出队。
这个问题与两种方法有关:
- 我不想将图像保存到文件中,但如果单元格再次显示我不想再次下载,而我已经下载了。缓存它们的最佳方式应该是什么?
您永远不应该使用 NSData 的 contentsOfURL 方法来检索非本地数据,原因有两个:
- 文档明确表示不支持这样做。
- 名称解析和检索同步进行,阻塞当前线程。
在你的例子中,在并发队列上做,最后一个并没有那么糟糕,但如果你获取了足够的资源,我怀疑它会导致相当数量的开销。
您的 NSURLSession 方法看起来较慢的一个原因是我 认为您可能同时获取所有资源(非常努力地访问服务器)你的另一种方法。更改 NSURLSession 中的并发限制可能会稍微提高其性能,但只有在您知道服务器可以处理它时才这样做。
回答其他问题:
您可以通过多种方式将图像绑定到特定的单元格,但最常见的方法是使用一个管理所有请求的单例,并且在该单例中:
- 保留一个将数据任务映射到唯一标识符的字典。将其设置为单元格的可访问性标识符,并使用内置查找方法查找具有该可访问性标识符的单元格。如果它不再存在,则丢弃该响应。
- 保留一个将数据任务映射到块的字典。当请求完成时,运行 块。让块保持对单元格的强引用。
在任何一种情况下,将 URL 存储在单元格上的 属性 中,并确保在设置图像之前它与请求的原始 URL 匹配,以便如果单元格得到重用,您不会用过时请求中的错误图像覆盖其现有图像。
- NSURL缓存是你的朋友。创建一个合理大小的磁盘缓存,它应该注意避免不必要的网络请求(假设你的服务器正确支持缓存)。