歡迎關(guān)注我的微博以便交流:輕墨
一個(gè)第三方庫(kù)能做到像新產(chǎn)品一樣,值得大家去寫(xiě)寫(xiě)使用體會(huì)的,并不多見(jiàn),AsyncDisplayKit卻完全可以,因?yàn)?code>AsyncDisplayKit不僅僅是一個(gè)工具,它更像一個(gè)系統(tǒng)UI框架,改變整個(gè)編碼體驗(yàn)。也正是這種極強(qiáng)的侵入性,導(dǎo)致不少聽(tīng)過(guò)、star過(guò),甚至下過(guò)demo跑過(guò)AsyncDisplayKit的你我,望而卻步,駐足觀望。但列表界面稍微復(fù)雜時(shí),煩人的高度計(jì)算,因?yàn)樾阅懿坏貌环艞?code>Autolayout而選擇上古時(shí)代的frame layout,令人精疲力盡,這時(shí)AsyncDisplayKit總會(huì)不自然浮現(xiàn)眼前,讓你躍躍欲試。
去年10月份,我們?nèi)肟恿恕?/p>
當(dāng)時(shí)還只是拿簡(jiǎn)單的列表頁(yè)試水,基本上手后,去年底在稍微空閑的時(shí)候用AsyncDisplayKit重構(gòu)了帖子詳情,今年三月份,又借著公司聊天增加群聊的契機(jī),用AsyncDisplayKit重構(gòu)整個(gè)聊天。林林總總,從簡(jiǎn)單到復(fù)雜,踩過(guò)的坑大大小小,將近一年的時(shí)光轉(zhuǎn)眼飛逝,可以寫(xiě)寫(xiě)總結(jié)了。
學(xué)習(xí)曲線
先說(shuō)說(shuō)學(xué)習(xí)曲線,這是大家都比較關(guān)心的問(wèn)題。
跟大多人一樣,一開(kāi)始我以為AsyncDisplayKit會(huì)像Rxswift等MVVM框架一樣,有著陡峭的學(xué)習(xí)曲線。但事實(shí)上,AsyncDisplayKit的學(xué)習(xí)曲線還算平滑。
主要是因?yàn)?code>AsyncDisplayKit只是對(duì)UIKit的再一次封裝,基本沿用了UIKit的API設(shè)計(jì),大部分情況下,只是將view改成node,UI前綴改為AS,寫(xiě)著寫(xiě)著,恍惚間,你以為自己還是在寫(xiě)UIKit呢。
比如ASDisplayNode與UIView:
let nodeA = ASDisplayNode()
let nodeB = ASDisplayNode()
let nodeC = ASDisplayNode()
nodeA.addSubnode(nodeB)
nodeA.addSubnode(nodeC)
nodeA.backgroundColor = .red
nodeA.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
nodeC.removeFromSupernode()
let viewA = UIView()
let viewB = UIView()
let viewC = UIView()
viewA.addSubview(viewB)
viewA.addSubview(viewC)
viewA.backgroundColor = .red
viewA.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
viewC.removeFromSuperview()
相信你看兩眼也就摸出門(mén)道了,大部分API一模一樣。
真正發(fā)生翻天覆地變化的是布局方式,AsyncDisplayKit用的是flexbox布局,UIView使用的是Autolayout。用AsyncDisplayKit的flexbox布局替代Autolayout布局,完全不亞于用Autolayout替換frame布局的蛻變,需要比較大的觀念轉(zhuǎn)變。
但flexbox布局被提出已久,且其本身直觀簡(jiǎn)單,較容易上手,學(xué)習(xí)曲線只是略陡峭。
這里有一個(gè)學(xué)習(xí)AsyncDisplayKit布局的小游戲,簡(jiǎn)單有趣,可以一玩。
整體上兩天即可上手,無(wú)須擔(dān)心學(xué)習(xí)曲線問(wèn)題。
體會(huì)
當(dāng)過(guò)了上手的艱難階段后,才是真正開(kāi)始體會(huì)AsyncDisplayKit的時(shí)候。用了將近一年,有幾點(diǎn)AsyncDisplayKit的優(yōu)勢(shì)相當(dāng)明顯:
1)cell中再也不用算高度和位置等frame信息了
這是非常非常非常非常誘人的,當(dāng)cell中有動(dòng)態(tài)文本時(shí),文本的高度計(jì)算很費(fèi)神,計(jì)算完,還得緩存,如果再加上其他動(dòng)態(tài)內(nèi)容,比如有時(shí)候沒(méi)圖片,那frame算起來(lái),簡(jiǎn)直讓人想哭,而如果用AsyncDisplayKit,所有的height、frame計(jì)算都煙消云散,甚至都不知道frame這個(gè)東西存在過(guò),很酸爽。
2)一幀不掉
平時(shí)界面稍微動(dòng)態(tài)點(diǎn),元素稍微多點(diǎn),Autolayout的性能就不堪重用,而上古時(shí)代的frame布局在高效緩存的基礎(chǔ)上確實(shí)可以做到高性能,但frame緩存的維護(hù)和計(jì)算都不是一般的復(fù)雜,而AsyncDisplayKit卻能在保持簡(jiǎn)介布局的同時(shí),做到一幀不掉,這是多么的讓人感動(dòng)!
3)更優(yōu)雅的架構(gòu)設(shè)計(jì)
前兩點(diǎn)好處是用AsyncDisplayKit最直接最容易被感受到的,其實(shí),當(dāng)深入使用時(shí),你會(huì)發(fā)現(xiàn),AsyncDisplayKit還會(huì)給程序架構(gòu)設(shè)計(jì)帶來(lái)一些改變,會(huì)使原本復(fù)雜的架構(gòu)變得更簡(jiǎn)單,更優(yōu)雅,更靈活,更容易維護(hù),更容易擴(kuò)展,也會(huì)使整個(gè)代碼更容易理解,而這個(gè)影響是深遠(yuǎn)的,畢竟代碼是寫(xiě)給別人看的。
但AsyncDisplayKit有一個(gè)極其著名的問(wèn)題,閃爍。
當(dāng)我們開(kāi)始試水使用AsyncDisplayKit時(shí),只要簡(jiǎn)單reload一下TableNode,那閃爍,眼睛都瞎了。后來(lái)查了官方的issue,才發(fā)現(xiàn)很多人都提了這個(gè)問(wèn)題,但官方也沒(méi)給出什么優(yōu)雅的解決方案。要知道,閃爍是非常影響用戶體驗(yàn)的。如果非要在不閃爍和帶閃爍的AsyncDisplayKit中選擇,我會(huì)毫不猶豫的選擇不閃爍,而放棄使用AsyncDisplayKit。但現(xiàn)在已經(jīng)不存在這個(gè)選擇了,因?yàn)榻?jīng)過(guò)AsyncDisplayKit的多次迭代努力加上一些小技巧,AsyncDisplayKit的異步閃爍已經(jīng)被優(yōu)雅的解決了。
但AsyncDisplayKit不宜廣泛使用,那些高度固定、UI簡(jiǎn)單用UIKit更好一些,畢竟AsyncDisplayKit并不像UIKit,人人都會(huì),如果內(nèi)容和高度復(fù)雜又很動(dòng)態(tài),強(qiáng)烈推薦AsyncDisplayKit,它會(huì)簡(jiǎn)化太多東西。
疑難點(diǎn)
一年的AsyncDisplayKit使用經(jīng)驗(yàn),踩過(guò)了不少坑,遇到了不少值得注意的問(wèn)題,一并列在這里,以供參考。
ASNetworkImageNode的緩存
ASNetworkImageNode是對(duì)UIImageView需要從網(wǎng)絡(luò)加載圖片這一使用場(chǎng)景的封裝,省去了YYWebImage或者SDWebImage等第三方庫(kù)的引入,只需要設(shè)置URL即可實(shí)現(xiàn)網(wǎng)絡(luò)圖片的自動(dòng)加載。
import AsyncDisplayKit
let avatarImageNode = ASNetworkImageNode()
avatarImageNode.url = URL(string: "http://shellhue.github.io/images/log.png")
這非常省事便捷,但ASNetworkImageNode默認(rèn)用的緩存機(jī)制和圖片下載器是PinRemoteImage,為了使用我們自己的緩存機(jī)制和圖片下載器,需要實(shí)現(xiàn)ASImageCacheProtocol圖片緩存協(xié)議和 ASImageDownloaderProtocol圖片下載器協(xié)議兩個(gè)協(xié)議,然后初始化時(shí),用ASNetworkImageNode的init(cache: ASImageCacheProtocol, downloader: ASImageDownloaderProtocol)初始化方法,傳入對(duì)應(yīng)的類,方便其間,一般會(huì)自定義一個(gè)初始化靜態(tài)方法。我們公司緩存機(jī)制和圖片下載器都是用的YYWebImage,橋接代碼如下。
import YYWebImage
import AsyncDisplayKit
extension ASNetworkImageNode {
static func imageNode() -> ASNetworkImageNode {
let manager = YYWebImageManager.shared()
return ASNetworkImageNode(cache: manager, downloader: manager)
}
}
extension YYWebImageManager: ASImageCacheProtocol, ASImageDownloaderProtocol {
public func downloadImage(with URL: URL,
callbackQueue: DispatchQueue,
downloadProgress: AsyncDisplayKit.ASImageDownloaderProgress?,
completion: @escaping AsyncDisplayKit.ASImageDownloaderCompletion) -> Any? {
weak var operation: YYWebImageOperation?
operation = requestImage(with: URL,
options: .setImageWithFadeAnimation,
progress: { (received, expected) -> Void in
callbackQueue.async(execute: {
let progress = expected == 0 ? 0 : received / expected
downloadProgress?(CGFloat(progress))
})
}, transform: nil, completion: { (image, url, from, state, error) in
completion(image, error, operation)
})
return operation
}
public func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
guard let operation = downloadIdentifier as? YYWebImageOperation else {
return
}
operation.cancel()
}
public func cachedImage(with URL: URL, callbackQueue: DispatchQueue, completion: @escaping AsyncDisplayKit.ASImageCacherCompletion) {
cache?.getImageForKey(cacheKey(for: URL), with: .all, with: { (image, cacheType) in
callbackQueue.async {
completion(image)
}
})
}
}
閃爍
初次使用AsyncDisplayKit,當(dāng)享受其一幀不掉如絲般柔滑的手感時(shí),ASTableNode和ASCollectionNode刷新時(shí)的閃爍一定讓你幾度崩潰,到AsyncDisplayKit的github上搜索閃爍相關(guān)issue,會(huì)出來(lái)100多個(gè)問(wèn)題。閃爍是AsyncDisplayKit與生俱來(lái)的問(wèn)題,聞名遐邇,而閃爍的體驗(yàn)非常糟糕。幸運(yùn)的是,幾經(jīng)探索,AsyncDisplayKit的閃爍問(wèn)題已經(jīng)完美解決,這個(gè)完美指的是一幀不掉的同時(shí)沒(méi)有任何閃爍,同時(shí)也沒(méi)增加代碼的復(fù)雜度。
閃爍可以分為四類,
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)?code>YYWebImage或者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ù)。
既然閃爍是reload時(shí),沒(méi)有事先同步檢查有無(wú)緩存導(dǎo)致的,繼承一個(gè)ASNetworkImageNode的子類,復(fù)寫(xiě)url設(shè)置邏輯:
import AsyncDisplayKit
class NetworkImageNode: ASNetworkImageNode {
override var url: URL? {
didSet {
if let u = url,
let image = UIImage.cachedImage(with: u) else {
self.image = image
placeholderEnabled = false
}
}
}
}
按道理不會(huì)閃爍了,但事實(shí)上仍然會(huì),只要是個(gè)ASNetworkImageNode,無(wú)論怎么設(shè)置,都會(huì)閃,這與官方的API說(shuō)明嚴(yán)重不符,很無(wú)語(yǔ)。迫不得已之下,當(dāng)有緩存時(shí),直接用ASImageNode替換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)
}
}
使用時(shí)將NetworkImageNode當(dāng)成ASNetworkImageNode使用即可。
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)槎际峭剑淮嬖谡嘉粓D的情況,因此也就不會(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),若reload某個(gè)indexPath的cell,在渲染完成之前,主線程是卡死的,這與UITableView的機(jī)制一樣,但速度會(huì)比UITableView快很多,因?yàn)?code>UITableView的布局計(jì)算、資源解壓、視圖合成等都是在主線程進(jìn)行,而ASTableNode則是多個(gè)線程并發(fā)進(jìn)行,何況布局等還有緩存。所以,一般也沒(méi)有問(wèn)題,貝聊的聊天界面只是簡(jiǎn)單這樣設(shè)置后,就不閃了,而且一幀不掉。但當(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)
}
}
關(guān)鍵代碼是,
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)
}
})
}
即,檢查當(dāng)前的indexPath是否被標(biāo)記,如果是,則先設(shè)置cell.neverShowPlaceholders = true,等待reload完成(一幀是1/60秒,這里等待0.5秒,足夠渲染了),將cell.neverShowPlaceholders = false。這樣reload時(shí)既不會(huì)閃爍,也不會(huì)影響滑動(dòng)時(shí)的異步繪制,因此一幀不掉。
這完全是耍小聰明的做法,但確實(shí)非常有效。
3)reloadData時(shí)的閃爍
在下拉刷新后,列表經(jīng)常需要重新刷新,即調(diào)用ASTableNode或者ASCollectionNode的reloadData方法,但會(huì)閃,而且很明顯。有了單個(gè)cell reload時(shí)閃爍的解決方案后,此類閃爍解決起來(lái),就很簡(jiǎn)單了。
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()
... // 其他代碼
}
將肉眼可見(jiàn)的cell添加進(jìn)indexPathesToBeReloaded中即可。
4)insertItems時(shí)更改ASCollectionNode的contentOffset引起的閃爍
我們公司的聊天界面是用AsyncDisplayKit寫(xiě)的,當(dāng)下拉加載更多新消息時(shí),為保持加載后當(dāng)前消息的位置不變,需要在collectionNode.insertItems(at: indexPaths)完成后,復(fù)原collectionNode.view.contentOffset,代碼如下:
func insertMessagesToTop(indexPathes: [IndexPath]) {
let originalContentSizeHeight = collectionNode.view.contentSize.height
let originalContentOffsetY = collectionNode.view.contentOffset.y
let heightFromOriginToContentBottom = originalContentSizeHeight - originalContentOffsetY
let heightFromOriginToContentTop = originalContentOffsetY
collectionNode.performBatch(animated: false, updates: {
self.collectionNode.insertItems(at: indexPaths)
}) { (finished) in
let contentSizeHeight = self.collectionNode.view.contentSize.height
self.collectionNode.view.contentOffset = CGPointMake(0, isLoadingMore ? (contentSizeHeight - heightFromOriginToContentBottom) : heightFromOriginToContentTop)
}
}
遺憾的是,會(huì)閃爍。起初以為是AsyncDisplayKit異步繪制導(dǎo)致的閃爍,一度還想放棄AsyncDisplayKit,用UITableView重寫(xiě)一遍,幸運(yùn)的是,當(dāng)時(shí)項(xiàng)目工期太緊,沒(méi)有時(shí)間重寫(xiě),也沒(méi)時(shí)間仔細(xì)排查,直接帶問(wèn)題上線了。
最近閑暇,經(jīng)仔細(xì)排查,方知不是AsyncDisplayKit的鍋,但也比較難修,有一定的參考價(jià)值,因此一并列在這里。
閃爍的原因是,collectionNode insertItems成功后會(huì)先繪制contentOffset為CGPoint(x: 0, y: 0)時(shí)的一幀畫(huà)面,無(wú)動(dòng)畫(huà)時(shí)這一幀畫(huà)面立即顯示,然后調(diào)用成功回調(diào),回調(diào)中復(fù)原了collectionNode.view.contentOffset,下一幀就顯示復(fù)原了位置的畫(huà)面,前后有變化因此閃爍。這是做消息類APP一并會(huì)遇到的bug,google一下,主要有兩種解決方案,
第一種,通過(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)雅。子類化的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
}
}
... // 其他代碼
}
布局
AsyncDisplayKit采用的是flexbox的布局思想,非常高效直觀簡(jiǎn)潔,但畢竟迥異于AutoLayout和frame layout的布局風(fēng)格,咋一上手,很不習(xí)慣,有些小技巧還是需要慢慢積累,有些概念也需要逐漸熟悉深入,下面列舉幾個(gè)筆者覺(jué)得比較重要的概念
1)設(shè)置任意間距
AutoLayout實(shí)現(xiàn)任意間距,比較容易直觀,因?yàn)?code>AutoLayout的約束,本來(lái)就是我的邊離你的邊有多遠(yuǎn)的概念,而AsyncDisplayKit并沒(méi)有,AsyncDisplayKit里面的概念是,我自己的前面有多少空白距離,我自己的后面有多少空白距離,更強(qiáng)調(diào)自己。假如有三個(gè)元素,怎么約束它們之間的間距?
AutoLayout是這樣的:
import Masonry
class SomeView: UIView {
override init() {
super.init()
let viewA = UIView()
let viewB = UIView()
let viewC = UIView()
addSubview(viewA)
addSubview(viewB)
addSubview(viewC)
viewB.snp.makeConstraints { (make) in
make.left.equalTo(viewA.snp.right).offset(15)
}
viewC.snp.makeConstraints { (make) in
make.left.equalTo(viewB.snp.right).offset(5)
}
}
}
而AsyncDisplayKit是這樣的:
import AsyncDisplayKit
class SomeNode: ASDisplayNode {
let nodeA = ASDisplayNode()
let nodeB = ASDisplayNode()
let nodeC = ASDisplayNode()
override init() {
super.init()
addSubnode(nodeA)
addSubnode(nodeB)
addSubnode(nodeC)
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
nodeB.style.spaceBefore = 15
nodeC.stlye.spaceBefore = 5
return ASStackLayoutSpec(direction: .horizontal, spacing: 0, justifyContent: .start, alignItems: .start, children: [nodeA, nodeB, nodeC])
}
}
如果是拿ASStackLayoutSpec布局,元素之間的任意間距一般是通過(guò)元素自己的spaceBefore或者spaceBefore style實(shí)現(xiàn),這是自我包裹性,更容易理解,如果不是拿ASStackLayoutSpec布局,可以將某個(gè)元素包裹成ASInsetsLayoutSpec,再設(shè)置UIEdgesInsets,保持自己的四周任意邊距。
能任意設(shè)置間距是自由布局的基礎(chǔ)。
2)flexGrow和flexShrink
flexGrow和flexShrink是相當(dāng)重要的概念,flexGrow是指當(dāng)有多余空間時(shí),拉伸誰(shuí)以及相應(yīng)的拉伸比例(當(dāng)有多個(gè)元素設(shè)置了flexGrow時(shí)),flexShrink相反,是指當(dāng)空間不夠時(shí),壓縮誰(shuí)及相應(yīng)的壓縮比例(當(dāng)有多個(gè)元素設(shè)置了flexShrink時(shí))。
靈活使用flexGrow和spacer(占位ASLayoutSpec)可以實(shí)現(xiàn)很多效果,比如等間距,

