[原文:https://github.com/nixzhu/dev-blog/blob/master/2014-11-22-asyncdisplaykit-tutorial-achieving-60-fps-scrolling.md]
Facebook 的 Paper 團(tuán)隊(duì)給我們帶來(lái)另一個(gè)很棒的庫(kù):AsyncDisplayKit。這個(gè)庫(kù)能讓你通過(guò)將圖像解碼、布局以及渲染操作放在后臺(tái)線程,從而帶來(lái)超級(jí)響應(yīng)的用戶界面,也就是說(shuō)不再會(huì)因界面卡頓而阻斷用戶交互。既然這么厲害,那就在本教程里學(xué)一下它吧。
例如,對(duì)于非常復(fù)雜的界面,你可以使用 AsyncDisplayKit 構(gòu)建它而得到一種如絲般順滑的,60幀每秒的滑動(dòng)體驗(yàn)。而平常的 UIKit 優(yōu)化就不太可能克服這樣的性能挑戰(zhàn)。
在本教程中,你將從一個(gè)初始項(xiàng)目開(kāi)始,它主要有一個(gè) UICollectionView 的滑動(dòng)問(wèn)題,而使用 AsyncDisplayKit 將大大提高其滑動(dòng)性能。一路上,你將學(xué)會(huì)如何在舊項(xiàng)目中使用 AsyncDisplayKit。
注意:在開(kāi)始本教程之前,你應(yīng)該已熟悉 Swift、Core Animation 以及 Core Graphics。
開(kāi)始
開(kāi)始之前,先看看 AsyncDisplayKit 的介紹。以對(duì)它有個(gè)簡(jiǎn)要的概念,知道它是要解決什么問(wèn)題。
準(zhǔn)備好了后就下載初始項(xiàng)目吧。你需要使用 Xcode 6.1 和 iOS 8.1 SDK 來(lái)編譯它。
注意:本教程的代碼使用 AsyncDisplayKit 1.0 來(lái)編寫(xiě)。這個(gè)版本已經(jīng)被包含在初始項(xiàng)目中了。
你要研究的項(xiàng)目是由 UICollectionView 制作的卡片式界面來(lái)描述不同的雨林動(dòng)物。每張信息卡包括一個(gè)圖片、名字以及一個(gè)對(duì)雨林動(dòng)物的描述。卡片的背景圖是主圖片的模糊版。視覺(jué)設(shè)計(jì)的細(xì)節(jié)保證了文字的清晰易讀。

在 Xcode 中,打開(kāi)初始項(xiàng)目里的 Layers.xcworkspace 。
在本教程里,請(qǐng)遵循以下原則以體會(huì) AsyncDisplayKit 的那些十分吸引人的好處。
將應(yīng)用運(yùn)行在真機(jī)上。在模擬器里運(yùn)行很難看出性能改善。
應(yīng)用是通用的,但在 ipad 上看起來(lái)最好。
最后,要真正感激這個(gè)庫(kù)能為你所做的事情,請(qǐng)盡量在最舊的能運(yùn)行 iOS 8.1 的設(shè)備上運(yùn)行本應(yīng)用。第三代的 iPad 最好,因?yàn)樗m有視網(wǎng)膜屏幕,但運(yùn)行得不是很快。
一旦你選定了設(shè)備,那就編譯并運(yùn)行本項(xiàng)目。你會(huì)看到如下界面:

試著滑動(dòng) Collection View 并注意那可憐的幀率。在第三代 iPad 上,幀率大概只有 15-20 FPS,實(shí)在丟掉太多幀了。在本教程的最后,你能在 60 FPS (或非常接近)的幀率上滑動(dòng)它。
注意:你所看到的圖像都在 App 的 asset 目錄里,并不是從網(wǎng)絡(luò)上獲取的。
測(cè)量響應(yīng)速度
在一個(gè)舊項(xiàng)目中使用 AsyncDisplayKit 前,你應(yīng)該通過(guò) Instruments 測(cè)量你的 UI 的性能,這樣才有一個(gè)基準(zhǔn)線以便對(duì)比改動(dòng)的效果。
最重要的是,你要知道是 CPU-綁定 還是 GPU-綁定。也就是說(shuō),是 CPU 還是 GPU 拉低了應(yīng)用的幀率。這個(gè)信息會(huì)告訴你該充分利用 AsyncDisplayKit 的哪個(gè)特性以?xún)?yōu)化應(yīng)用的性能。
如果你有時(shí)間,看看之前提到的 WWDC 2012 session 和/或在真實(shí)設(shè)備上使用 Instruments 來(lái)評(píng)估初始項(xiàng)目的時(shí)間曲線。滑動(dòng)性能是 CPU-綁定 的。你能猜到是什么原因?qū)е铝?Collection View 丟掉這么多幀嗎?
丟幀是因?yàn)槟:?cell 的背景圖像時(shí)阻塞了主線程。
為項(xiàng)目準(zhǔn)備好使用 AsyncDisplayKit
在舊項(xiàng)目里使用 AsyncDisplayKit,歸結(jié)起來(lái)就是使用 Display Node 層次結(jié)構(gòu)替換視圖層次結(jié)構(gòu)和/或 Layer 樹(shù)。各種 Display Node 是 AsyncDisplayKit 的關(guān)鍵所在。它們位于視圖之上,而且是線程安全的,也就是說(shuō)之前在主線程才能執(zhí)行的任務(wù)現(xiàn)在也可以在非主線程執(zhí)行。這就能減輕主線程的工作量以執(zhí)行其他操 作,例如處理觸摸事件,或如在本應(yīng)用的情況里,處理 Collection View 的滑動(dòng)。
這就意味著在本教程里,你的第一步是移除視圖層次結(jié)構(gòu)。
移除視圖層次結(jié)構(gòu)
打開(kāi) RainforestCardCell.swift 并刪除 awakeFromNib() 中所有的 addSubview(...) 調(diào)用,然后得到如下:
1 2 3 4 5 6 | override func awakeFromNib() { super.awakeFromNib() contentView.layer.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColor contentView.layer.borderWidth = 1} |
接下來(lái),替換 layoutSubviews() 的內(nèi)容如下:
1 2 3 | override func layoutSubviews() { super.layoutSubviews()} |
再將 configureCellDisplayWithCardInfo(cardInfo:) 的內(nèi)容替換如下:
1 2 3 4 5 | func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size} |
刪除 RainforestCardCell 的所有視圖屬性,只留一個(gè)如下:
1 2 3 4 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? ...} |
最后,編譯并運(yùn)行,你看到的就全是空空如也的卡片:

現(xiàn)在所有的 cell 都空了,滑動(dòng)起來(lái)超級(jí)順滑。你的目標(biāo)是保證之后添加回取代各視圖的 node 后,滑動(dòng)依然順滑。
你可用 Instruments 的 Core Animation 模版在真機(jī)上檢測(cè)應(yīng)用的性能,看看你的改動(dòng)如何影響幀率。
添加一個(gè)占位圖
打開(kāi) RainforestCardCell.swift ,給 RainforestCardCell 添加一個(gè)可選的 CALayer 變量,名為 placeholderLayer:
1 2 3 4 5 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! ...} |
你之所以需要一個(gè)占位圖是因?yàn)轱@示會(huì)異步完成,如果這個(gè)過(guò)程需要些時(shí)間,那用戶就會(huì)看到空的 cell —— 這并不愉快。就如同如果你要從網(wǎng)絡(luò)上獲取圖像,那么就需要用占位圖來(lái)填充 cell,這能讓你的用戶知道內(nèi)容還沒(méi)有準(zhǔn)備好。雖然在我們這種情況里,你是在后臺(tái)線程繪制而不是從網(wǎng)絡(luò)下載。
在 awakeFromNib() 里,刪除 contentView 的 border 設(shè)置再創(chuàng)建并配置一個(gè) placeholderLayer。將其添加到 cell 的 contentView 的 Layer 上。現(xiàn)在這個(gè)方法如下:
1 2 3 4 5 6 7 8 9 10 | override func awakeFromNib() { super.awakeFromNib() placeholderLayer = CALayer() placeholderLayer.contents = UIImage(named: "cardPlaceholder")!.CGImage placeholderLayer.contentsGravity = kCAGravityCenter placeholderLayer.contentsScale = UIScreen.mainScreen().scale placeholderLayer.backgroundColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 1).CGColor contentView.layer.addSublayer(placeholderLayer)} |
在 layoutSubviews() 里,你需要布局 placeholderLayer。替換這個(gè)方法為:
1 2 3 4 5 | override func layoutSubviews() { super.layoutSubviews() placeholderLayer?.frame = bounds} |
編譯并運(yùn)行,你從虛無(wú)的邊緣回來(lái)了:

