歡迎關注個人微博以便交流:輕墨node
一個第三方庫能作到像新產品同樣,值得你們去寫寫使用體會的,並很少見,AsyncDisplayKit
卻徹底能夠,由於AsyncDisplayKit
不只僅是一個工具,它更像一個系統UI框架,改變整個編碼體驗。也正是這種極強的侵入性,致使很多聽過、star過,甚至下過demo跑過AsyncDisplayKit
的你我,望而卻步,駐足觀望。但列表界面稍微複雜時,煩人的高度計算,由於性能不得不放棄Autolayout
而選擇上古時代的frame layout
,使人精疲力盡,這時AsyncDisplayKit
總會不天然浮現眼前,讓你躍躍欲試。git
去年10月份,咱們入坑了。github
當時還只是拿簡單的列表頁試水,基本上手後,去年末在稍微空閒的時候用AsyncDisplayKit
重構了帖子詳情,今年三月份,又藉着公司聊天增長羣聊的契機,用AsyncDisplayKit
重構整個聊天。林林總總,從簡單到複雜,踩過的坑大大小小,將近一年的時光轉眼飛逝,能夠寫寫總結了。shell
先說說學習曲線,這是你們都比較關心的問題。swift
跟大多人同樣,一開始我覺得AsyncDisplayKit
會像Rxswift
等MVVM
框架同樣,有着陡峭的學習曲線。但事實上,AsyncDisplayKit
的學習曲線還算平滑。api
主要是由於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
是對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
的閃爍問題已經完美解決,這個完美指的是一幀不掉的同時沒有任何閃爍,同時也沒增長代碼的複雜度。
閃爍能夠分爲四類,
當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
使用便可。
當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
時既不會閃爍,也不會影響滑動時的異步繪製,所以一幀不掉。
這徹底是耍小聰明的作法,但確實很是有效。
在下拉刷新後,列表常常須要從新刷新,即調用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
中便可。
咱們公司的聊天界面是用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
的佈局風格,咋一上手,很不習慣,有些小技巧仍是須要慢慢積累,有些概念也須要逐漸熟悉深刻,下面列舉幾個筆者以爲比較重要的概念
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
,保持本身的四周任意邊距。
能任意設置間距是自由佈局的基礎。
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
,麻煩去了。
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
式的佈局,相應的動畫方式也有兩種
若是採用的是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
便可
這種佈局方式,是在某個子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/
歡迎關注個人微博以便交流:輕墨