美文网首页iOS DeveloperiOS点点滴滴Texture
AsyncDisplayKit 闪烁问题汇总

AsyncDisplayKit 闪烁问题汇总

作者: MMD_ | 来源:发表于2017-12-25 15:43 被阅读311次

在使用AsyncDisplayKit这个组建时,当你reload的时候你会发现屏幕闪烁,差点闪瞎自己的双眼,下面说下病症及解决方案;

-(1) ASNetworkImageNode reload时的闪烁

当ASCellNode中包含ASNetworkImageNode,则这个cell reload时,ASNetworkImageNode会异步从本地缓存或者网络请求图片,请求到图片后再设置ASNetworkImageNode展示图片,但在异步过程中,ASNetworkImageNode会先展示PlaceholderImage,从PlaceholderImage--->fetched image的展示替换导致闪烁发生,即使整个cell的数据没有任何变化,只是简单的reload,ASNetworkImageNode的图片加载逻辑依然不变,因此仍然会闪烁,这显著区别于UIImageView,因为YYWebImage或者SDWebImage对UIImageView的image设置逻辑是,先同步检查有无内存缓存,有的话直接显示,没有的话再先显示PlaceholderImage,等待加载完成后再显示加载的图片,也即逻辑是memory cached image--->PlaceholderImage--->fetched image的逻辑,刷新当前cell时,如果数据没有变化memory cached image一般都会有,因此不会闪烁。

  • AsyncDisplayKit官方给的修复思路是:
import AsyncDisplayKit
 
let node = ASNetworkImageNode()
node.placeholderColor = UIColor.red
node.placeholderFadeDuration = 3

这样修改后,确实没有闪烁了,但这只是将PlaceholderImage--->fetched image图片替换导致的闪烁拉长到3秒而已,自欺欺人,并没有修复。

最终解决思路
  • 迫不得已之下,当有缓存时,直接用ASImageNode替换ASNetworkImageNode。
  • 使用时将NetworkImageNode当成ASNetworkImageNode使用即可。
import AsyncDisplayKit
 
class NetworkImageNode: ASDisplayNode {
  private var networkImageNode = ASNetworkImageNode.imageNode()
  private var imageNode = ASImageNode()
 
  var placeholderColor: UIColor? {
    didSet {
      networkImageNode.placeholderColor = placeholderColor
    }
  }
 
  var image: UIImage? {
    didSet {
      networkImageNode.image = image
    }
  }
 
  override var placeholderFadeDuration: TimeInterval {
    didSet {
      networkImageNode.placeholderFadeDuration = placeholderFadeDuration
    }
  }
 
  var url: URL? {
    didSet {
      guard let u = url,
        let image = UIImage.cachedImage(with: u) else {
          networkImageNode.url = url
          return
      }
 
      imageNode.image = image
    }
  }
 
  override init() {
    super.init()
    addSubnode(networkImageNode)
    addSubnode(imageNode)
  }
 
  override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    return ASInsetLayoutSpec(insets: .zero,
                             child: networkImageNode.url == nil ? imageNode : networkImageNode)
  }
 
  func addTarget(_ target: Any?, action: Selector, forControlEvents controlEvents: ASControlNodeEvent) {
    networkImageNode.addTarget(target, action: action, forControlEvents: controlEvents)
    imageNode.addTarget(target, action: action, forControlEvents: controlEvents)
  }
}

-(2) reload 单个cell时的闪烁

当reload ASTableNode或者ASCollectionNode的某个indexPath的cell时,也会闪烁。原因和ASNetworkImageNode很像,都是异步惹的祸。当异步计算cell的布局时,cell使用placeholder占位(通常是白图),布局完成时,才用渲染好的内容填充cell,placeholder到渲染好的内容切换引起闪烁。UITableViewCell因为都是同步,不存在占位图的情况,因此也就不会闪。

  • 先看官方的修改方案
func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {
  let cell = ASCellNode()
  ... // 其他代码
  cell.neverShowPlaceholders = true
  return cell
}

这个方案非常有效,因为设置cell.neverShowPlaceholders = true,会让cell从异步状态衰退回同步状态,但当页面布局较为复杂时,滑动时的卡顿掉帧就变的肉眼可见。

  • 这时,可以设置ASTableNode的leadingScreensForBatching减缓卡顿