實(shí)現(xiàn)代碼如下,
import AsyncDisplayKit
class ContainerNode: ASDisplayNode {
let nodeA = ASDisplayNode()
let nodeB = ASDisplayNode()
override init() {
super.init()
addSubnode(nodeA)
addSubnode(nodeB)
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let spacer1 = ASLayoutSpec()
let spacer2 = ASLayoutSpec()
let spacer3 = ASLayoutSpec()
spacer1.stlye.flexGrow = 1
spacer2.stlye.flexGrow = 1
spacer3.stlye.flexGrow = 1
return ASStackLayoutSpec(direction: .horizontal, spacing: 0, justifyContent: .start, alignItems: .start, children: [spacer1, nodeA,spacer2, nodeB, spacer3])
}
}
如果spacer的flexGrow不同就可以實(shí)現(xiàn)指定比例的布局,再結(jié)合width樣式,輕松實(shí)現(xiàn)以下布局

布局代碼如下,
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
let spacer1 = ASLayoutSpec()
let spacer2 = ASLayoutSpec()
let spacer3 = ASLayoutSpec()
spacer1.stlye.flexGrow = 2
spacer2.stlye.width = ASDimensionMake(100)
spacer3.stlye.flexGrow = 1
return ASStackLayoutSpec(direction: .horizontal, spacing: 0, justifyContent: .start, alignItems: .start, children: [spacer1, nodeA,spacer2, nodeB, spacer3])
}
相同的布局如果用Autolayout,麻煩去了。
3)constrainedSize的理解
constrainedSize是指某個(gè)node的大小取值范圍,有minSize和maxSize兩個(gè)屬性。比如下圖的布局:

import AsyncDisplayKit
class ContainerNode: ASDisplayNode {
let nodeA = ASDisplayNode()
let nodeB = ASDisplayNode()
override init() {
super.init()
addSubnode(nodeA)
addSubnode(nodeB)
nodeA.style.preferredSize = CGSize(width: 100, height: 100)
}
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
nodeB.style.flexShrink = 1
nodeB.style.flexGrow = 1
let stack = ASStackLayoutSpec(direction: .horizontal, spacing: e, justifyContent: .start, alignItems: .start, children: [nodeA, nodeB])
return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(a, b, c, d), child: stack)
}
}
其中方法override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec中的constrainedSize所指是ContainerNode自身大小的取值范圍。給定constrainedSize,AsyncDisplayKit會(huì)根據(jù)ContainerNode在layoutSpecThatFits(_:)中施加在nodeA、nodeB的布局規(guī)則和nodeA、nodeB自身屬性計(jì)算nodeA、nodeB的constrainedSize。
假如constrainedSize的minSize是CGSize(width: 0, height: 0),maxSize為CGSize(width: 375, height: Inf+)(Inf+為正無(wú)限大),則:
1)根據(jù)布局規(guī)則和nodeA自身樣式屬性maxWidth、minWidth、width、height、preferredSize,可計(jì)算出nodeA的constrainedSize的minSize和maxSize均為其preferredSize即CGSize(width: 100, height: 100),因?yàn)椴季忠?guī)則為水平向的ASStackLayout,當(dāng)空間富余或者空間不足時(shí),nodeA即不壓縮又不拉伸,所以會(huì)取其指定的preferredSize。
2)根據(jù)布局規(guī)則和nodeB自身樣式屬性maxWidth、minWidth、width、height、preferredSize,可以計(jì)算出其constrainedSize的minSize是CGSize(width: 0, height: 0),maxSize為CGSize(width: 375 - 100 - b - e - d, height: Inf+),因?yàn)?code>nodeB的flexShrink和flexGrow均為1,也即當(dāng)空間富余或者空間不足時(shí),nodeB添滿富余空間或壓縮至空間夠?yàn)橹埂?/p>
如果不指定nodeB的flexShrink和flexGrow,那么當(dāng)空間富余或者空間不足時(shí),AsyncDisplayKit就不知道壓縮和拉伸哪一個(gè)布局元素,則nodeB的constrainedSize的maxSize就變?yōu)?code>CGSize(width: Inf+, height: Inf+),即完全無(wú)大小限制,可想而知,nodeB的子node的布局將完全不對(duì)。這也說(shuō)明另外一個(gè)問(wèn)題,node的constrainedSize并不是一定大于其子node的constrainedSize。
理解constrainedSize的計(jì)算,才能熟練利用node的樣式maxWidth、minWidth、width、height、preferredSize、flexShrink和flexGrow進(jìn)行布局。如果發(fā)現(xiàn)布局結(jié)果不對(duì),而對(duì)應(yīng)node的布局代碼確是正確無(wú)誤,一般極有可能是因?yàn)榇?code>node的父布局元素不正確。
動(dòng)畫(huà)
因?yàn)?code>AsyncDisplayKit的布局方式有兩種,frame布局和flexbox式的布局,相應(yīng)的動(dòng)畫(huà)方式也有兩種
1)frame布局
如果采用的是frame布局,動(dòng)畫(huà)跟普通的UIView相同
class ViewController: ASViewController {
let nodeA = ASDisplayNode()
override func viewDidLoad() {
super.viewDidLoad()
nodeA.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
... // 其他代碼
}
... // 其他代碼
func animateNodeA() {
UIView.animate(withDuration: 0.5) {
let newFrame = ... // 新的frame
nodeA.frame = newFrame
}
}
}
不要覺(jué)得用了AsyncDisplayKit就告別了frame布局,ViewController中主要元素個(gè)數(shù)很少,布局簡(jiǎn)單,因此,一般也還是采用frame layout,如果只是做一些簡(jiǎn)單的動(dòng)畫(huà),直接采用UIView的動(dòng)畫(huà)API即可
2)flexbox式的布局
這種布局方式,是在某個(gè)子node中常用的,如果node內(nèi)部布局發(fā)生了變化,又需要做動(dòng)畫(huà)時(shí),就需要復(fù)寫(xiě)AsyncDisplayKit的動(dòng)畫(huà)API,并基于提供的動(dòng)畫(huà)上下文類context,做動(dòng)畫(huà):
class SomeNode: ASDisplayNode {
let nodeA = ASDisplayNode()
override func animateLayoutTransition(_ context: ASContextTransitioning) {
// 利用context可以獲取animate前后布局信息
UIView.animate(withDuration: 0.5) {
// 不使用系統(tǒng)默認(rèn)的fade動(dòng)畫(huà),采用自定義動(dòng)畫(huà)
let newFrame = ... // 新的frame
nodeA.frame = newFrame
}
}
}
系統(tǒng)默認(rèn)的動(dòng)畫(huà)是漸隱漸顯,可以獲取animate前后布局信息,比如某個(gè)子node兩種布局中的frame,然后再自定義動(dòng)畫(huà)類型。如果想觸發(fā)動(dòng)畫(huà),主動(dòng)調(diào)用SomeNode的觸發(fā)方法transitionLayout(withAnimation:shouldMeasureAsync:measurementCompletion:)即可。
內(nèi)存泄漏
為了方便將一個(gè)UIView或者CALayer轉(zhuǎn)化為一個(gè)ASDisplayNode,系統(tǒng)提供了用block初始化ASDisplayNode的簡(jiǎn)便方法:
public convenience init(viewBlock: @escaping AsyncDisplayKit.ASDisplayNodeViewBlock)
public convenience init(viewBlock: @escaping AsyncDisplayKit.ASDisplayNodeViewBlock, didLoad didLoadBlock: AsyncDisplayKit.ASDisplayNodeDidLoadBlock? = nil)
public convenience init(layerBlock: @escaping AsyncDisplayKit.ASDisplayNodeLayerBlock)
public convenience init(layerBlock: @escaping AsyncDisplayKit.ASDisplayNodeLayerBlock, didLoad didLoadBlock: AsyncDisplayKit.ASDisplayNodeDidLoadBlock? = nil)
需要注意的是所傳入的block會(huì)被要?jiǎng)?chuàng)建的node持有。如果block中反過(guò)來(lái)持有了這個(gè)node的持有者,則會(huì)產(chǎn)生循環(huán)引用,導(dǎo)致內(nèi)存泄漏:
class SomeNode {
var nodeA: ASDisplayNode!
let color = UIColor.red
override init() {
super.init()
nodeA = ASDisplayNode {
let view = UIView()
view.backgroundColor = self.color // 內(nèi)存泄漏
return view
}
}
}
子線程崩潰
AsyncDisplayKit的性能優(yōu)勢(shì)來(lái)源于異步繪制,異步的意思是有時(shí)候node會(huì)在子線程創(chuàng)建,如果繼承了一個(gè)ASDisplayNode,一不小心在初始化時(shí)調(diào)用了UIKit的相關(guān)方法,則會(huì)出現(xiàn)子線程崩潰。比如以下node,
class SomeNode {
let iconImageNode: ASDisplayNode
let color = UIColor.red
override init() {
iconImageNode = ASImageNode()
iconImageNode.image = UIImage(named: "iconName") // 需注意SomeNode有時(shí)會(huì)在子線程初始化,而UIImage(named:)并不是線程安全
super.init()
}
}
但在node初始化時(shí)調(diào)用UIImage(named:)創(chuàng)建圖片是不可避免的,用methodSwizzle將UIImage(named:)置換成安全的即可。
其實(shí)在子線程初始化node并不多見(jiàn),一般都在主線程。
總結(jié)
一年的實(shí)踐下來(lái),閃爍是AsyncDisplayKit遇到的最大的問(wèn)題,修復(fù)起來(lái)也頗為費(fèi)神。其他bug,有時(shí)雖然很讓人頭疼,但由于AsyncDisplayKit是對(duì)UIKit的再封裝,實(shí)在不行,仍然可以越過(guò)AsyncDisplayKit用UIKit的方法修復(fù)。
學(xué)習(xí)曲線也不算很陡峭。
考慮到AsyncDisplayKit的種種好處,非常推薦AsyncDisplayKit,當(dāng)然還是僅限于用在比較復(fù)雜和動(dòng)態(tài)的頁(yè)面中。
個(gè)人博客原文鏈接:http://qingmo.me/
歡迎關(guān)注我的微博以便交流:輕墨