樸素的 CALayer 不是由 UIView 支持的,當(dāng)它們改變 frame 時(shí),默認(rèn)會(huì)有隱式動(dòng)畫(huà)。這就是為何你看到 layer 在布局時(shí)放大。要修復(fù)這個(gè)問(wèn)題,改動(dòng) layoutSubviews 如下:
1 2 3 4 5 6 7 8 | override func layoutSubviews() { super.layoutSubviews() CATransaction.begin() CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions) placeholderLayer?.frame = bounds CATransaction.commit()} |
編譯并運(yùn)行,問(wèn)題解決了。
現(xiàn)在占位圖不會(huì)亂動(dòng),不再動(dòng)畫(huà)它們的 frame 了。
第一個(gè) Node
重建 App 的第一步是給每一個(gè) UICollectionView cell 添加一個(gè)背景圖片 Node,步驟如下:
1. 創(chuàng)建、布局并添加一個(gè)圖像 Node 到 UICollectionView cell;
2. 處理 cell 重用 Node 和它們的 layer;以及
3. 模糊圖像 Node
但在做之前,打開(kāi) Layers-Bridging-Header.h 并導(dǎo)入 AsyncDisplayKit :
1 | #import |
這會(huì)讓所有的 Swift 文件都能訪問(wèn) AsyncDisplayKit 的各種類(lèi)。
編譯一下,確保沒(méi)有錯(cuò)誤。
方向:雨林 Collection View 結(jié)構(gòu)
現(xiàn)在,我們來(lái)看看 Collection View 的組成:
· View Controller :RainforestViewController 沒(méi)有什么花哨的東西。它只是為所有的雨林卡片獲取一個(gè)數(shù)據(jù)數(shù)組,并為 UICollectionView 實(shí)現(xiàn) Data Source。事實(shí)上,你不需要花太多時(shí)間到 View Controller 上。
· Data Source :大部分時(shí)間都將花在 cell 類(lèi) RainforestCardCell 上。View Controller 出隊(duì)每個(gè) cell 并將雨林卡片的數(shù)據(jù)用 configureCellDisplayWithCardInfo(cardInfo:) 傳給它。cell 就使用這個(gè)數(shù)據(jù)來(lái)配置自身。
· Cell :在 configureCellDisplayWithCardInfo(cardInfo:) 里,cell 創(chuàng)建、配置、布局以及添加 Node 到它自己身上。這就意味著每次 View Controller 出隊(duì)一個(gè) cell,這個(gè) cell 就會(huì)創(chuàng)建并添加給它自己一個(gè)新的 Node 層次結(jié)構(gòu)。
如果你使用 View 而不是 Node,那么這樣做對(duì)于性能來(lái)說(shuō)就不是最佳策略。但因?yàn)槟憧梢援惒降貏?chuàng)建、配置以及布局,而且 Node 也是異步地繪制的,所以這不會(huì)是一個(gè)問(wèn)題。真正的難點(diǎn)是在 cell 準(zhǔn)備重用時(shí)取消任何在進(jìn)行的異步操作并移除舊 Node 。
注意 :本教程的這個(gè)策略來(lái)添加 Node 到 cell 還算 OK。對(duì)于精通 AsyncDisplayKit 來(lái)說(shuō),這是很好的第一步。
然而,在實(shí)際生產(chǎn)中,你最好使用 ASRangeController 來(lái)緩存你的 Node,這樣你就不用每次在 cell 重用時(shí)重建它的 Node 層次結(jié)構(gòu)。ASRangeController 超出了本教程的范圍,但若你想了解更多的信息,看看頭文件 ASRangeController.h 的注釋吧。
再注意一下:1.1 版的 AsyncDisplayKit (本教程編寫(xiě)時(shí)還未放出,但會(huì)在此后不久放出)包含有 ASCollectionView。使用 ASCollectionView 會(huì)讓本 App 的整個(gè) Collection View 都由 Display Node 控制。而在本教程中,每個(gè) cell 會(huì)包含一個(gè) Display Node 層次結(jié)構(gòu)。如上面所解釋的,這能工作,但如果使用 ASCollectionView 可能會(huì)更好。給力的 ASCollectionView!
OK,該動(dòng)手了。
添加背景圖片 Node
現(xiàn)在你要走一遍用 Node 配置 cell 的過(guò)程,一次一步:
打開(kāi) RainforestCardCell.swift 并替換 configureCellDisplayWithCardInfo(cardInfo:) 為:
1 2 3 4 5 6 7 8 9 10 | func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size //MARK: Node Creation Section let backgroundImageNode = ASImageNode() backgroundImageNode.image = image backgroundImageNode.contentMode = .ScaleaspectFill} |
這就創(chuàng)建并配置了一個(gè) ASImageNode 常量,叫做 backgroundImageNode。
注意:確保包含 //MARK: 注釋?zhuān)@樣更容易看清代碼位置。
AsyncDisplayKit 帶有好幾種 Node 類(lèi)型,包括 ASImageNode,用于顯示圖片。它相當(dāng)于 UIImageView,除了 ASImageNode 是默認(rèn)異步地解碼圖片。
添加如下代碼到 configureCellDisplayWithCardInfo(cardInfo:) 底部:
1 | backgroundImageNode.layerBacked = true |
這讓 backgroundImageNode 變?yōu)?Layer 支持的 Node。
Node 可由 UIView 支持或 CALayer 支持。當(dāng) Node 需要處理事件時(shí)(例如觸摸事件),你就要使用 UIView 支持的 Node。如果你不需要處理事件,只需要顯示一下內(nèi)容,那使用 Layer 支持的 Node 會(huì)更加輕量,因此可以獲得一個(gè)小的性能提升。
因?yàn)楸窘坛痰?App 不需要處理事件,所以你可讓所有的 Node 都設(shè)置為 Layer 支持的。在上面的代碼中,由于 backgroundImageNode 為 Layer 支持的,AsyncDisplayKit 會(huì)創(chuàng)建一個(gè) CALayer 用于雨林動(dòng)物圖像內(nèi)容的顯示。
繼續(xù) configureCellDisplayWithCardInfo(cardInfo:) 并添加如下代碼:
1 2 | //MARK: Node Layout SectionbackgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size) |
這里使用 FrameCalculator 為 backgroundImageNode 布局。
FrameCalculator 是一個(gè)幫助類(lèi),它包裝了cell 的布局,為每個(gè) Node 返回 frame。注意所有的東西都是手動(dòng)布局的, 沒(méi)有使用 Auto Layout 約束 。如果你需要構(gòu)建自適應(yīng)布局或者本地化驅(qū)動(dòng)的布局,那就要注意,因?yàn)槟悴荒芙o Node 添加約束。
接下來(lái),添加如下代碼到 configureCellDisplayWithCardInfo(cardInfo:) 底部:
1 2 | //MARK: Node Layer and Wrap Up Sectionself.contentView.layer.addSublayer(backgroundImageNode.layer) |
這句將 backgroundImageNode 的 Layer 添加到 cell contentView 的 Layer 上。
注意,AsyncDisplayKit 會(huì)為 backgroundImageNode 創(chuàng)建一個(gè) Layer。然而,你必須要將 Node 放到某個(gè) Layer 樹(shù)中才能在屏幕上顯示。這個(gè) Node 會(huì)異步地繪制,所以直到繪制完成,它的內(nèi)容都不會(huì)顯示,盡管它的 Layer 已經(jīng)在一個(gè) Layer 樹(shù)中。
從技術(shù)角度來(lái)說(shuō), Layer 一直都存在。但渲染圖像是異步進(jìn)行的。Layer 初始化時(shí)沒(méi)有內(nèi)容(例如是透明的)。一旦渲染完成,Layer 的 contents 就會(huì)更新為包含圖像內(nèi)容。
在這個(gè)點(diǎn),cell 的 contentView 的 Layer 將會(huì)包含兩個(gè) Sublayer:一個(gè)占位圖和 Node 的 Layer。在 Node 完成繪制前,只有占位圖會(huì)顯示。
注意到 configureCellDisplayWithCardInfo(cardInfo:) 會(huì)在每次 cell 出隊(duì)時(shí)被調(diào)用。每次 cell 被回收,這個(gè)邏輯會(huì)添加一個(gè)新的 Sublayer 到 cell 的 contentView Layer 上。不要擔(dān)心,你很快會(huì)解決這個(gè)問(wèn)題。
回到 RainforestCardCell.swift 開(kāi)頭,給 RainforestCardCell 添加一個(gè) ASImageNode 變量存為屬性 backgroundImageNode,如下:
1 2 3 4 5 6 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? ///< ADD THIS LINE ...} |
你之所以需要這個(gè)屬性是因?yàn)楸仨氁心硞€(gè)東西將 backgroundImageNode 的引用保留住,否則 ARC 就會(huì)將其釋放,也就不會(huì)有任何東西顯示出來(lái)——即使 Node 的 Layer 在一個(gè) Layer 樹(shù)中,你依然需要保留 Node。
在 configureCellDisplayWithCardInfo(cardInfo:) 底部的 Node Layer and Wrap Up Section ,設(shè)置 cell 新的 backgroundImageNode 為之前的 backgroundImageNode:
1 | self.backgroundImageNode = backgroundImageNode |
下面是完整的 configureCellDisplayWithCardInfo(cardInfo:) 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size //MARK: Node Creation Section let backgroundImageNode = ASImageNode() backgroundImageNode.image = image backgroundImageNode.contentMode = .ScaleAspectFill backgroundImageNode.layerBacked = true //MARK: Node Layout Section backgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size) //MARK: Node Layer and Wrap Up Section self.contentView.layer.addSublayer(backgroundImageNode.layer) self.backgroundImageNode = backgroundImageNode} |
編譯并運(yùn)行,觀察 AsyncDisplayKit 是如何異步地使用圖像設(shè)置 Layer 的 contents 的。這能讓你在 CPU 還在繪制 Layer 的內(nèi)容的同時(shí)上下滑動(dòng)界面。

