在 UITableViewCell 中执行耗时的任务,在滚动时暂停

Perform time consuming tasks inside UITableViewCell, pausing on scrolling

我有 TableView,其中包含表示事件的海关单元格。它看起来非常接近这里的第一张和第三张图片。

如您所见(抱歉分辨率太低)在一些活动中有朋友将要参加的照片。 不幸的是,关于朋友的信息没有加载关于事件的其他信息。

因此,在我获得活动列表后,我可以请求加载将要参加每个活动的好友列表。

现在我使用像

这样的代码
class EventCell : UITableViewCell {
  var eventForCell : Event? {
      didSet {
          eventTitleLabel.text = eventForCell.title
          eventDateLabel.text = eventForCell.date
          presentFriends(eventID : eventForCell.id)
      }
   }

   func presentFriends(eventID : Int) {
        //searching for friends for event with specific eventID
        .......
        .......

        //for every friend found adding UIImageView in cell
        for friend in friendsOnEvent {
          let avatar = UIImageView(....)
          self.addSubview(avatar)
        }
   }
}

此代码有效,但照片显示不流畅。此外,如果您快速滚动列表,它们就会开始闪烁。如果用户快速滚动事件列表,甚至可能不需要加载它们。所以我有两个问题:

  1. 考虑到为每个事件显示朋友可能需要时间,有时它会在单元格滚动离开后完成,我如何才能获得流畅的滚动体验。
  2. 如果我已经加载了事件列表并且已经显示了带有它们的单元格。在获得有关将要参加的朋友的信息后,我如何更新这些单元格?
  3. 当用户滚动并且我正在创建异步任务以在单元格中显示一些图像时,我认为我应该使用对自身的弱引用并且可能检查它不等于 nil 这样如果单元格现在不可见则任务将被取消。应该怎么做?

更新:

我在 UITableViewPrefetchingDataSource 协议中找到了关于 tableView(_:prefetchRowsAt:) 方法的信息,我应该在这种情况下使用它吗?也许有人对此有一些经验?

1.cellForRowAt 期间(重新)创建视图对象通常是一种不好的做法。从屏幕截图来看,我假设单个单元格上的化身数量是有限制的——我建议在单元格初始值设定项中创建所有 UIImageView 对象,然后在 presentFriends 中将图像设置为它们,并隐藏未使用的 (isHidden = true) 或将它们的 alpha 设置为 0(当然,不要忘记取消隐藏那些已使用的)。

2. 如果您使用 SDWebImage 加载图像,请实施 prepareForReuse 并取消当前下载以在滚动期间获得一点性能提升,并防止未定义的行为(当您尝试设置另一个图像而前一个图像尚未下载时)。基于 this question, this one and this one 我希望是这样的:

    override func prepareForReuse() {
        super.prepareForReuse()
    
        self.imageView.sd_cancelCurrentImageLoad()
    }

 Or you can use [this gist][4] for an inspiration.

P.S.: 此外,您将不得不眨眼数数,因为图像是从网上下载的 - 总会有一些延迟。通过缓存,您可以立即获得已经下载的那些,但是新的会有延迟 - 没有办法阻止这种情况(除非您在呈现 tableView 之前预加载所有可以出现在 tableView 中的图像)。

P.S.2: 你可以尝试使用[UITableViewDataSourcePrefetching][6]实现预取。这可以帮助您解决因从网络下载头像而导致的闪烁。这会使事情变得有点复杂,但如果你真的想消除眨眼,你将不得不亲自动手。

首先,正如您从文档中看到的那样,prefetchRowsAt 不会为您提供单元格对象 - 因此您必须将图像预取到您的模型对象,而不是简单地使用 sd_setImage在给定单元格的 UIImageView 对象上。也许前面提到的 gist 可以帮助您将图像下载到模型中。

现在也如 documentation 所述:

Loading Data Asynchronously

The tableView(_:prefetchRowsAt:) method is not necessarily called for every cell in the table view. Your implementation of tableView(_:cellForRowAt:) must therefore be able to cope with the following potential situations:

Data has been loaded via the prefetch request, and is ready to be displayed.

Data is currently being prefetched, but is not yet available.

Data has not yet been requested.

简单地说,当你在 cellForRowAt 时,你不能依赖 prefetchRowsAt 被调用 - 它可能已经被调用,也可能没有被调用(或者可能正在调用进度)。

文档继续建议使用 Operation 作为下载资源的支持工具 - prefetchRowsAt 可以创建 Operation 负责为给定行下载头像的对象。 cellForRowAt 然后可以向模型询问给定行的 Operation 个对象 - 如果没有任何对象,则意味着没有为该行调用 prefetchRowsAt,您必须创建 Operation 对象。否则,您可以使用 prefetchRowsAt.

创建的操作

无论如何,如果您认为这是值得的,您可以将此 tutorial 作为实施预取的灵感。

您可以像您提到的那样使用 UITableViewDataSourcePrefetching

当某些单元格将要显示但尚未显示在屏幕上时,它会调用您的预取数据源。
这样您就可以在呈现之前准备所有需要时间加载的资源。

您只需实施:

func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath])

并从所有 indexpaths.

中获取与单元格相关的数据

注意它仅在 iOS10.

后可用

我个人使用带有缓存的 AlamofireImage,这样已经下载的图像就不会被提取两次,有很多替代方案,但在这种情况下使用缓存图像是一个很好的做法。