在使用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
}
}
... // 其他代碼
}