如果你運(yùn)行在舊設(shè)備上,注意圖像是如何彈出到位置——這是爆米花特效,但不總是讓人喜歡!本教程的最后一節(jié)會(huì)搞定這個(gè)不令人愉快的彈出效果,給你展示如何讓圖像自然地淡入,如同搖滾巨星。
如之前所討論的,新的 Node 會(huì)在每次 cell 被重用時(shí)創(chuàng)建。這并不很理想,因?yàn)檫@意味著新的 Layer 會(huì)在每次 cell 被重用時(shí)加入。
如果你想看看 Sublayer 堆積太多的影響,那就不停的滑上滑下多次,然后加斷點(diǎn)打印出 cell 的 contentView 的 Layer 的 sublayers 屬性。你會(huì)看到很多 Layer,這并不好。
處理 Cell 重用
繼續(xù) RainforestCardCell.swift ,給 RainforestCardCell 添加一個(gè)叫做 contentLayer 的 CALayer 屬性。這個(gè)屬性也是一個(gè)可選類(lèi)型:
1 2 3 4 5 6 7 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? var contentLayer: CALayer? ///< ADD THIS LINE ...} |
你將使用此屬性去移除 cell 的 contentView 的 Layer 樹(shù)中舊的 Node Layer。雖然你可以簡(jiǎn)單地保留 Node 并訪問(wèn)其 Layer 屬性,但上面的寫(xiě)法更加明確。
添加如下代碼到 configureCellDisplayWithCardInfo(cardInfo:) 結(jié)尾:
1 | self.contentLayer = backgroundImageNode.layer |
這句讓 backgroundImageNode 的 Layer 保留到 contentLayer 屬性。
替換 PRepareForReuse() 的實(shí)現(xiàn)如下:
1 2 3 4 | override func prepareForReuse() { super.prepareForReuse() backgroundImageNode?.preventOrCancelDisplay = true} |
因?yàn)?AsyncDisplayKit 能夠異步地繪制 Node,所以 Node 讓你能預(yù)防從頭繪制或取消任何在進(jìn)行的繪制。無(wú)論是你需要預(yù)防或取消繪制,都可將 preventOrCancelDisplay 設(shè)置為 true,如上面代碼所示。在本例中,你要在 cell 被重用前取消任何正在進(jìn)行的繪制活動(dòng)。
接下來(lái),添加如下代碼到 prepareForReuse() 尾部:
1 | contentLayer?.removeFromSuperlayer() |
這將 contentLayer 從其 Superlayer (也就是 contentView 的 Layer)中移除。
每次一個(gè) cell 被回收時(shí),這個(gè)代碼就移除 Node 的舊 Layer ,因而解決了堆積問(wèn)題。所以在任何時(shí)間,你的 Node 最多只有兩個(gè) Sublayer:占位圖和 Node 的 Layer。
接下來(lái)添加如下代碼到 prepareForReuse() 尾部:
1 2 | contentLayer = nilbackgroundImageNode = nil |
這確保 cell 釋放它們的引用,這樣如有必要,ARC 才好做清理工作。
編譯并運(yùn)行。這次,沒(méi)有 Sublayer 會(huì)堆積的問(wèn)題,且所有不必要的繪制都會(huì)被取消。

是時(shí)候來(lái)點(diǎn)兒模糊效果了,Baby,模糊哦。

模糊圖像
要模糊圖像,你要添加一個(gè)額外的步驟到圖像 Node 的顯示過(guò)程里。
繼續(xù) RainforestCardCell.swift ,在 configureCellDisplayWithCardInfo(cardInfo:) 的設(shè)置 backgroundImageNode.layerBacked 的后面,添加如下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | backgroundImageNode.imageModificationBlock = { input in if input == nil { return input } if let blurredImage = input.applyBlurWithRadius( 30, tintColor: UIColor(white: 0.5, alpha: 0.3), saturationDeltaFactor: 1.8, maskImage: nil, didCancel:{ return false }) { return blurredImage } else { return image }} |
ASImageNode 的 imageModificationBlock 給你一個(gè)機(jī)會(huì)在顯示之前去處理底層的圖像。這是非常實(shí)用的功能,它讓你能對(duì)圖像 Node 做一些操作,例如添加濾鏡等。
在上面的代碼里,你使用 imageModificationBlock 來(lái)為 cell 的背景圖像應(yīng)用模糊效果。關(guān)鍵點(diǎn)就是圖像 Node 將會(huì)繪制它的內(nèi)容并在后臺(tái)執(zhí)行這個(gè)閉包,而主線程依然順滑流暢。這個(gè)閉包接受原始的 UIImage 并返回一個(gè)修改過(guò)的 UIImage。
上面的代碼使用了 UIImage 的模糊 category,它由 Apple 在 WWDC 2013 提供,使用了 Accelerate framework 在 CPU 上模糊圖像。因?yàn)槟:龝?huì)消耗很多時(shí)間和內(nèi)存,這個(gè)版本的 category 被修改為包含了取消機(jī)制。這個(gè)模糊方法將定期調(diào)用 didCancel 閉包來(lái)決定是否應(yīng)該要停止模糊。
現(xiàn)在,上面的代碼給 didCancel 簡(jiǎn)單地返回 false。之后你會(huì)重寫(xiě) didCancel 閉包。
注意:還記得第一次運(yùn)行 App 時(shí) Collection View 那可憐的滑動(dòng)效果嗎?模糊方法阻塞了主線程。通過(guò)使用 AsyncDisplayKit 將模糊放入后臺(tái),你就大幅度地提高了 Collection View 的滑動(dòng)性能。簡(jiǎn)直天壤之別。
編譯并運(yùn)行,觀察模糊效果:

