一個第三方庫能作到像新產品同樣,值得你們去寫寫使用體會的,並很少見,AsyncDisplayKit
卻徹底能夠,由於AsyncDisplayKit
不只僅是一個工具,它更像一個系統UI框架,改變整個編碼體驗。也正是這種極強的侵入性,致使很多聽過、star過,甚至下過demo跑過AsyncDisplayKit
的你我,望而卻步,駐足觀望。但列表界面稍微複雜時,煩人的高度計算,由於性能不得不放棄Autolayout
而選擇上古時代的frame layout
,使人精疲力盡,這時AsyncDisplayKit
總會不天然浮現眼前,讓你躍躍欲試。node
去年10月份,咱們入坑了。git
當時還只是拿簡單的列表頁試水,基本上手後,去年末在稍微空閒的時候用AsyncDisplayKit
重構了帖子詳情,今年三月份,又藉着公司聊天增長羣聊的契機,用AsyncDisplayKit
重構整個聊天。林林總總,從簡單到複雜,踩過的坑大大小小,將近一年的時光轉眼飛逝,能夠寫寫總結了。github
先說說學習曲線,這是你們都比較關心的問題。shell
跟大多人同樣,一開始我覺得AsyncDisplayKit
會像Rxswift
等MVVM
框架同樣,有着陡峭的學習曲線。但事實上,AsyncDisplayKit
的學習曲線還算平滑。swift
主要是由於AsyncDisplayKit
只是對UIKit
的再一次封裝,基本沿用了UIKit
的API
設計,大部分狀況下,只是將view
改爲node
,UI
前綴改成AS
,寫着寫着,恍惚間,你覺得本身仍是在寫UIKit
呢。api
好比ASDisplayNode
與UIView
:緩存
1 |
let nodeA = ASDisplayNode() |
相信你看兩眼也就摸出門道了,大部分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
便可實現網絡圖片的自動加載。
1 |
import AsyncDisplayKit |
這很是省事便捷,但ASNetworkImageNode
默認用的緩存機制和圖片下載器是PinRemoteImage
,爲了使用咱們本身的緩存機制和圖片下載器,須要實現ASImageCacheProtocol
圖片緩存協議和 ASImageDownloaderProtocol
圖片下載器協議兩個協議,而後初始化時,用ASNetworkImageNode
的init(cache: ASImageCacheProtocol, downloader: ASImageDownloaderProtocol)
初始化方法,傳入對應的類,方便其間,通常會自定義一個初始化靜態方法。咱們公司緩存機制和圖片下載器都是用的YYWebImage
,橋接代碼以下。
1 |
import YYWebImage |
初次使用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
官方給的修復思路是:
1 |
import AsyncDisplayKit |
這樣修改後,確實沒有閃爍了,但這只是將PlaceholderImage
—>fetched image
圖片替換致使的閃爍拉長到3秒而已,自欺欺人,並無修復。
既然閃爍是reload
時,沒有事先同步檢查有無緩存致使的,繼承一個ASNetworkImageNode
的子類,複寫url
設置邏輯:
1 |
import AsyncDisplayKit |
按道理不會閃爍了,但事實上仍然會,只要是個ASNetworkImageNode
,不管怎麼設置,都會閃,這與官方的API說明嚴重不符,很無語。無可奈何之下,當有緩存時,直接用ASImageNode
替換ASNetworkImageNode
。
1 |
import AsyncDisplayKit |
使用時將NetworkImageNode
當成ASNetworkImageNode
使用便可。
當reload ASTableNode
或者ASCollectionNode
的某個indexPath
的cell
時,也會閃爍。緣由和ASNetworkImageNode
很像,都是異步惹的禍。當異步計算cell
的佈局時,cell
使用placeholder
佔位(一般是白圖),佈局完成時,才用渲染好的內容填充cell
,placeholder
到渲染好的內容切換引發閃爍。UITableViewCell
由於都是同步,不存在佔位圖的狀況,所以也就不會閃。
先看官方的修改方案,
1 |
func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode { |
這個方案很是有效,由於設置cell.neverShowPlaceholders = true
,會讓cell
從異步狀態衰退回同步狀態,若reload
某個indexPath
的cell
,在渲染完成以前,主線程是卡死的,這與UITableView
的機制同樣,但速度會比UITableView
快不少,由於UITableView
的佈局計算、資源解壓、視圖合成等都是在主線程進行,而ASTableNode
則是多個線程併發進行,況且佈局等還有緩存。因此,通常也沒有問題,貝聊的聊天界面只是簡單這樣設置後,就不閃了,並且一幀不掉。但當頁面佈局較爲複雜時,滑動時的卡頓掉幀就變的肉眼可見。
這時,能夠設置ASTableNode
的leadingScreensForBatching
減緩卡頓
1 |
override func viewDidLoad() { |
通常設置tableNode.leadingScreensForBatching = 4
即提早計算四個屏幕的內容時,掉幀就很不明顯了,典型的空間換時間。但仍不完美,仍然會掉幀,而咱們指望的是一幀不掉,如絲般順滑。這不難,基於上面不閃的方案,刷點小聰明就能解決。
1 |
class ViewController: ASViewController { |
關鍵代碼是,
1 |
if indexPathesToBeReloaded.contains(indexPath) { |
即,檢查當前的indexPath
是否被標記,若是是,則先設置cell.neverShowPlaceholders = true
,等待reload
完成(一幀是1/60秒,這裏等待0.5秒,足夠渲染了),將cell.neverShowPlaceholders = false
。這樣reload
時既不會閃爍,也不會影響滑動時的異步繪製,所以一幀不掉。
這徹底是耍小聰明的作法,但確實很是有效。
在下拉刷新後,列表常常須要從新刷新,即調用ASTableNode
或者ASCollectionNode
的reloadData
方法,但會閃,並且很明顯。有了單個cell reload
時閃爍的解決方案後,此類閃爍解決起來,就很簡單了。
1 |
func reloadDataActionHappensHere() { |
將肉眼可見的cell
添加進indexPathesToBeReloaded
中便可。
咱們公司的聊天界面是用AsyncDisplayKit
寫的,當下拉加載更多新消息時,爲保持加載後當前消息的位置不變,須要在collectionNode.insertItems(at: indexPaths)
完成後,復原collectionNode.view.contentOffset
,代碼以下:
1 |
func insertMessagesToTop(indexPathes: [IndexPath]) { |
遺憾的是,會閃爍。起初覺得是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
以下:
1 |
class CollectionFlowLayout: UICollectionViewFlowLayout { |
當須要insertItems
而且保持位置時,將CollectionFlowLayout
的isInsertingToTop
設置爲true
便可,完成後再設置爲false
。以下,
1 |
class MessagesViewController: ASViewController { |
AsyncDisplayKit
採用的是flexbox
的佈局思想,很是高效直觀簡潔,但畢竟迥異於AutoLayout
和frame layout
的佈局風格,咋一上手,很不習慣,有些小技巧仍是須要慢慢積累,有些概念也須要逐漸熟悉深刻,下面列舉幾個筆者以爲比較重要的概念
AutoLayout
實現任意間距,比較容易直觀,由於AutoLayout
的約束,原本就是個人邊離你的邊有多遠的概念,而AsyncDisplayKit
並無,AsyncDisplayKit
裏面的概念是,我本身的前面有多少空白距離,我本身的後面有多少空白距離,更強調本身。假若有三個元素,怎麼約束它們之間的間距?
AutoLayout
是這樣的:
1 |
import Masonry |
而AsyncDisplayKit
是這樣的:
1 |
import AsyncDisplayKit |
若是是拿ASStackLayoutSpec
佈局,元素之間的任意間距通常是經過元素本身的spaceBefore
或者spaceBefore style
實現,這是自我包裹性,更容易理解,若是不是拿ASStackLayoutSpec
佈局,能夠將某個元素包裹成ASInsetsLayoutSpec
,再設置UIEdgesInsets
,保持本身的四周任意邊距。
能任意設置間距是自由佈局的基礎。
flexGrow
和flexShrink
是至關重要的概念,flexGrow
是指當有多餘空間時,拉伸誰以及相應的拉伸比例(當有多個元素設置了flexGrow
時),flexShrink
相反,是指當空間不夠時,壓縮誰及相應的壓縮比例(當有多個元素設置了flexShrink
時)。
靈活使用flexGrow
和spacer
(佔位ASLayoutSpec
)能夠實現不少效果,好比等間距,
實現代碼以下,
1 |
import AsyncDisplayKit |
若是spacer
的flexGrow
不一樣就能夠實現指定比例的佈局,再結合width
樣式,輕鬆實現如下佈局
佈局代碼以下,
1 |
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { |
相同的佈局若是用Autolayout
,麻煩去了。
constrainedSize
是指某個node
的大小取值範圍,有minSize
和maxSize
兩個屬性。好比下圖的佈局:
1 |
import AsyncDisplayKit |
其中方法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
相同
1 |
class ViewController: ASViewController { |
不要以爲用了AsyncDisplayKit
就告別了frame
佈局,ViewController
中主要元素個數不多,佈局簡單,所以,通常也仍是採用frame layout
,若是隻是作一些簡單的動畫,直接採用UIView
的動畫API
便可
這種佈局方式,是在某個子node
中經常使用的,若是node
內部佈局發生了變化,又須要作動畫時,就須要複寫AsyncDisplayKit
的動畫API
,並基於提供的動畫上下文類context
,作動畫:
1 |
class SomeNode: ASDisplayNode { |
系統默認的動畫是漸隱漸顯,能夠獲取animate
先後佈局信息,好比某個子node
兩種佈局中的frame
,而後再自定義動畫類型。若是想觸發動畫,主動調用SomeNode
的觸發方法transitionLayout(withAnimation:shouldMeasureAsync:measurementCompletion:)
便可。
爲了方便將一個UIView
或者CALayer
轉化爲一個ASDisplayNode
,系統提供了用block
初始化ASDisplayNode
的簡便方法:
1 |
public convenience init(viewBlock: @escaping AsyncDisplayKit.ASDisplayNodeViewBlock) |
須要注意的是所傳入的block
會被要建立的node
持有。若是block
中反過來持有了這個node
的持有者,則會產生循環引用,致使內存泄漏:
1 |
class SomeNode { |
AsyncDisplayKit
的性能優點來源於異步繪製,異步的意思是有時候node
會在子線程建立,若是繼承了一個ASDisplayNode
,一不當心在初始化時調用了UIKit
的相關方法,則會出現子線程崩潰。好比如下node
,
1 |
class SomeNode { |
但在node
初始化時調用UIImage(named:)
建立圖片是不可避免的,用methodSwizzle
將UIImage(named:)
置換成安全的便可。
其實在子線程初始化node
並很少見,通常都在主線程。
一年的實踐下來,閃爍是AsyncDisplayKit
遇到的最大的問題,修復起來也頗爲費神。其餘bug,有時雖然很讓人頭疼,但因爲AsyncDisplayKit
是對UIKit的再封裝,實在不行,仍然能夠越過AsyncDisplayKit
用UIKit
的方法修復。
學習曲線也不算很陡峭。
考慮到AsyncDisplayKit
的種種好處,很是推薦AsyncDisplayKit
,固然仍是僅限於用在比較複雜和動態的頁面中。
http://qingmo.me/2017/07/21/asyndisplaykit/
https://blog.csdn.net/weixin_35755389/article/details/54692401