美文网首页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