注意你可以如何非常流暢地滑動(dòng) Collection View。
當(dāng) Collection View 出隊(duì)一個(gè) cell 時(shí),一個(gè)模糊操作將開(kāi)始于后臺(tái)線程。當(dāng)用戶快速滑動(dòng)時(shí),Collection View 會(huì)重用每個(gè) cell 多次,并開(kāi)始許多模糊操作。我們的目標(biāo)是在 cell 準(zhǔn)備被重用時(shí)取消正在進(jìn)行中的模糊操作。
你已經(jīng)在 prepareForReuse() 里取消了 Node 的繪制操作 ,但一旦控制被移交給處理你圖像修改的閉包,那就是你的責(zé)任來(lái)處理 Node 的 preventOrCancelDisplay 設(shè)置,你現(xiàn)在就要做。
取消模糊操作
要取消進(jìn)行中的模糊操作,你需要實(shí)現(xiàn)模糊方法的 didCancel 閉包。
添加一個(gè)捕捉列表到 imageModificationBlock 以捕捉一個(gè) backgroundImageNode 的 weak 引用:
1 2 3 | backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input in ...} |
你需要 weak 引用來(lái)避免閉包和圖像 Node 之間的保留環(huán)問(wèn)題。你將使用這個(gè) weak backgroundImageNode 來(lái)確定是否要取消模糊操作。
是時(shí)候構(gòu)建模糊取消閉包了。添加下面代碼到 imageModificationBlock:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input in if input == nil { return input } // ADD FROM HERE... let didCancelBlur: () -> Bool = { var isCancelled = true // 1 if let strongBackgroundImageNode = backgroundImageNode { // 2 let isCancelledClosure = { isCancelled = strongBackgroundImageNode.preventOrCancelDisplay } // 3 if NSThread.isMainThread() { isCancelledClosure() } else { dispatch_sync(dispatch_get_main_queue(), isCancelledClosure) } } return isCancelled } // ...TO HERE ...} |
下面解釋一下這些代碼:
1. 得到 backgroundImageNode 的 strong 引用,準(zhǔn)備用其干活。如果 backgroundImageNode 在本次運(yùn)行時(shí)消失,那么 isCancelled 將保持為 true,然后模糊操作會(huì)被取消。如果沒(méi)有 Node 需要顯示,自然沒(méi)有必要繼續(xù)模糊操作。
2. 在此你將操作取消檢查包在閉包里,因?yàn)橐坏?Node 創(chuàng)建它的 Layer 或 View,那就只能在主線程訪問(wèn) Node 的屬性。由于你需要訪問(wèn) preventOrCancelDisplay,所以你必須在主線程檢查。
3. 最后,確保 isCancelledClosure 是在主線程運(yùn)行,無(wú)論是已在主線程而直接運(yùn)行,還是不在主線程而通過(guò) dispatch_sync 來(lái)調(diào)度。它必須是一個(gè)同步的調(diào)度,因?yàn)槲覀冃枰]包完成,并在 didCancelBlur 閉包返回之前設(shè)置 isCancelled。
在調(diào)用 applyBlurWithRadius(...) 中,修改傳遞給 didCancel 的參數(shù),替換一直返回 false 的閉包為你剛才定義并保留在 didCancelBlur 的閉包。
1 2 3 4 5 6 7 8 | if let blurredImage = input.applyBlurWithRadius( 30, tintColor: UIColor(white: 0.5, alpha: 0.3), saturationDeltaFactor: 1.8, maskImage: nil, didCancel: didCancelBlur) { ...} |
編譯并運(yùn)行。你看你不會(huì)注意到太多差別,但現(xiàn)在任何在 cell 離開(kāi)屏幕時(shí)還未完成的模糊都會(huì)被取消了。這就意味著設(shè)備比之前做得更少。你可能觀察到輕微的性能提升,特別是在較慢的設(shè)備如第三代 iPad 上運(yùn)行時(shí)。