override func viewDidLoad() {
  super.viewDidLoad()
  tableNode.leadingScreensForBatching = 4
}
  • 一般设置tableNode.leadingScreensForBatching = 4即提前计算四个屏幕的内容时,掉帧就很不明显了,典型的空间换时间。但仍不完美,仍然会掉帧,而我们期望的是一帧不掉,如丝般顺滑。这不难,基于上面不闪的方案,刷点小聪明就能解决。
class ViewController: ASViewController {
  ... // 其他代码
  private var indexPathesToBeReloaded: [IndexPath] = []
 
  func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {
    let cell = ASCellNode()
    ... // 其他代码
 
    cell.neverShowPlaceholders = false
    if indexPathesToBeReloaded.contains(indexPath) {
      let oldCellNode = tableNode.nodeForRow(at: indexPath)
      cell.neverShowPlaceholders = true
      oldCellNode?.neverShowPlaceholders = true
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
        cell.neverShowPlaceholders = false
        if let indexP = self.indexPathesToBeReloaded.index(of: indexPath) {
          self.indexPathesToBeReloaded.remove(at: indexP)
        }
      })
    }
    return cell
  }
 
  func reloadActionHappensHere() {
    ... // 其他代码
 
    let indexPath = ... // 需要roload的indexPath
      indexPathesToBeReloaded.append(indexPath)
    tableNode.reloadRows(at: [indexPath], with: .none)
  }
}

-(3) reloadData时的闪烁

在下拉刷新后,列表经常需要重新刷新,即调用ASTableNode或者ASCollectionNode的reloadData方法,但会闪,而且很明显。有了单个cell reload时闪烁的解决方案后,此类闪烁解决起来,就很简单了。将肉眼可见的cell添加进indexPathesToBeReloaded中即可。

func reloadDataActionHappensHere() {
  ... // 其他代码
  
  let count = tableNode.dataSource?.tableNode?(tableNode, numberOfRowsInSection: 0) ?? 0
  if count > 2 {
    // 将肉眼可见的cell添加进indexPathesToBeReloaded中
    indexPathesToBeReloaded.append(IndexPath(row: 0, section: 0))
    indexPathesToBeReloaded.append(IndexPath(row: 1, section: 0))
    indexPathesToBeReloaded.append(IndexPath(row: 2, section: 0))
  }
  tableNode.reloadData()
1
  ... // 其他代码
}

-(4) insertItems时更改ASCollectionNode的contentOffset引起的闪烁

  • 第一种,通过仿射变换倒置ASCollectionNode,这样下拉加载更多,就变成正常列表的上拉加载更多,也就无需移动contentOffset。ASCollectionNode还特意设置了个属性inverted,方便大家开发。然而这种方案换汤不换药,当收到新消息,同时正在查看历史消息,依然需要插入新消息并复原contentOffset,闪烁依然在其他情形下发生。
  • 第二种,集成一个UICollectionViewFlowLayout,重写prepare()方法,做相应处理即可。这个方案完美,简介优雅。子类化的CollectionFlowLayout如下:
class CollectionFlowLayout: UICollectionViewFlowLayout {
  var isInsertingToTop = false
  override func prepare() {
    super.prepare()
    guard let collectionView = collectionView else {
      return
    }
    if !isInsertingToTop {
      return
    }
    let oldSize = collectionView.contentSize
    let newSize = collectionViewContentSize
    let contentOffsetY = collectionView.contentOffset.y + newSize.height - oldSize.height
    collectionView.setContentOffset(CGPoint(x: collectionView.contentOffset.x, y: contentOffsetY), animated: false)
  }
}

当需要insertItems并且保持位置时,将CollectionFlowLayout的isInsertingToTop设置为true即可,完成后再设置为false。如下,

class MessagesViewController: ASViewController {
  ... // 其他代码
  var collectionNode: ASCollectionNode!
  var flowLayout: CollectionFlowLayout!
  override func viewDidLoad() {
    super.viewDidLoad()
    flowLayout = CollectionFlowLayout()
    collectionNode = ASCollectionNode(collectionViewLayout: flowLayout)
    ... // 其他代码
  }
 
  ... // 其他代码
  func insertMessagesToTop(indexPathes: [IndexPath]) {
    flowLayout.isInsertingToTop = true
    collectionNode.performBatch(animated: false, updates: {
      self.collectionNode.insertItems(at: indexPaths)
    }) { (finished) in
      self.flowLayout.isInsertingToTop = false
    }
  }
 
  ... // 其他代码
}

相关文章

网友评论

    本文标题:AsyncDisplayKit 闪烁问题汇总

    本文链接:https://www.haomeiwen.com/subject/yeplgxtx.html