在使用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
}
}
... // 其他代码
}
网友评论