當(dāng)然,若沒(méi)有東西在前面,背景就不是真正的背景!你的卡片需要內(nèi)容。通過(guò)下面四個(gè)小節(jié),你將學(xué)會(huì):
· 創(chuàng)建一個(gè)容器 Node,它將所有的 Subnode 繪制到一個(gè)單獨(dú)的 CALayer 里;
· 構(gòu)建一個(gè) Node 層次結(jié)構(gòu);
· 創(chuàng)建一個(gè)自定義的 ASDisplayNode 子類(lèi);并
· 在后臺(tái)構(gòu)建并布局 Node 層次結(jié)構(gòu);
做完這些,你就會(huì)得到一個(gè)看起來(lái)和添加 AsyncDisplayKit 之前一樣的 App,但有著黃油般順滑的滑動(dòng)體驗(yàn)。
柵格化的容器 Node
直到現(xiàn)在,你一直在操作 cell 內(nèi)的一個(gè)單獨(dú)的 Node。接下來(lái),你將創(chuàng)建一個(gè)容器 Node,它會(huì)包含所有的卡片內(nèi)容。
添加一個(gè)容器 Node
繼續(xù) RainforestCardCell.swift ,在 configureCellDisplayWithCardInfo(cardInfo:) 的 backgroundImageNode.imageModificationBlock 后面以及 Node Layout Section 前面添加如下代碼:
1 2 3 4 5 6 | //MARK: Container Node Creation Sectionlet containerNode = ASDisplayNode()containerNode.layerBacked = truecontainerNode.shouldRasterizeDescendants = truecontainerNode.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColorcontainerNode.borderWidth = 1 |
這就創(chuàng)建并配置了一個(gè)叫做 containerNode 的 ASDisplayNode 常量。注意這個(gè)容器的 shouldRasterizeDescendants,這是一個(gè)關(guān)于節(jié)點(diǎn)如何工作的提示以及一個(gè)如何讓它們工作得更好地機(jī)會(huì)。
如單詞 “descendants(子孫)” 所暗示的,你可以創(chuàng)建 AsyncDisplayKit Node 的層次結(jié)構(gòu)或樹(shù),就如你可以創(chuàng)建 Core Animation Layer 的層次結(jié)構(gòu)一樣。例如,如果你有一個(gè)都是 Layer 支持的 Node 層次結(jié)構(gòu),那么 AsyncDisplayKit 將會(huì)為每個(gè) Node 創(chuàng)建一個(gè)分離的 CALayer,Layer 層次結(jié)構(gòu)將會(huì)和 Node 層次結(jié)構(gòu)一樣,如同鏡像。
這聽(tīng)起來(lái)很熟悉:它類(lèi)似于當(dāng)你使用普通的 UIKit 時(shí),Layer 層次結(jié)構(gòu)鏡像于 View 層次結(jié)構(gòu)。然而,這個(gè) Layer 的棧有一些不同的效果:
首先,因?yàn)槭钱惒戒秩荆憔筒粫?huì)看到每個(gè) Layer 一個(gè)接一個(gè)地顯示。當(dāng) AsyncDisplayKit 繪制完成每個(gè) Layer,它馬上制作 Layer 的顯示內(nèi)容。所以如果你有一個(gè) Layer 的繪制比其他 Layer 耗時(shí)更長(zhǎng),那么它將會(huì)在它們之后顯示。用戶會(huì)看到零碎的 Layer 組件,這個(gè)過(guò)程通常是不可見(jiàn)的,因?yàn)?Core Animation 會(huì)在顯示任何東西之前重繪所有必須的 Layer 。
第二,有許多 Layer 能夠引起性能問(wèn)題。每個(gè) CALayer 都需要一個(gè)支持存儲(chǔ)來(lái)保存它的像素位圖和內(nèi)容。同樣,Core Animation 必須將每個(gè) Layer 通過(guò) XPC 發(fā)給渲染服務(wù)器。最后,渲染服務(wù)器可能需要重繪一些 Layer 以復(fù)合它們,例如在混合 Layer 時(shí)。總的來(lái)說(shuō),更多的 Layer 意味著 Core Animation 更多的工作。所以限制 Layer 使用的數(shù)量有許多不同的好處。
為了解決這個(gè)問(wèn)題,AsyncDisplayKit 有一個(gè)方便的特性:它允許你繪制一個(gè) Node 層次結(jié)構(gòu)到一個(gè)單獨(dú)的 Layer 容器里。這就是 shouldRasterizeDescendants 所做的。當(dāng)你設(shè)置它,那在完成所有的 Subnode 的繪制之前,ASDisplayNode 將不會(huì)設(shè)置 Layer 的 contents。
所以在之前的步驟里,設(shè)置容器 Node 的 shouldRasterizeDescendants 為 true 有兩個(gè)好處:
1. 它確保卡片一次顯示所有的 Node,如同舊的同步繪制;
2. 而且它通過(guò)柵格化 Layer 棧為單個(gè) Layer 并較少未來(lái)的合成而提高了效率。
不足之處是,由于你將所有的 Layer 放入一個(gè)位圖,你就不能在之后單獨(dú)動(dòng)畫(huà)某個(gè) Node 了。
要獲得更多信息,請(qǐng)看 shouldRasterizeDescendants 在頭文件 ASDisplayNode.h 里的注釋。
接下來(lái),在 Container Node Creation Section 后,添加 backgroundImageNode 為 containerNode 的 Subnode:
1 2 | //MARK: Node Hierarchy SectioncontainerNode.addSubnode(backgroundImageNode) |
注意:添加 Node 的順序很重要,就如同 subview 和 sublayer。最先添加的 Node 會(huì)被之后添加的阻擋顯示。
替換 Node Layout Section 的第一行為:
1 2 | //MARK: Node Layout SectioncontainerNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size) |
最后,使用 FrameCalculator 布局 backgroundImageNode:
1 2 | backgroundImageNode.frame = FrameCalculator.frameForBackgroundImage( containerBounds: containerNode.bounds) |
這設(shè)置 backgroundImageNode 填滿整個(gè) containerNode。
你幾乎完成了新的 Node 層次結(jié)構(gòu),但首先你需要正確地設(shè)置 Layer 層次結(jié)構(gòu),因?yàn)槿萜?Node 現(xiàn)在是根。
管理容器 Node 的 Layer
在 Node Layer and Wrap Up Section ,將 backgroundImageNode 的 Layer 添加到 containerNode 的 Layer 上而不是 contentView 的 Layer 上:
1 2 3 4 | // Replace the following line...// self.contentView.layer.addSublayer(backgroundImageNode.layer)// ...with this line:self.contentView.layer.addSublayer(containerNode.layer) |
刪除下面的 backgroundImageNode 保留:
1 | self.backgroundImageNode = backgroundImageNode |
因?yàn)?cell 只需要單獨(dú)保留容器 Node ,所以你要移除 backgroundImageNode 屬性。
不再設(shè)置 cell 的 contentLayer 屬性為 backgroundImageNode 的 Layer,現(xiàn)在將其設(shè)置為 containerNode 的 Layer:
1 2 3 4 | // Replace the following line...// self.contentLayer = backgroundImageNode.layer// ...with this line:self.contentLayer = containerNode.layer |
給 RainforestCardCell 添加一個(gè)可選的 ASDisplayNode 實(shí)例存儲(chǔ)為屬性 containerNode:
1 2 3 4 5 6 7 8 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? var contentLayer: CALayer? var containerNode: ASDisplayNode? ///< ADD THIS LINE ...} |
記住你需要保留你自己的 Node ,如果你不這么做它們就會(huì)被立即釋放。
回到 configureCellDisplayWithCardInfo(cardInfo:),在 Node Layer and Wrap Up Section 最后,設(shè)置 containerNode 屬性為 containerNode 常量:
1 | self.containerNode = containerNode |
編譯并運(yùn)行。模糊的圖像將會(huì)再此顯示!但還有最后一件事要去改變,因?yàn)楝F(xiàn)在有了新的 Node 層次結(jié)構(gòu)。回憶之前 cell 重用時(shí)你將圖像停止顯示。現(xiàn)在你需要讓整個(gè) Node 層次結(jié)構(gòu)停止顯示。
在新的 Node 層次結(jié)構(gòu)上處理 Cell 重用
繼續(xù) RainforestCardCell.swift ,在 prepareForReuse() 里,替換設(shè)置 backgroundImageNode.preventOrCancelDisplay 為在 containerNode 上調(diào)用 recursiveSetPreventOrCancelDisplay(...) 并傳遞 true:
1 2 3 4 5 6 7 8 9 10 11 | override func prepareForReuse() { super.prepareForReuse() // Replace this line... // backgroundImageNode?.preventOrCancelDisplay = true // ...with this line: containerNode?.recursiveSetPreventOrCancelDisplay(true) contentLayer?.removeFromSuperlayer() ...} |
當(dāng)你要取消整個(gè) Node 層次結(jié)構(gòu)的繪制,就使用 recursiveSetPreventOrCancelDisplay()。這個(gè)方法將會(huì)設(shè)置這個(gè) Node 以及其所有子 Node 的 preventOrCancelDisplay 屬性,無(wú)論 true 或 false。
接下來(lái),依然在 prepareForReuse(),用設(shè)置 containerNode 為 nil 替換設(shè)置 backgroundImageNode 為 nil:
1 2 3 4 5 6 7 8 9 | override func prepareForReuse() { ... contentLayer = nil // Replace this line... // backgroundImageNode = nil // ...with this line: containerNode = nil} |
移除 RainforestCardCell 的 backgroundImageNode 屬性:
1 2 3 4 5 6 7 8 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! // var backgroundImageNode: ASImageNode? ///< REMOVE THIS LINE var contentLayer: CALayer? var containerNode: ASDisplayNode? ...} |
編譯并運(yùn)行。這個(gè) App 就如之前一樣,但現(xiàn)在你的圖像 Node 在容器 Node 內(nèi),而重用依然和它應(yīng)有的方式一樣。

Cell 內(nèi)容
目前為止你有了一個(gè) Node 層次結(jié)構(gòu),但容器內(nèi)還只有一個(gè) Node——圖像 Node。現(xiàn)在是時(shí)候設(shè)置 Node 層次結(jié)構(gòu)去復(fù)制在添加 AsyncDisplayKit 之前時(shí)應(yīng)用的視圖層次結(jié)構(gòu)了。這意味著添加 text 和一個(gè)未模糊的特征圖像。
添加特征圖像
我們要添加特征圖像了,它是一個(gè)未模糊的圖像,顯示在卡片的頂部。
打開(kāi) RainforestCardCell.swift 并找到 configureCellDisplayWithCardInfo(cardInfo:)。在 Node Creation Section 的底部,添加如下代碼:
1 2 3 4 | let featureImageNode = ASImageNode()featureImageNode.layerBacked = truefeatureImageNode.contentMode = .ScaleAspectFitfeatureImageNode.image = image |
這會(huì)創(chuàng)建并配置一個(gè)叫做 featureImageNode 的 ASImageNode 常量。它被設(shè)置為 Layer 支持的,放大以適用,并設(shè)置顯示圖像,這次不需要模糊。
在 Node Hierarchy Section 的最后,添加 featureImageNode 為 containerNode 的 Subnode:
1 | containerNode.addSubnode(featureImageNode) |
你正在用更多 Node 填充容器哦!
在 Node Layout Section ,使用 FrameCalculator 布局 featureImageNode:
1 2 3 | featureImageNode.frame = FrameCalculator.frameForFeatureImage( featureImageSize: image.size, containerFrameWidth: containerNode.frame.size.width) |
編譯并運(yùn)行。你就會(huì)看到特征圖像在卡片的頂部出現(xiàn),位于模糊圖像的上方。注意特征圖像和模糊圖像是如何在同一時(shí)間跳出。這是你之前添加的 shouldRasterizeDescendants 在起作用。

添加 Title 文本
接下來(lái)添加文字 Label,以顯示動(dòng)物的名字和描述。首先來(lái)動(dòng)物名字吧。
繼續(xù) configureCellDisplayWithCardInfo(cardInfo:),找到 Node Creation Section 。添加下列代碼到這節(jié)尾部,就在創(chuàng)建 featureImageNode 之后:
1 2 3 4 | let titleTextNode = ASTextNode()titleTextNode.layerBacked = truetitleTextNode.backgroundColor = UIColor.clearColor()titleTextNode.attributedString = NSAttributedString.attributedStringForTitleText(cardInfo.name) |
這就創(chuàng)建了一個(gè)叫做 titleTextNode 的 ASTextNode 常量。
ASTextNode 是另一個(gè) AsyncDisplayKit 提供的 Node 子類(lèi),其用于顯示文本。它是一個(gè)具有 UILabel 效果的 Node。它接受一個(gè) attributedString,由 TextKit 支持,有許多特性如文本鏈接。要學(xué)到更多關(guān)于這個(gè) Node 的功能,去看 ASTextNode.h 吧。
初始項(xiàng)目包含有一個(gè) NSAttributedString 的擴(kuò)展,它提供了一個(gè)工廠方法去生成一個(gè)屬性字符串用于 Title 和 Description 文本以顯示在雨林卡片上。上面的代碼使用了這個(gè)擴(kuò)展的 attributedStringForTitleText(...) 方法。
現(xiàn)在,在 Node Hierarchy Section 底部,添加如下代碼:
1 | containerNode.addSubnode(titleTextNode) |
這就添加了 titleTextNode 到 Node 層次結(jié)構(gòu)里。它將位于特征圖像和背景圖像之上,因?yàn)樗谒鼈冎筇砑印?/p>
在 Node Layout Section 底部添加如下代碼:
1 2 3 | titleTextNode.frame = FrameCalculator.frameForTitleText( containerBounds: containerNode.bounds, featureImageFrame: featureImageNode.frame) |
一樣使用 FrameCalculator 布局 titleTextNode,就像 backgroundImageNode 和 featureImageNode 那樣。
編譯并運(yùn)行。你就有了一個(gè) Title 顯示在特征圖像的頂部。再次說(shuō)明, Label 只會(huì)在整個(gè) cell 準(zhǔn)備好渲染時(shí)才渲染。

添加 Description 文本
添加一個(gè)有著 Description 文本的 Node 和添加 Title 文本的 Node 類(lèi)似。
回到 configureCellDisplayWithCardInfo(cardInfo:) ,在 Node Creation Section 最后,添加如下代碼。就在之前創(chuàng)建 titleTextNode 的語(yǔ)句之后:
1 2 3 4 5 | let descriptionTextNode = ASTextNode()descriptionTextNode.layerBacked = truedescriptionTextNode.backgroundColor = UIColor.clearColor()descriptionTextNode.attributedString = NSAttributedString.attributedStringForDescriptionText(cardInfo.description) |
這就創(chuàng)建并配置了一個(gè)叫做 descriptionTextNode 的 ASTextNode 實(shí)例。
在 Node Hierarchy Section 最后,添加 descriptionTextNode 到 containerNode:
1 | containerNode.addSubnode(descriptionTextNode) |
在 Node Layout Section ,一樣使用 FrameCalculator 布局 descriptionTextNode:
1 2 3 | descriptionTextNode.frame = FrameCalculator.frameForDescriptionText( containerBounds: containerNode.bounds, featureImageFrame: featureImageNode.frame) |
編譯并運(yùn)行。現(xiàn)在你能看到 Description 文本了。

Custom Node Subclasses 自定義 Node 子類(lèi)
目前為止,你使用了 ASImageNode 和 ASTextNode。這會(huì)帶你走很遠(yuǎn),但有些時(shí)候你需要你自己的 Node,就如同某些時(shí)候在傳統(tǒng)的 UIKit 編程里你需要自己的 View 一樣。
創(chuàng)建梯度 Node 類(lèi)
接下來(lái),你將給 GradientView.swift 添加 Core Graphics 代碼來(lái)構(gòu)建一個(gè)自定義的梯度 Display Node。這會(huì)被用于創(chuàng)建一個(gè)繪制梯度的自定義 Node 。梯度圖會(huì)顯示在特征圖像的底部以便讓 Title 看起來(lái)更加明顯。
打開(kāi) Layers-Bridging-Header.h 并添加如下代碼:
1 | #import |
需這一步是因?yàn)檫@個(gè)類(lèi)沒(méi)有包含在庫(kù)的主頭文件里。你在子類(lèi)化任何 ASDisplayNode 或 _ASDisplayLayer 時(shí)都需要訪問(wèn)這個(gè)類(lèi)。
菜單 File/New/File… 。選擇 iOS/Source/Cocoa Touch Class 。命名類(lèi)為 GradientNode 并使其作為 ASDisplayNode 的子類(lèi)。選擇 Swift 語(yǔ)言并點(diǎn)擊 Next 。保存文件再打開(kāi) GradientNode.swift 。
添加如下方法到這個(gè)類(lèi):
1 2 3 4 | class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!, isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) {} |
如同 UIView 或 CALayer,你可以子類(lèi)化 ASDisplayNode 去做自定義繪制。你可以使用如同用于 UIView 的 Layer 或單獨(dú)的 CALayer 的繪制代碼,這取決于客戶 Node 如何配置 Node。查看 ASDisplayNode+Subclasses.h 獲取更多關(guān)于子類(lèi)化 ASDisplayNode 的信息。
進(jìn)一步,ASDisplayNode 的繪制方法比在 UIView 和 CALayer 里的接受更多參數(shù),給你提供方法少做工作,并更有效率。
要為你的自定義 Display Node 填充內(nèi)容,你需要實(shí)現(xiàn)來(lái)自 _ASDisplayLayerDelegate 協(xié)議的 drawRect(...) 或 displayWithParameters(...)。在繼續(xù)之前,看看 _ASDisplayLayer.h 得到這個(gè)方法和它們參數(shù)的信息。搜索 _ASDisplayLayerDelegate。重點(diǎn)看看頭文件注釋里關(guān)于 drawRect(...) 的描述。
因?yàn)樘荻葓D位于特征圖的上方,使用 Core Graphics 繪制,所以你需要使用 drawRect(...) 。
打開(kāi) GradientView.swift 并拷貝 drawRect(...) 的內(nèi)容到 GradientNode.swift 的 drawRect(...),如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!, isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) { let myContext = UIGraphicsGetCurrentContext() CGContextSaveGState(myContext) CGContextClipToRect(myContext, bounds) let componentCount: UInt = 2 let locations: [CGFloat] = [0.0, 1.0] let components: [CGFloat] = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0] let myColorSpace = CGColorSpaceCreateDeviceRGB() let myGradient = CGGradientCreateWithColorComponents(myColorSpace, components, locations, componentCount) let myStartPoint = CGPoint(x: bounds.midX, y: bounds.maxY) let myEndPoint = CGPoint(x: bounds.midX, y: bounds.midY) CGContextDrawLinearGradient(myContext, myGradient, myStartPoint, myEndPoint, UInt32(kCGGradientDrawsAfterEndLocation)) CGContextRestoreGState(myContext)} |
然后刪除 GradientView.swift,編譯并確保沒(méi)有錯(cuò)誤。
添加梯度 Node
打開(kāi) RainforestCardCell.swift 并找到 configureCellDisplayWithCardInfo(cardInfo:)。在 Node Creation Section 底部,添加如下代碼,就在創(chuàng)建 descriptionTextNode 的代碼之后:
1 2 3 | let gradientNode = GradientNode()gradientNode.opaque = falsegradientNode.layerBacked = true |
這就創(chuàng)建了一個(gè)叫做 gradientNode 的 GradientNode 常量。
在 Node Hierarchy Section,在添加 featureImageNode 那樣下面,添加 gradientNode 到 containerNode:
1 2 3 4 5 6 | //MARK: Node Hierarchy SectioncontainerNode.addSubnode(backgroundImageNode)containerNode.addSubnode(featureImageNode)containerNode.addSubnode(gradientNode) ///< ADD THIS LINEcontainerNode.addSubnode(titleTextNode)containerNode.addSubnode(descriptionTextNode) |
梯度 Node 需要這個(gè)位置才能在特征圖之上,Title 之下。
然后添加如下代碼到 Node Layout Section 底部:
1 2 | gradientNode.frame = FrameCalculator.frameForGradient( featureImageFrame: featureImageNode.frame) |
編譯并運(yùn)行。你將看到梯度在特征圖的底部。Title 確實(shí)看得更清楚了!

