AsyncDisplayKit 閃爍問(wèn)題匯總

在使用AsyncDisplayKit這個(gè)組建時(shí),當(dāng)你reload的時(shí)候你會(huì)發(fā)現(xiàn)屏幕閃爍,差點(diǎn)閃瞎自己的雙眼,下面說(shuō)下病癥及解決方案;

-(1) ASNetworkImageNode reload時(shí)的閃爍

當(dāng)ASCellNode中包含ASNetworkImageNode,則這個(gè)cell reload時(shí),ASNetworkImageNode會(huì)異步從本地緩存或者網(wǎng)絡(luò)請(qǐng)求圖片,請(qǐng)求到圖片后再設(shè)置ASNetworkImageNode展示圖片,但在異步過(guò)程中,ASNetworkImageNode會(huì)先展示PlaceholderImage,從PlaceholderImage--->fetched image的展示替換導(dǎo)致閃爍發(fā)生,即使整個(gè)cell的數(shù)據(jù)沒(méi)有任何變化,只是簡(jiǎn)單的reload,ASNetworkImageNode的圖片加載邏輯依然不變,因此仍然會(huì)閃爍,這顯著區(qū)別于UIImageView,因?yàn)閅YWebImage或者SDWebImage對(duì)UIImageView的image設(shè)置邏輯是,先同步檢查有無(wú)內(nèi)存緩存,有的話直接顯示,沒(méi)有的話再先顯示PlaceholderImage,等待加載完成后再顯示加載的圖片,也即邏輯是memory cached image--->PlaceholderImage--->fetched image的邏輯,刷新當(dāng)前cell時(shí),如果數(shù)據(jù)沒(méi)有變化memory cached image一般都會(huì)有,因此不會(huì)閃爍。

  • AsyncDisplayKit官方給的修復(fù)思路是:
import AsyncDisplayKit
 
let node = ASNetworkImageNode()
node.placeholderColor = UIColor.red
node.placeholderFadeDuration = 3

這樣修改后,確實(shí)沒(méi)有閃爍了,但這只是將PlaceholderImage--->fetched image圖片替換導(dǎo)致的閃爍拉長(zhǎng)到3秒而已,自欺欺人,并沒(méi)有修復(fù)。

最終解決思路
  • 迫不得已之下,當(dāng)有緩存時(shí),直接用ASImageNode替換ASNetworkImageNode。
  • 使用時(shí)將NetworkImageNode當(dāng)成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 單個(gè)cell時(shí)的閃爍

當(dāng)reload ASTableNode或者ASCollectionNode的某個(gè)indexPath的cell時(shí),也會(huì)閃爍。原因和ASNetworkImageNode很像,都是異步惹的禍。當(dāng)異步計(jì)算cell的布局時(shí),cell使用placeholder占位(通常是白圖),布局完成時(shí),才用渲染好的內(nèi)容填充cell,placeholder到渲染好的內(nèi)容切換引起閃爍。UITableViewCell因?yàn)槎际峭?,不存在占位圖的情況,因此也就不會(huì)閃。

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

這個(gè)方案非常有效,因?yàn)樵O(shè)置cell.neverShowPlaceholders = true,會(huì)讓cell從異步狀態(tài)衰退回同步狀態(tài),但當(dāng)頁(yè)面布局較為復(fù)雜時(shí),滑動(dòng)時(shí)的卡頓掉幀就變的肉眼可見(jiàn)。

  • 這時(shí),可以設(shè)置ASTableNode的leadingScreensForBatching減緩卡頓
override func viewDidLoad() {
  super.viewDidLoad()
  tableNode.leadingScreensForBatching = 4
}
  • 一般設(shè)置tableNode.leadingScreensForBatching = 4即提前計(jì)算四個(gè)屏幕的內(nèi)容時(shí),掉幀就很不明顯了,典型的空間換時(shí)間。但仍不完美,仍然會(huì)掉幀,而我們期望的是一幀不掉,如絲般順滑。這不難,基于上面不閃的方案,刷點(diǎn)小聰明就能解決。
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時(shí)的閃爍

在下拉刷新后,列表經(jīng)常需要重新刷新,即調(diào)用ASTableNode或者ASCollectionNode的reloadData方法,但會(huì)閃,而且很明顯。有了單個(gè)cell reload時(shí)閃爍的解決方案后,此類(lèi)閃爍解決起來(lái),就很簡(jiǎn)單了。將肉眼可見(jiàn)的cell添加進(jìn)indexPathesToBeReloaded中即可。

func reloadDataActionHappensHere() {
  ... // 其他代碼
  
  let count = tableNode.dataSource?.tableNode?(tableNode, numberOfRowsInSection: 0) ?? 0
  if count > 2 {
    // 將肉眼可見(jiàn)的cell添加進(jìn)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時(shí)更改ASCollectionNode的contentOffset引起的閃爍

  • 第一種,通過(guò)仿射變換倒置ASCollectionNode,這樣下拉加載更多,就變成正常列表的上拉加載更多,也就無(wú)需移動(dòng)contentOffset。ASCollectionNode還特意設(shè)置了個(gè)屬性inverted,方便大家開(kāi)發(fā)。然而這種方案換湯不換藥,當(dāng)收到新消息,同時(shí)正在查看歷史消息,依然需要插入新消息并復(fù)原contentOffset,閃爍依然在其他情形下發(fā)生。
  • 第二種,集成一個(gè)UICollectionViewFlowLayout,重寫(xiě)prepare()方法,做相應(yīng)處理即可。這個(gè)方案完美,簡(jiǎn)介優(yōu)雅。子類(lèi)化的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)
  }
}

當(dāng)需要insertItems并且保持位置時(shí),將CollectionFlowLayout的isInsertingToTop設(shè)置為true即可,完成后再設(shè)置為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
    }
  }
 
  ... // 其他代碼
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容