歡迎關注我的微博以便交流:輕墨
一個第三方庫能做到像新產品一樣,值得大家去寫寫使用體會的,並不多見,AsyncDisplayKit
卻完全可以,因為AsyncDisplayKit
不僅僅是一個工具,它更像一個系統UI框架,改變整個編碼體驗。也正是這種極強的侵入性,導致不少聽過、star過,甚至下過demo跑過AsyncDisplayKit
的你我,望而卻步,駐足觀望。但列表介面稍微複雜時,煩人的高度計算,因為效能不得不放棄Autolayout
而選擇上古時代的frame layout
,令人精疲力盡,這時AsyncDisplayKit
總會不自然浮現眼前,讓你躍躍欲試。
去年10月份,我們入坑了。
當時還只是拿簡單的列表頁試水,基本上手後,去年底在稍微空閒的時候用AsyncDisplayKit
重構了帖子詳情,今年三月份,又藉著公司聊天增加群聊的契機,用AsyncDisplayKit
重構整個聊天。林林總總,從簡單到複雜,踩過的坑大大小小,將近一年的時光轉眼飛逝,可以寫寫總結了。
學習曲線
先說說學習曲線,這是大家都比較關心的問題。
跟大多人一樣,一開始我以為AsyncDisplayKit
會像Rxswift
等MVVM
框架一樣,有著陡峭的學習曲線。但事實上,AsyncDisplayKit
的學習曲線還算平滑。
主要是因為AsyncDisplayKit
只是對UIKit
的再一次封裝,基本沿用了UIKit
的API
設計,大部分情況下,只是將view
改成node
,UI
字首改為AS
,寫著寫著,恍惚間,你以為自己還是在寫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()複製程式碼
相信你看兩眼也就摸出門道了,大部分API一模一樣。
真正發生翻天覆地變化的是佈局方式,AsyncDisplayKit
用的是flexbox
佈局,UIView
使用的是Autolayout
。用AsyncDisplayKit
的flexbox
佈局替代Autolayout
佈局,完全不亞於用Autolayout
替換frame
佈局的蛻變,需要比較大的觀念轉變。
但flexbox
佈局被提出已久,且其本身直觀簡單,較容易上手,學習曲線只是略陡峭。
集中精力,整體上兩天即可上手,無須擔心學習曲線問題。
這裡有一個學習AsyncDisplayKit
佈局的小遊戲,簡單有趣,可以一玩。
體會
當過了上手的艱難階段後,才是真正開始體會AsyncDisplayKit
的時候。用了將近一年,有幾點AsyncDisplayKit
的優勢相當明顯:
1)cell
中再也不用算高度和位置等frame
資訊了
這是非常非常非常非常誘人的,當cell
中有動態文字時,文字的高度計算很費神,計算完,還得快取,如果再加上其他動態內容,比如有時候沒圖片,那frame
算起來,簡直讓人想哭,而如果用AsyncDisplayKit
,所有的height
、frame
計算都煙消雲散,甚至都不知道frame
這個東西存在過。
2)一幀不掉
平時介面稍微動態點,元素稍微多點,Autolayout
的效能就不堪重用,而上古時代的frame
佈局在高效快取的基礎上確實可以做到高效能,但frame
快取的維護和計算都不是一般的複雜,而AsyncDisplayKit
卻能在保持簡介佈局的同時,做到一幀不掉,這是多麼的讓人感動!
3)更優雅的架構設計
前兩點好處是用AsyncDisplayKit
最直接最容易被感受到的,其實,當深入使用時,你會發現,AsyncDisplayKit
還會給程式架構設計帶來一些改變,會使原本複雜的架構變得更簡單,更優雅,更靈活,更容易維護,更容易擴充套件,也會使整個程式碼更容易理解,而這個影響是深遠的,畢竟程式碼是寫給別人看的。
但AsyncDisplayKit
有一個極其著名的問題,閃爍。
當我們開始試水使用AsyncDisplayKit
時,只要簡單reload
一下TableNode
,那閃爍,眼睛都瞎了。後來查了官方的issue
,才發現很多人都提了這個問題,但官方也沒給出什麼優雅的解決方案。要知道,閃爍是非常影響使用者體驗的。如果非要在不閃爍和帶閃爍的AsyncDisplayKit
中選擇,我會毫不猶豫的選擇不閃爍,而放棄使用AsyncDisplayKit
。但現在已經不存在這個選擇了,因為經過AsyncDisplayKit
的多次迭代努力加上一些小技巧,AsyncDisplayKit
的非同步閃爍已經被優雅的解決了。
但AsyncDisplayKit
不宜廣泛使用,那些高度固定、UI
簡單的用UIKit
就好了,畢竟AsyncDisplayKit
並不像UIKit
,人人都會。但如果內容和高度複雜又很動態,強烈推薦AsyncDisplayKit
,它會簡化太多東西。
疑難點
一年的AsyncDisplayKit
使用經驗,踩過了不少坑,遇到了不少值得注意的問題,一併列在這裡,以供參考。
ASNetworkImageNode的快取
ASNetworkImageNode
是對UIImageView
需要從網路載入圖片這一使用場景的封裝,省去了YYWebImage
或者SDWebImage
等第三方庫的引入,只需要設定URL
即可實現網路圖片的自動載入。
import AsyncDisplayKit
let avatarImageNode = ASNetworkImageNode()
avatarImageNode.url = URL(string: "http://shellhue.github.io/images/log.png")複製程式碼
這非常省事便捷,但ASNetworkImageNode
預設用的快取機制和圖片下載器是PinRemoteImage
,為了使用我們自己的快取機制和圖片下載器,需要實現ASImageCacheProtocol
圖片快取協議和 ASImageDownloaderProtocol
圖片下載器協議兩個協議,然後初始化時,用ASNetworkImageNode
的init(cache: ASImageCacheProtocol, downloader: ASImageDownloaderProtocol)
初始化方法,傳入對應的類,方便其間,一般會自定義一個初始化靜態方法。我們公司快取機制和圖片下載器都是用的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
,當享受其一幀不掉如絲般柔滑的手感時,ASTableNode
和ASCollectionNode
重新整理時的閃爍一定讓你幾度崩潰,到AsyncDisplayKit
的github
上搜尋閃爍相關issue,會出來100多個問題。閃爍是AsyncDisplayKit
與生俱來的問題,聞名遐邇,而閃爍的體驗非常糟糕。幸運的是,幾經探索,AsyncDisplayKit
的閃爍問題已經完美解決,這個完美指的是一幀不掉的同時沒有任何閃爍,同時也沒增加程式碼的複雜度。
閃爍可以分為四類,
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秒而已,自欺欺人,並沒有修復。
既然閃爍是reload
時,沒有事先同步檢查有無快取導致的,繼承一個ASNetworkImageNode
的子類,複寫url
設定邏輯:
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
}
}
}
}複製程式碼
按道理不會閃爍了,但事實上仍然會,只要是個ASNetworkImageNode
,無論怎麼設定,都會閃,這與官方的API說明嚴重不符,很無語。迫不得已之下,當有快取時,直接用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)
}
}複製程式碼
使用時將NetworkImageNode
當成ASNetworkImageNode
使用即可。
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
從非同步狀態衰退回同步狀態,若reload
某個indexPath
的cell
,在渲染完成之前,主執行緒是卡死的,這與UITableView
的機制一樣,但速度會比UITableView
快很多,因為UITableView
的佈局計算、資源解壓、檢視合成等都是在主執行緒進行,而ASTableNode
則是多個執行緒併發進行,何況佈局等還有快取。所以,一般也沒有問題,貝聊的聊天介面只是簡單這樣設定後,就不閃了,而且一幀不掉。但當頁面佈局較為複雜時,滑動時的卡頓掉幀就變的肉眼可見。
這時,可以設定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)
}
}複製程式碼
關鍵程式碼是,
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)
}
})
}複製程式碼
即,檢查當前的indexPath
是否被標記,如果是,則先設定cell.neverShowPlaceholders = true
,等待reload
完成(一幀是1/60秒,這裡等待0.5秒,足夠渲染了),將cell.neverShowPlaceholders = false
。這樣reload
時既不會閃爍,也不會影響滑動時的非同步繪製,因此一幀不掉。
這完全是耍小聰明的做法,但確實非常有效。
3)reloadData時的閃爍
在下拉重新整理後,列表經常需要重新重新整理,即呼叫ASTableNode
或者ASCollectionNode
的reloadData
方法,但會閃,而且很明顯。有了單個cell reload
時閃爍的解決方案後,此類閃爍解決起來,就很簡單了。
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()
... // 其他程式碼
}複製程式碼
將肉眼可見的cell
新增進indexPathesToBeReloaded
中即可。
4)insertItems時更改ASCollectionNode的contentOffset引起的閃爍
我們公司的聊天介面是用AsyncDisplayKit
寫的,當下拉載入更多新訊息時,為保持載入後當前訊息的位置不變,需要在collectionNode.insertItems(at: indexPaths)
完成後,復原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)
}
}複製程式碼
遺憾的是,會閃爍。起初以為是AsyncDisplayKit
非同步繪製導致的閃爍,一度還想放棄AsyncDisplayKit
,用UITableView
重寫一遍,幸運的是,當時專案工期太緊,沒有時間重寫,也沒時間仔細排查,直接帶問題上線了。
最近閒暇,經仔細排查,方知不是AsyncDisplayKit
的鍋,但也比較難修,有一定的參考價值,因此一併列在這裡。
閃爍的原因是,collectionNode insertItems
成功後會先繪製contentOffset
為CGPoint(x: 0, y: 0)
時的一幀畫面,無動畫時這一幀畫面立即顯示,然後呼叫成功回撥,回撥中復原了collectionNode.view.contentOffset
,下一幀就顯示覆原了位置的畫面,前後有變化因此閃爍。這是做訊息類APP一併會遇到的bug,google一下,主要有兩種解決方案,
第一種,通過仿射變換倒置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
採用的是flexbox
的佈局思想,非常高效直觀簡潔,但畢竟迥異於AutoLayout
和frame layout
的佈局風格,咋一上手,很不習慣,有些小技巧還是需要慢慢積累,有些概念也需要逐漸熟悉深入,下面列舉幾個筆者覺得比較重要的概念
1)設定任意間距
AutoLayout
實現任意間距,比較容易直觀,因為AutoLayout
的約束,本來就是我的邊離你的邊有多遠的概念,而AsyncDisplayKit
並沒有,AsyncDisplayKit
裡面的概念是,我自己的前面有多少空白距離,我自己的後面有多少空白距離,更強調自己。假如有三個元素,怎麼約束它們之間的間距?
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
佈局,元素之間的任意間距一般是通過元素自己的spaceBefore
或者spaceBefore style
實現,這是自我包裹性,更容易理解,如果不是拿ASStackLayoutSpec
佈局,可以將某個元素包裹成ASInsetsLayoutSpec
,再設定UIEdgesInsets
,保持自己的四周任意邊距。
能任意設定間距是自由佈局的基礎。
2)flexGrow和flexShrink
flexGrow
和flexShrink
是相當重要的概念,flexGrow
是指當有多餘空間時,拉伸誰以及相應的拉伸比例(當有多個元素設定了flexGrow
時),flexShrink
相反,是指當空間不夠時,壓縮誰及相應的壓縮比例(當有多個元素設定了flexShrink
時)。
靈活使用flexGrow
和spacer
(佔位ASLayoutSpec
)可以實現很多效果,比如等間距,
實現程式碼如下,
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
不同就可以實現指定比例的佈局,再結合width
樣式,輕鬆實現以下佈局
佈局程式碼如下,
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
是指某個node
的大小取值範圍,有minSize
和maxSize
兩個屬性。比如下圖的佈局:
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
會根據ContainerNode
在layoutSpecThatFits(_:)
中施加在nodeA、nodeB
的佈局規則和nodeA、nodeB
自身屬性計算nodeA、nodeB
的constrainedSize
。
假如constrainedSize
的minSize
是CGSize(width: 0, height: 0)
,maxSize
為CGSize(width: 375, height: Inf+)
(Inf+
為正無限大),則:
1)根據佈局規則和nodeA
自身樣式屬性maxWidth
、minWidth
、width
、height
、preferredSize
,可計算出nodeA
的constrainedSize
的minSize
和maxSize
均為其preferredSize
即CGSize(width: 100, height: 100)
,因為佈局規則為水平向的ASStackLayout
,當空間富餘或者空間不足時,nodeA
即不壓縮又不拉伸,所以會取其指定的preferredSize
。
2)根據佈局規則和nodeB
自身樣式屬性maxWidth
、minWidth
、width
、height
、preferredSize
,可以計算出其constrainedSize
的minSize
是CGSize(width: 0, height: 0)
,maxSize
為CGSize(width: 375 - 100 - b - e - d, height: Inf+)
,因為nodeB
的flexShrink
和flexGrow
均為1,也即當空間富餘或者空間不足時,nodeB
添滿富餘空間或壓縮至空間夠為止。
如果不指定nodeB
的flexShrink
和flexGrow
,那麼當空間富餘或者空間不足時,AsyncDisplayKit
就不知道壓縮和拉伸哪一個佈局元素,則nodeB
的constrainedSize
的maxSize
就變為CGSize(width: Inf+, height: Inf+)
,即完全無大小限制,可想而知,nodeB
的子node
的佈局將完全不對。這也說明另外一個問題,node
的constrainedSize
並不是一定大於其子node
的constrainedSize
。
理解constrainedSize
的計算,才能熟練利用node
的樣式maxWidth
、minWidth
、width
、height
、preferredSize
、flexShrink
和flexGrow
進行佈局。如果發現佈局結果不對,而對應node
的佈局程式碼確是正確無誤,一般極有可能是因為此node
的父佈局元素不正確。
動畫
因為AsyncDisplayKit
的佈局方式有兩種,frame
佈局和flexbox
式的佈局,相應的動畫方式也有兩種
1)frame佈局
如果採用的是frame
佈局,動畫跟普通的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
}
}
}複製程式碼
不要覺得用了AsyncDisplayKit
就告別了frame
佈局,ViewController
中主要元素個數很少,佈局簡單,因此,一般也還是採用frame layout
,如果只是做一些簡單的動畫,直接採用UIView
的動畫API
即可
2)flexbox式的佈局
這種佈局方式,是在某個子node
中常用的,如果node
內部佈局發生了變化,又需要做動畫時,就需要複寫AsyncDisplayKit
的動畫API
,並基於提供的動畫上下文類context
,做動畫:
class SomeNode: ASDisplayNode {
let nodeA = ASDisplayNode()
override func animateLayoutTransition(_ context: ASContextTransitioning) {
// 利用context可以獲取animate前後佈局資訊
UIView.animate(withDuration: 0.5) {
// 不使用系統預設的fade動畫,採用自定義動畫
let newFrame = ... // 新的frame
nodeA.frame = newFrame
}
}
}複製程式碼
系統預設的動畫是漸隱漸顯,可以獲取animate
前後佈局資訊,比如某個子node
兩種佈局中的frame
,然後再自定義動畫型別。如果想觸發動畫,主動呼叫SomeNode
的觸發方法transitionLayout(withAnimation:shouldMeasureAsync:measurementCompletion:)
即可。
記憶體洩漏
為了方便將一個UIView
或者CALayer
轉化為一個ASDisplayNode
,系統提供了用block
初始化ASDisplayNode
的簡便方法:
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
會被要建立的node
持有。如果block
中反過來持有了這個node
的持有者,則會產生迴圈引用,導致記憶體洩漏:
class SomeNode {
var nodeA: ASDisplayNode!
let color = UIColor.red
override init() {
super.init()
nodeA = ASDisplayNode {
let view = UIView()
view.backgroundColor = self.color // 記憶體洩漏
return view
}
}
}複製程式碼
子執行緒崩潰
AsyncDisplayKit
的效能優勢來源於非同步繪製,非同步的意思是有時候node
會在子執行緒建立,如果繼承了一個ASDisplayNode
,一不小心在初始化時呼叫了UIKit
的相關方法,則會出現子執行緒崩潰。比如以下node
,
class SomeNode {
let iconImageNode: ASDisplayNode
let color = UIColor.red
override init() {
iconImageNode = ASImageNode()
iconImageNode.image = UIImage(named: "iconName") // 需注意SomeNode有時會在子執行緒初始化,而UIImage(named:)並不是執行緒安全
super.init()
}
}複製程式碼
但在node
初始化時呼叫UIImage(named:)
建立圖片是不可避免的,用methodSwizzle
將UIImage(named:)
置換成安全的即可。
其實在子執行緒初始化node
並不多見,一般都在主執行緒。
總結
一年的實踐下來,閃爍是AsyncDisplayKit
遇到的最大的問題,修復起來也頗為費神。其他bug,有時雖然很讓人頭疼,但由於AsyncDisplayKit
是對UIKit的再封裝,實在不行,仍然可以越過AsyncDisplayKit
用UIKit
的方法修復。
學習曲線也不算很陡峭。
考慮到AsyncDisplayKit
的種種好處,非常推薦AsyncDisplayKit
,當然還是僅限於用在比較複雜和動態的頁面中。
個人部落格原文連結:qingmo.me/
歡迎關注我的微博以便交流:輕墨