爆米花特效
如之前提到的,cell 的 Node 內(nèi)容會(huì)在完成繪制時(shí)“彈出”。這不是很理想。所以讓我們繼續(xù),以修復(fù)這個(gè)問(wèn)題。但首先,更加深入 AsyncDisplayKit 以看看它是如何工作的。
在 configureCellDisplayWithCardInfo(cardInfo:) 的 Container Node Creation Section ,關(guān)閉容器 Node 的 shouldRasterizeDescendants:
1 | containerNode.shouldRasterizeDescendants = false |
編譯并運(yùn)行。你會(huì)注意到現(xiàn)在容器層次結(jié)構(gòu)里不同的 Node 一個(gè)接一個(gè)的彈出。你會(huì)看到文字彈出,然后是特征圖,然后是模糊背景圖。
當(dāng) shouldRasterizeDescendants 關(guān)閉后,AsyncDisplayKit 就不是繪制一個(gè)容器 Layer 了,它會(huì)創(chuàng)建一個(gè)鏡像卡片 Node 層次結(jié)構(gòu)的 Layer 樹(shù)。記得爆米花特效存在是因?yàn)槊總€(gè) Layer 都在它繪制結(jié)束后立即出現(xiàn),而某些 Layer 比另外一個(gè)花費(fèi)更多時(shí)間在繪制上。
這不是我們所需要的,但它描述了 AsyncDisplayKit 的工作方式。我們不想要這個(gè)行為,所以還是將 shouldRasterizeDescendants 打開(kāi):
1 | containerNode.shouldRasterizeDescendants = true |
編譯并運(yùn)行。又回到整個(gè) cell 在其渲染結(jié)束后彈出了。
該重新思考如何擺脫爆米花特效了。但首先,讓我們看看 Node 在后臺(tái)如何構(gòu)造。
在后臺(tái)構(gòu)造 Node
除了異步地繪制,使用 AsyncDisplayKit,你同樣可以異步地創(chuàng)建、配置以及布局。深呼吸一下,因?yàn)檫@就是你接下來(lái)要做的事情。
創(chuàng)建一個(gè) Node 構(gòu)造操作(Operation)
你要將 Node 層次結(jié)構(gòu)的構(gòu)造包裝到一個(gè) NSOperation 中。這樣做很棒,因?yàn)檫@個(gè)操作能很容易的在不同的操作隊(duì)列上執(zhí)行,包括后臺(tái)隊(duì)列。
打開(kāi) RainforestCardCell.swift 。然后添加如下方法:
1 2 3 4 5 6 7 | func nodeConstructionOperationWithCardInfo(cardInfo: RainforestCardInfo, image: UIImage) -> NSOperation { let nodeConstructionOperation = NSBlockOperation() nodeConstructionOperation.addExecutionBlock { // TODO: Add node hierarchy construction } return nodeConstructionOperation} |
繪制并不是唯一會(huì)拖慢主線程的操作。對(duì)于復(fù)雜的屏幕,布局計(jì)算也有可能變的昂貴。目前為止,本教程當(dāng)前狀態(tài)的項(xiàng)目,一個(gè)緩慢的 Node 布局會(huì)引起 Collection View 丟幀。
60 FPS 意味著你有大約 17ms 的時(shí)間讓你的 cell 準(zhǔn)備好顯示,否則一個(gè)或多個(gè)幀就會(huì)被丟掉。這在 Table View 和 Collection View 有很復(fù)雜的 cell 時(shí)是非常常見(jiàn)的,滑動(dòng)時(shí)丟幀就是這個(gè)原因。
AsyncDisplayKit 前來(lái)救援!
你將使用上面的 nodeConstructionOperation 將所有 Node 層次結(jié)構(gòu)構(gòu)造以及布局從主線程剝離并放入后臺(tái) NSOperationQueue,進(jìn)一步確保 Collection View 能盡量以接近 60 FPS 的幀率滑動(dòng)。
警告:你可以在后臺(tái)訪問(wèn)并設(shè)置 Node 的屬性,但只能在 Node 的 Layer 或 View 被創(chuàng)建之前,也就是當(dāng)你第一次訪問(wèn) Node 的 Layer 或 View 屬性時(shí)。
一旦 Node 的 Layer 或 View 被創(chuàng)建,你必須在主線程才能訪問(wèn)和設(shè)置 Node 的屬性,因?yàn)?Node 將會(huì)轉(zhuǎn)發(fā)這些調(diào)用到它的 Layer 或 View。如果你得到一個(gè)崩潰 log 說(shuō)“Incorrect display node thread affinity”,那就意味著在創(chuàng)建 Node 的 Layer 或 View 之后,你依然嘗試在后臺(tái)訪問(wèn)或設(shè)置 Node 的屬性。
修改 nodeConstructionOperation 操作 Block 的內(nèi)容如下:
1 2 3 4 5 6 7 8 9 | nodeConstructionOperation.addExecutionBlock { [weak self, unowned nodeConstructionOperation] in if nodeConstructionOperation.cancelled { return } if let strongSelf = self { // TODO: Add node hierarchy construction }} |
在這個(gè)操作運(yùn)行時(shí),cell 可能已經(jīng)被釋放了。在那種情況下,你不需要做任何工作。類(lèi)似的,如果操作被取消了,那一樣也沒(méi)有工作要做了。
之所以對(duì) nodeConstructionOperation` 使用 unowned 引用是為了避免在操作和執(zhí)行閉包之間產(chǎn)生保留環(huán)。
現(xiàn)在找到 configureCellDisplayWithCardInfo(cardInfo:)。將任何在 Image Size Section 之后的代碼移動(dòng)到 nodeConstructionOperation 的執(zhí)行閉包里。將代碼放在 strongSelf 的條件語(yǔ)句里,即TODO的位置。之后 configureCellDisplayWithCardInfo(cardInfo:) 將看起來(lái)如下:
1 2 3 4 5 | func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size} |
目前,你會(huì)有一些編譯錯(cuò)誤。這是因?yàn)椴僮?Block 里的 self 是 weak 引用,因此是可選的。但你有一個(gè) self 的 strong 引用,因?yàn)榇a在可選綁定語(yǔ)句內(nèi)。所以替換錯(cuò)誤的幾行成下面的樣子:
1 2 3 | strongSelf.contentView.layer.addSublayer(containerNode.layer)strongSelf.contentLayer = containerNode.layerstrongSelf.containerNode = containerNode |
最后,添加如下代碼到你剛改動(dòng)的三行之下:
1 | containerNode.setNeedsDisplay() |
編譯確保沒(méi)有錯(cuò)誤。如果你現(xiàn)在運(yùn)行,那么只有占位圖會(huì)顯示,因?yàn)?Node 的創(chuàng)建操作還沒(méi)有實(shí)際使用。讓我們來(lái)添加它。
使用 Node 創(chuàng)建操作
打開(kāi) RainforestCardCell.swift 并添加如下屬性:
1 2 3 4 5 6 7 8 9 | class RainforestCardCell: UICollectionViewCell { var featureImageSizeOptional: CGSize? var placeholderLayer: CALayer! var backgroundImageNode: ASImageNode? var contentLayer: CALayer? var containerNode: ASDisplayNode? var nodeConstructionOperation: NSOperation? ///< ADD THIS LINE ...} |
這就添加了一個(gè)叫做 nodeConstructionOperation 的可選屬性
當(dāng) cell 準(zhǔn)備回收時(shí),你會(huì)使用這個(gè)屬性去取消 Node 的構(gòu)造。這會(huì)在用戶非常快速地滑動(dòng) Collection View 時(shí)發(fā)生,特別是如果布局還需要一些計(jì)算時(shí)間的話。
在 prepareForReuse() 添加如下指示的代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | override func prepareForReuse() { super.prepareForReuse() // ADD FROM HERE... if let operation = nodeConstructionOperation { operation.cancel() } // ...TO HERE containerNode?.recursiveSetPreventOrCancelDisplay(true) contentLayer?.removeFromSuperlayer() contentLayer = nil containerNode = nil} |
這就在 cell 重用時(shí)取消了操作,所以如果 Node 創(chuàng)建還沒(méi)完成,它也不會(huì)完成。
現(xiàn)在找到 configureCellDisplayWithCardInfo(cardInfo:) 并添加如下指示的代碼:
1 2 3 4 5 6 7 8 9 10 11 | func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) { // ADD FROM HERE... if let oldNodeConstructionOperation = nodeConstructionOperation { oldNodeConstructionOperation.cancel() } // ...TO HERE //MARK: Image Size Section let image = UIImage(named: cardInfo.imageName)! featureImageSizeOptional = image.size} |
這個(gè) cell 現(xiàn)在會(huì)在它準(zhǔn)備重用并開(kāi)始配置時(shí),取消任何進(jìn)行中的 Node 構(gòu)造操作。這確保了操作被取消,即使 cell 在準(zhǔn)備好重用前就被重新配置。
編譯并確保沒(méi)有錯(cuò)誤。
在主線程運(yùn)行
AsyncDisplayKit 允許你在非主線程做許多工作。但當(dāng)它要面對(duì) UIKit 和 CoreAnimation 時(shí),你還是需要在主線程做。目前為止,你從主線程移走了所有的 Node 創(chuàng)建。但還有一件事需要被放在主線程——即設(shè)置 CoreAnimation 的 Layer 層次結(jié)構(gòu)。
在 RainforestCardCell.swift 里,找到 nodeConstructionOperationWithCardInfo(cardInfo:image:) 并替換 Node Layer and Wrap Up Section 為如下代碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // 1dispatch_async(dispatch_get_main_queue()) { [weak nodeConstructionOperation] in if let strongNodeConstructionOperation = nodeConstructionOperation { // 2 if strongNodeConstructionOperation.cancelled { return } // 3 if strongSelf.nodeConstructionOperation !== strongNodeConstructionOperation { return } // 4 if containerNode.preventOrCancelDisplay { return } // 5 //MARK: Node Layer and Wrap Up Section strongSelf.contentView.layer.addSublayer(containerNode.layer) containerNode.setNeedsDisplay() strongSelf.contentLayer = containerNode.layer strongSelf.containerNode = containerNode }} |
下面描述一下:
1. 回憶到當(dāng) Node 的 Layer 屬性被第一個(gè)訪問(wèn)時(shí),所有的 Layer 會(huì)被創(chuàng)建。這就是為何你必須運(yùn)行 Node Layer 并在主線程包裝小節(jié),因此代碼訪問(wèn) Node 的 Layer。
2. 操作被檢查以確定是否在添加 Layer 之前就已經(jīng)取消了。在操作完成前,cell 被重用或者重新配置,就很可能會(huì)出現(xiàn)這樣的情況,那你就不應(yīng)該添加 Layer 了。
3. 作為一個(gè)保險(xiǎn),確保 Node 當(dāng)前的 nodeConstructionOperation 和調(diào)度此閉包的操作是同一個(gè) NSOperation 。
4. 如果 containerNode 的 preventOrCancel 是 true 就立即返回。如果構(gòu)造操作完成,但 Node 的繪制還沒(méi)有被取消,你依然不想 Node 的 Layer 顯示在 cell 里。
5. 最后,添加 Node 的 Layer 到層次結(jié)構(gòu)中,如果必要,這將創(chuàng)建 Layer。
編譯確保沒(méi)有錯(cuò)誤。
開(kāi)始 Node 創(chuàng)建操作
你依然沒(méi)有 實(shí)際 創(chuàng)建和開(kāi)始操作。讓我們現(xiàn)在來(lái)來(lái)吧。
繼續(xù)在 RainforestCardCell.swift 里,改變 configureCellDisplayWithCardInfo(cardInfo:) 的方法簽名為:
1 2 3 | func configureCellDisplayWithCardInfo( cardInfo: RainforestCardInfo, nodeConstructionQueue: NSOperationQueue) |
這里添加了一個(gè)新的參數(shù) nodeConstructionQueue。它就是一個(gè)用于 Node 創(chuàng)建操作的入隊(duì)的 NSOperationQueue 。
在 configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:) 底部,添加如下代碼:
1 2 3 | let newNodeConstructionOperation = nodeConstructionOperationWithCardInfo(cardInfo, image: image)nodeConstructionOperation = newNodeConstructionOperationnodeConstructionQueue.addOperation(newNodeConstructionOperation) |
這就創(chuàng)建了一個(gè) Node 構(gòu)造操作,將其保留在 nodeConstructionOperation 屬性,并將其添加到傳入的隊(duì)列。
最后,打開(kāi) RainforestViewController.swift 。給 RainforestViewController 添加一個(gè)叫做 nodeConstructionQueue 的初始化為常量的屬性,如下:
1 2 3 4 5 | class RainforestViewController: UICollectionViewController { let rainforestCardsInfo = getAllCardInfo() let nodeConstructionQueue = NSOperationQueue() ///< ADD THIS LINE ...} |
接下來(lái),在 collectionView(collectionView:cellForItemAtIndexPath indexPath:) 里,傳遞 View Controller 的 nodeConstructionQueue 到 configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:) :
1 | cell.configureCellDisplayWithCardInfo(cardInfo, nodeConstructionQueue: nodeConstructionQueue) |
cell 將會(huì)創(chuàng)建一個(gè)新的 Node 構(gòu)造操作并將其添加到 View Controller 的操作隊(duì)列里并發(fā)運(yùn)行。記住在 cell 出隊(duì)時(shí)就會(huì)創(chuàng)建一個(gè)新 Node 層次結(jié)構(gòu)。這并不理想,但足夠好。如果你要緩存 Node 的重用,看看 ASRangeController 吧。
哦呼,OK,現(xiàn)在編譯并運(yùn)行!你將看到和之前一樣的效果,但現(xiàn)在布局和渲染都沒(méi)在主線程執(zhí)行了。牛!我打賭里你重來(lái)沒(méi)有想過(guò)你會(huì)看到這一天你所做的 事情。這就是 AsyncDisplayKit 的威力。你可以將更多更多不需要在主線程的操作從主線程移除,這將給主線程更多機(jī)會(huì)處理用戶交互,讓你的 App 摸起來(lái)如黃油般順滑。

淡入 Cell
現(xiàn)在是有趣的部分。在這個(gè)簡(jiǎn)短的小節(jié),你將學(xué)到:
· 用自定義 Display Layer 子類(lèi)來(lái)支持 Node;
· 觸發(fā) Node Layer 的隱式動(dòng)畫(huà)。
這將會(huì)確保你移除爆米花特效并最終帶來(lái)良好的淡入動(dòng)畫(huà)。
創(chuàng)建一個(gè)新的 Layer 子類(lèi)。
菜單 File/New/File… ,選擇 iOS/Source/Cocoa Touch Class 并單擊 Next 。命名類(lèi)為 AnimatedContentsDisplayLayer 并使其作為 _ASDisplayLayer 的子類(lèi)。選擇 Swift 語(yǔ)言并單擊 Next。最后保存并打開(kāi) AnimatedContentsDisplayLayer.swift 。
現(xiàn)在添加如下方法到類(lèi):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | override func actionForKey(event: String!) -> CAAction! { if let action = super.actionForKey(event) { return action } if event == "contents" && contents == nil { let transition = CATransition() transition.duration = 0.6 transition.type = kCATransitionFade return transition } return nil} |
Layer 有一個(gè) contents 屬性,它告訴系統(tǒng)為這個(gè) Layer 繪制什么。AsyncDisplayKit 通過(guò)在后臺(tái)渲染 contents 并最后在主線程設(shè)置 contents。
這個(gè)代碼將會(huì)添加一個(gè)過(guò)渡動(dòng)畫(huà),這樣 contents 就會(huì)淡如到 View 中。你可以在 Apple 的 Core Animation Programming Guide 找到更多關(guān)于隱式 Layer 動(dòng)畫(huà)以及 CAAction 的信息.。
編譯并確保沒(méi)有錯(cuò)誤。
淡入容器 Node
你已經(jīng)設(shè)置好一個(gè) Layer 會(huì)在其 contents 被設(shè)置時(shí)淡入,你現(xiàn)在就要使用這個(gè) Layer。
打開(kāi) RainforestCardCell.swift 。在 nodeConstructionOperationWithCardInfo(cardInfo:image:) 里,在 Container Node Creation Section 開(kāi)頭,改動(dòng)如下行:
1 2 3 4 | // REPLACE THIS LINE...// let containerNode = ASDisplayNode()// ...WITH THIS LINE:let containerNode = ASDisplayNode(layerClass: AnimatedContentsDisplayLayer.self) |
這會(huì)告訴容器 Node 使用 AnimatedContentsDisplayLayer 實(shí)例作為其支持 Layer,因此自動(dòng)帶來(lái)淡入的效果。
注意:只有 _ASDisplayLayer 的子類(lèi)才能被異步地繪制。
編譯并運(yùn)行。你將看到容器 Node 會(huì)在其繪制好之后淡入。

又往何處去?
恭喜!在你需要高性能地滑動(dòng)你的用戶界面的時(shí)候,你有了另外一個(gè)工具在手。
在本教程里,你通過(guò)替換視圖層次結(jié)構(gòu)為一個(gè)柵格化的 AsyncDisplayKit Node 層次結(jié)構(gòu),顯著改善了一個(gè)性能很差的 Collection View 的滑動(dòng)性能。多么令人激動(dòng)!
這只是一個(gè)例子而已。AsyncDisplayKit 保有提高 UI 性能到一定水平的承諾,這通過(guò)平常的 UIKit 優(yōu)化往往難以達(dá)到。
實(shí)際說(shuō)來(lái),要充分利用 AsyncDisplayKit,你需要對(duì)標(biāo)準(zhǔn) UIKit 的真正性能瓶頸的所在有足夠的了解。AsyncDisplayKit 很棒的一點(diǎn)是它引發(fā)我們探討這些問(wèn)題并思考我們的 App 能如何在物理的極限上更快以及更具響應(yīng)性。
AsyncDisplayKit 是探討此性能前沿的一個(gè)非常強(qiáng)大的工具。明智地使用它,并步步逼近超級(jí)響應(yīng)UI的極限。
這僅僅是 AsyncDisplayKit 的一個(gè)開(kāi)始!它作者和貢獻(xiàn)者每天都在構(gòu)建新的特性。請(qǐng)關(guān)注 1.1 版的 ASCollectionView 以及 ASMultiplexImageNode。從頭文件中可看到“ASMultiplexImageNode 是一個(gè)圖像 Node,它能加載并顯示一個(gè)圖像的多個(gè)版本。例如,它可以在高分辨率的圖像還在渲染時(shí)先顯示一個(gè)低分辨率的圖像。” 非常酷,對(duì)吧 :]
你可以在此下載最終的 Xcode 項(xiàng)目。
AsyncDisplayKit 的指導(dǎo)在這里,AsyncDisplayKit 的 Github 倉(cāng)庫(kù)在這里。
這個(gè)庫(kù)的作者在收集 API 設(shè)計(jì)的反饋。你可以在 Facebook 上 的 Paper Engineering Community group 分享你的想法,或者直接參與到 AsyncDisplayKit 的開(kāi)發(fā)中,通過(guò) GitHub 貢獻(xiàn)你的 pull request。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注