[轉]AsyncDisplayKit 教程:達到 60 FPS 的滾動幀率

[原文:https://github.com/nixzhu/dev-blog/blob/master/2014-11-22-asyncdisplaykit-tutorial-achieving-60-fps-scrolling.md]html

Facebook 的 Paper 團隊給咱們帶來另外一個很棒的庫:AsyncDisplayKit。這個庫能讓你經過將圖像解碼、佈局以及渲染操做放在後臺線程,從而帶來超級響應的用戶界面,也就是說再也不會因界面卡頓而阻斷用戶交互。既然這麼厲害,那就在本教程裏學一下它吧。node

例如,對於很是複雜的界面,你可使用 AsyncDisplayKit 構建它而獲得一種如絲般順滑的,60幀每秒的滑動體驗。而日常的 UIKit 優化就不太可能克服這樣的性能挑戰。ios

在本教程中,你將從一個初始項目開始,它主要有一個 UICollectionView 的滑動問題,而使用 AsyncDisplayKit 將大大提升其滑動性能。一路上,你將學會如何在舊項目中使用 AsyncDisplayKit。git

注意:在開始本教程以前,你應該已熟悉 Swift、Core Animation 以及 Core Graphics。github

開始編程

開始以前,先看看 AsyncDisplayKit 的介紹。以對它有個簡要的概念,知道它是要解決什麼問題。swift

準備好了後就下載初始項目吧。你須要使用 Xcode 6.1 和 iOS 8.1 SDK 來編譯它。數組

注意:本教程的代碼使用 AsyncDisplayKit 1.0 來編寫。這個版本已經被包含在初始項目中了。緩存

你要研究的項目是由 UICollectionView 製做的卡片式界面來描述不一樣的雨林動物。每張信息卡包括一個圖片、名字以及一個對雨林動物的描述。卡片的背景圖是主圖片的模糊版。視覺設計的細節保證了文字的清晰易讀。安全

01.png

在 Xcode 中,打開初始項目裏的 Layers.xcworkspace 。

在本教程裏,請遵循如下原則以體會 AsyncDisplayKit 的那些十分吸引人的好處。

將應用運行在真機上。在模擬器裏運行很難看出性能改善。

應用是通用的,但在 iPad 上看起來最好。

最後,要真正感激這個庫能爲你所作的事情,請儘可能在最舊的能運行 iOS 8.1 的設備上運行本應用。第三代的 iPad 最好,由於它雖有視網膜屏幕,但運行得不是很快。

一旦你選定了設備,那就編譯並運行本項目。你會看到以下界面:

02.png

試着滑動 Collection View 並注意那可憐的幀率。在第三代 iPad 上,幀率大概只有 15-20 FPS,實在丟掉太多幀了。在本教程的最後,你能在 60 FPS (或很是接近)的幀率上滑動它。

注意:你所看到的圖像都在 App 的 asset 目錄裏,並非從網絡上獲取的。

測量響應速度

在一箇舊項目中使用 AsyncDisplayKit 前,你應該經過 Instruments 測量你的 UI 的性能,這樣纔有一個基準線以便對比改動的效果。

最重要的是,你要知道是 CPU-綁定 仍是 GPU-綁定。也就是說,是 CPU 仍是 GPU 拉低了應用的幀率。這個信息會告訴你該充分利用 AsyncDisplayKit 的哪一個特性以優化應用的性能。

若是你有時間,看看以前提到的 WWDC 2012 session 和/或在真實設備上使用 Instruments 來評估初始項目的時間曲線。滑動性能是 CPU-綁定 的。你能猜到是什麼緣由致使了 Collection View 丟掉這麼多幀嗎?

丟幀是由於模糊 cell 的背景圖像時阻塞了主線程。

爲項目準備好使用 AsyncDisplayKit

在舊項目裏使用 AsyncDisplayKit,歸結起來就是使用 Display Node 層次結構替換視圖層次結構和/或 Layer 樹。各類 Display Node 是 AsyncDisplayKit 的關鍵所在。它們位於視圖之上,並且是線程安全的,也就是說以前在主線程才能執行的任務如今也能夠在非主線程執行。這就能減輕主線程的工做量以執行其餘操 做,例如處理觸摸事件,或如在本應用的狀況裏,處理 Collection View 的滑動。

這就意味着在本教程裏,你的第一步是移除視圖層次結構。

移除視圖層次結構

打開 RainforestCardCell.swift 並刪除 awakeFromNib() 中全部的 addSubview(...) 調用,而後獲得以下:

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
}

接下來,替換 layoutSubviews() 的內容以下:

1
2
3
override func layoutSubviews() {
   super .layoutSubviews()
}

再將 configureCellDisplayWithCardInfo(cardInfo:) 的內容替換以下:

1
2
3
4
5
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {
   //MARK: Image Size Section
   let image = UIImage(named: cardInfo.imageName)!
   featureImageSizeOptional = image.size
}

刪除 RainforestCardCell 的全部視圖屬性,只留一個以下:

1
2
3
4
class RainforestCardCell: UICollectionViewCell {
   var  featureImageSizeOptional: CGSize?
   ...
}

最後,編譯並運行,你看到的就全是空空如也的卡片:

03.jpg

如今全部的 cell 都空了,滑動起來超級順滑。你的目標是保證以後添加回取代各視圖的 node 後,滑動依然順滑。

你可用 Instruments 的  Core Animation 模版在真機上檢測應用的性能,看看你的改動如何影響幀率。

添加一個佔位圖

打開 RainforestCardCell.swift ,給 RainforestCardCell 添加一個可選的 CALayer 變量,名爲 placeholderLayer:

1
2
3
4
5
class RainforestCardCell: UICollectionViewCell {
   var  featureImageSizeOptional: CGSize?
   var  placeholderLayer: CALayer!
   ...
}

你之因此須要一個佔位圖是由於顯示會異步完成,若是這個過程須要些時間,那用戶就會看到空的 cell —— 這並不愉快。就如同若是你要從網絡上獲取圖像,那麼就須要用佔位圖來填充 cell,這能讓你的用戶知道內容尚未準備好。雖然在咱們這種狀況裏,你是在後臺線程繪製而不是從網絡下載。

在 awakeFromNib() 裏,刪除 contentView 的 border 設置再建立並配置一個 placeholderLayer。將其添加到 cell 的 contentView 的 Layer 上。如今這個方法以下:

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。替換這個方法爲:

1
2
3
4
5
override func layoutSubviews() {
   super .layoutSubviews()
 
   placeholderLayer?.frame = bounds
}

編譯並運行,你從虛無的邊緣回來了:

04.png

樸素的 CALayer 不是由 UIView 支持的,當它們改變 frame 時,默認會有隱式動畫。這就是爲什麼你看到 layer 在佈局時放大。要修復這個問題,改動 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()
}

編譯並運行,問題解決了。

如今佔位圖不會亂動,再也不動畫它們的 frame 了。

第一個 Node

重建 App 的第一步是給每個 UICollectionView cell 添加一個背景圖片 Node,步驟以下:

1. 建立、佈局並添加一個圖像 Node 到 UICollectionView cell;

2. 處理 cell 重用 Node 和它們的 layer;以及

3. 模糊圖像 Node

但在作以前,打開 Layers-Bridging-Header.h 並導入 AsyncDisplayKit :

1
#import

這會讓全部的 Swift 文件都能訪問 AsyncDisplayKit 的各類類。

編譯一下,確保沒有錯誤。

方向:雨林 Collection View 結構

如今,咱們來看看 Collection View 的組成:

· View Controller :RainforestViewController 沒有什麼花哨的東西。它只是爲全部的雨林卡片獲取一個數據數組,併爲 UICollectionView 實現 Data Source。事實上,你不須要花太多時間到 View Controller 上。

· Data Source :大部分時間都將花在 cell 類 RainforestCardCell 上。View Controller 出隊每一個 cell 並將雨林卡片的數據用 configureCellDisplayWithCardInfo(cardInfo:) 傳給它。cell 就使用這個數據來配置自身。

· Cell :在 configureCellDisplayWithCardInfo(cardInfo:) 裏,cell 建立、配置、佈局以及添加 Node 到它本身身上。這就意味着每次 View Controller 出隊一個 cell,這個 cell 就會建立並添加給它本身一個新的 Node 層次結構。

若是你使用 View 而不是 Node,那麼這樣作對於性能來講就不是最佳策略。但由於你能夠異步地建立、配置以及佈局,並且 Node 也是異步地繪製的,因此這不會是一個問題。真正的難點是在 cell 準備重用時取消任何在進行的異步操做並移除舊 Node 。

注意 :本教程的這個策略來添加 Node 到 cell 還算 OK。對於精通 AsyncDisplayKit 來講,這是很好的第一步。

然而,在實際生產中,你最好使用 ASRangeController 來緩存你的 Node,這樣你就不用每次在 cell 重用時重建它的 Node 層次結構。ASRangeController 超出了本教程的範圍,但若你想了解更多的信息,看看頭文件 ASRangeController.h 的註釋吧。

再注意一下:1.1 版的 AsyncDisplayKit (本教程編寫時還未放出,但會在此後不久放出)包含有 ASCollectionView。使用 ASCollectionView 會讓本 App 的整個 Collection View 都由 Display Node 控制。而在本教程中,每一個 cell 會包含一個 Display Node 層次結構。如上面所解釋的,這能工做,但若是使用 ASCollectionView 可能會更好。給力的 ASCollectionView!

OK,該動手了。

添加背景圖片 Node

如今你要走一遍用 Node 配置 cell 的過程,一次一步:

打開 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
}

這就建立並配置了一個 ASImageNode 常量,叫作 backgroundImageNode。

注意:確保包含 //MARK: 註釋,這樣更容易看清代碼位置。

AsyncDisplayKit 帶有好幾種 Node 類型,包括 ASImageNode,用於顯示圖片。它至關於 UIImageView,除了 ASImageNode 是默認異步地解碼圖片。

添加以下代碼到 configureCellDisplayWithCardInfo(cardInfo:) 底部:

1
backgroundImageNode.layerBacked =  true

這讓 backgroundImageNode 變爲 Layer 支持的 Node。

Node 可由 UIView 支持或 CALayer 支持。當 Node 須要處理事件時(例如觸摸事件),你就要使用  UIView 支持的 Node。若是你不須要處理事件,只須要顯示一下內容,那使用 Layer 支持的 Node 會更加輕量,所以能夠得到一個小的性能提高。

由於本教程的 App 不須要處理事件,因此你可以讓全部的 Node 都設置爲 Layer 支持的。在上面的代碼中,因爲 backgroundImageNode 爲 Layer 支持的,AsyncDisplayKit 會建立一個 CALayer 用於雨林動物圖像內容的顯示。

繼續 configureCellDisplayWithCardInfo(cardInfo:) 並添加以下代碼:

1
2
//MARK: Node Layout Section
backgroundImageNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size)

這裏使用 FrameCalculator 爲 backgroundImageNode 佈局。

FrameCalculator  是一個幫助類,它包裝了cell 的佈局,爲每一個 Node 返回 frame。注意全部的東西都是手動佈局的, 沒有使用 Auto Layout 約束 。若是你須要構建自適應佈局或者本地化驅動的佈局,那就要注意,由於你不能給 Node 添加約束。

接下來,添加以下代碼到 configureCellDisplayWithCardInfo(cardInfo:) 底部:

1
2
//MARK: Node Layer and Wrap Up Section
self.contentView.layer.addSublayer(backgroundImageNode.layer)

這句將 backgroundImageNode 的 Layer 添加到 cell contentView 的 Layer 上。

注意,AsyncDisplayKit 會爲 backgroundImageNode 建立一個 Layer。然而,你必需要將 Node 放到某個 Layer 樹中才能在屏幕上顯示。這個 Node 會異步地繪製,因此直到繪製完成,它的內容都不會顯示,儘管它的 Layer 已經在一個 Layer 樹中。

從技術角度來講, Layer 一直都存在。但渲染圖像是異步進行的。Layer 初始化時沒有內容(例如是透明的)。一旦渲染完成,Layer 的 contents 就會更新爲包含圖像內容。

在這個點,cell 的 contentView 的 Layer 將會包含兩個 Sublayer:一個佔位圖和 Node 的 Layer。在 Node 完成繪製前,只有佔位圖會顯示。

注意到 configureCellDisplayWithCardInfo(cardInfo:) 會在每次 cell 出隊時被調用。每次 cell 被回收,這個邏輯會添加一個新的 Sublayer 到 cell 的 contentView Layer 上。不要擔憂,你很快會解決這個問題。

回到 RainforestCardCell.swift 開頭,給 RainforestCardCell 添加一個 ASImageNode 變量存爲屬性 backgroundImageNode,以下:

1
2
3
4
5
6
class RainforestCardCell: UICollectionViewCell {
   var  featureImageSizeOptional: CGSize?
   var  placeholderLayer: CALayer!
   var  backgroundImageNode: ASImageNode?  ///< ADD THIS LINE
   ...
}

你之因此須要這個屬性是由於必需要有某個東西將 backgroundImageNode 的引用保留住,不然 ARC 就會將其釋放,也就不會有任何東西顯示出來——即便 Node 的 Layer 在一個 Layer 樹中,你依然須要保留 Node。

在 configureCellDisplayWithCardInfo(cardInfo:) 底部的 Node Layer and Wrap Up Section ,設置 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
}

編譯並運行,觀察 AsyncDisplayKit 是如何異步地使用圖像設置 Layer 的 contents 的。這能讓你在 CPU 還在繪製 Layer 的內容的同時上下滑動界面。

05.png

若是你運行在舊設備上,注意圖像是如何彈出到位置——這是爆米花特效,但不老是讓人喜歡!本教程的最後一節會搞定這個不使人愉快的彈出效果,給你展現如何讓圖像天然地淡入,如同搖滾巨星。

如以前所討論的,新的 Node 會在每次 cell 被重用時建立。這並不很理想,由於這意味着新的 Layer 會在每次 cell 被重用時加入。

若是你想看看 Sublayer 堆積太多的影響,那就不停的滑上滑下屢次,而後加斷點打印出 cell 的 contentView 的 Layer 的 sublayers 屬性。你會看到不少 Layer,這並很差。

處理 Cell 重用

繼續 RainforestCardCell.swift ,給 RainforestCardCell 添加一個叫作 contentLayer 的 CALayer 屬性。這個屬性也是一個可選類型:

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 樹中舊的 Node Layer。雖然你能夠簡單地保留 Node 並訪問其 Layer 屬性,但上面的寫法更加明確。

添加以下代碼到 configureCellDisplayWithCardInfo(cardInfo:) 結尾:

1
self.contentLayer = backgroundImageNode.layer

這句讓  backgroundImageNode 的 Layer 保留到 contentLayer 屬性。

替換 prepareForReuse() 的實現以下:

1
2
3
4
override func prepareForReuse() {
   super .prepareForReuse()
   backgroundImageNode?.preventOrCancelDisplay =  true
}

由於 AsyncDisplayKit 可以異步地繪製 Node,因此 Node 讓你能預防從頭繪製或取消任何在進行的繪製。不管是你須要預防或取消繪製,均可將 preventOrCancelDisplay 設置爲 true,如上面代碼所示。在本例中,你要在 cell 被重用前取消任何正在進行的繪製活動。

接下來,添加以下代碼到 prepareForReuse() 尾部:

1
contentLayer?.removeFromSuperlayer()

這將  contentLayer 從其 Superlayer (也就是 contentView 的 Layer)中移除。

每次一個 cell 被回收時,這個代碼就移除 Node 的舊 Layer ,於是解決了堆積問題。因此在任什麼時候間,你的 Node 最多隻有兩個 Sublayer:佔位圖和 Node 的 Layer。

接下來添加以下代碼到 prepareForReuse() 尾部:

1
2
contentLayer = nil
backgroundImageNode = nil

這確保 cell 釋放它們的引用,這樣若有必要,ARC 纔好作清理工做。

編譯並運行。此次,沒有 Sublayer 會堆積的問題,且全部沒必要要的繪製都會被取消。

06.png

是時候來點兒模糊效果了,Baby,模糊哦。

07.png

模糊圖像

要模糊圖像,你要添加一個額外的步驟到圖像 Node 的顯示過程裏。

繼續 RainforestCardCell.swift ,在 configureCellDisplayWithCardInfo(cardInfo:) 的設置  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 給你一個機會在顯示以前去處理底層的圖像。這是很是實用的功能,它讓你能對圖像 Node 作一些操做,例如添加濾鏡等。

在上面的代碼裏,你使用 imageModificationBlock 來爲 cell 的背景圖像應用模糊效果。關鍵點就是圖像 Node 將會繪製它的內容並在後臺執行這個閉包,而主線程依然順滑流暢。這個閉包接受原始的  UIImage 並返回一個修改過的  UIImage。

上面的代碼使用了  UIImage 的模糊 category,它由 Apple 在 WWDC 2013 提供,使用了 Accelerate framework 在 CPU 上模糊圖像。由於模糊會消耗不少時間和內存,這個版本的 category 被修改成包含了取消機制。這個模糊方法將按期調用 didCancel 閉包來決定是否應該要中止模糊。

如今,上面的代碼給  didCancel 簡單地返回 false。以後你會重寫 didCancel 閉包。

注意:還記得第一次運行 App 時 Collection View 那可憐的滑動效果嗎?模糊方法阻塞了主線程。經過使用 AsyncDisplayKit 將模糊放入後臺,你就大幅度地提升了 Collection View 的滑動性能。簡直天壤之別。

編譯並運行,觀察模糊效果:

08.png

注意你能夠如何很是流暢地滑動 Collection View。

當 Collection View 出隊一個 cell 時,一個模糊操做將開始於後臺線程。當用戶快速滑動時,Collection View 會重用每一個 cell 屢次,並開始許多模糊操做。咱們的目標是在 cell 準備被重用時取消正在進行中的模糊操做。

你已經在 prepareForReuse() 裏取消了 Node 的繪製操做 ,但一旦控制被移交給處理你圖像修改的閉包,那就是你的責任來處理 Node 的 preventOrCancelDisplay 設置,你如今就要作。

取消模糊操做

要取消進行中的模糊操做,你須要實現模糊方法的 didCancel 閉包。

添加一個捕捉列表到 imageModificationBlock 以捕捉一個 backgroundImageNode 的 weak 引用:

1
2
3
backgroundImageNode.imageModificationBlock = { [weak backgroundImageNode] input  in
    ...
}

你須要 weak 引用來避免閉包和圖像 Node 之間的保留環問題。你將使用這個 weak  backgroundImageNode 來肯定是否要取消模糊操做。

是時候構建模糊取消閉包了。添加下面代碼到 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 引用,準備用其幹活。若是 backgroundImageNode 在本次運行時消失,那麼 isCancelled 將保持爲 true,而後模糊操做會被取消。若是沒有 Node 須要顯示,天然沒有必要繼續模糊操做。

2. 在此你將操做取消檢查包在閉包裏,由於一旦 Node 建立它的 Layer 或 View,那就只能在主線程訪問 Node 的屬性。因爲你須要訪問 preventOrCancelDisplay,因此你必須在主線程檢查。

3. 最後,確保 isCancelledClosure 是在主線程運行,不管是已在主線程而直接運行,仍是不在主線程而經過  dispatch_sync 來調度。它必須是一個同步的調度,由於咱們須要閉包完成,並在 didCancelBlur 閉包返回以前設置 isCancelled。

在調用 applyBlurWithRadius(...) 中,修改傳遞給 didCancel 的參數,替換一直返回  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) {
   ...
}

編譯並運行。你看你不會注意到太多差異,但如今任何在 cell 離開屏幕時還未完成的模糊都會被取消了。這就意味着設備比以前作得更少。你可能觀察到輕微的性能提高,特別是在較慢的設備如第三代 iPad 上運行時。

08.png

固然,若沒有東西在前面,背景就不是真正的背景!你的卡片須要內容。經過下面四個小節,你將學會:

· 建立一個容器 Node,它將全部的 Subnode 繪製到一個單獨的 CALayer 裏;

· 構建一個 Node 層次結構;

· 建立一個自定義的 ASDisplayNode  子類;並

· 在後臺構建並佈局 Node 層次結構;

作完這些,你就會獲得一個看起來和添加 AsyncDisplayKit 以前同樣的 App,但有着黃油般順滑的滑動體驗。

柵格化的容器 Node

直到如今,你一直在操做 cell 內的一個單獨的 Node。接下來,你將建立一個容器 Node,它會包含全部的卡片內容。

添加一個容器 Node

繼續 RainforestCardCell.swift ,在 configureCellDisplayWithCardInfo(cardInfo:) 的  backgroundImageNode.imageModificationBlock 後面以及 Node Layout Section 前面添加以下代碼:

1
2
3
4
5
6
//MARK: Container Node Creation Section
let containerNode = ASDisplayNode()
containerNode.layerBacked =  true
containerNode.shouldRasterizeDescendants =  true
containerNode.borderColor = UIColor(hue: 0, saturation: 0, brightness: 0.85, alpha: 0.2).CGColor
containerNode.borderWidth = 1

這就建立並配置了一個叫作 containerNode 的 ASDisplayNode 常量。注意這個容器的 shouldRasterizeDescendants,這是一個關於節點如何工做的提示以及一個如何讓它們工做得更好地機會。

如單詞 「descendants(子孫)」 所暗示的,你能夠建立 AsyncDisplayKit Node 的層次結構或樹,就如你能夠建立 Core Animation Layer 的層次結構同樣。例如,若是你有一個都是 Layer 支持的 Node 層次結構,那麼 AsyncDisplayKit 將會爲每一個 Node 建立一個分離的 CALayer,Layer 層次結構將會和 Node 層次結構同樣,如同鏡像。

這聽起來很熟悉:它相似於當你使用普通的 UIKit 時,Layer 層次結構鏡像於 View 層次結構。然而,這個 Layer 的棧有一些不一樣的效果:

首先,由於是異步渲染,你就不會看到每一個 Layer 一個接一個地顯示。當 AsyncDisplayKit 繪製完成每一個 Layer,它立刻製做 Layer 的顯示內容。因此若是你有一個 Layer 的繪製比其餘 Layer 耗時更長,那麼它將會在它們以後顯示。用戶會看到零碎的 Layer 組件,這個過程一般是不可見的,由於 Core Animation 會在顯示任何東西以前重繪全部必須的 Layer 。

第二,有許多 Layer 可以引發性能問題。每一個 CALayer 都須要一個支持存儲來保存它的像素位圖和內容。一樣,Core Animation 必須將每一個 Layer 經過 XPC 發給渲染服務器。最後,渲染服務器可能須要重繪一些 Layer 以複合它們,例如在混合 Layer 時。總的來講,更多的 Layer 意味着 Core Animation 更多的工做。因此限制 Layer 使用的數量有許多不一樣的好處。

爲了解決這個問題,AsyncDisplayKit 有一個方便的特性:它容許你繪製一個 Node 層次結構到一個單獨的 Layer 容器裏。這就是 shouldRasterizeDescendants 所作的。當你設置它,那在完成全部的 Subnode 的繪製以前,ASDisplayNode 將不會設置 Layer 的 contents。

因此在以前的步驟裏,設置容器 Node 的 shouldRasterizeDescendants 爲 true 有兩個好處:

1. 它確保卡片一次顯示全部的 Node,如同舊的同步繪製;

2. 並且它經過柵格化 Layer 棧爲單個 Layer 並較少將來的合成而提升了效率。

不足之處是,因爲你將全部的 Layer 放入一個位圖,你就不能在以後單獨動畫某個 Node 了。

要得到更多信息,請看 shouldRasterizeDescendants 在頭文件 ASDisplayNode.h 裏的註釋。

接下來,在 Container Node Creation Section 後,添加 backgroundImageNode 爲 containerNode 的 Subnode:

1
2
//MARK: Node Hierarchy Section
containerNode.addSubnode(backgroundImageNode)

注意:添加 Node 的順序很重要,就如同 subview 和 sublayer。最早添加的 Node 會被以後添加的阻擋顯示。

替換 Node Layout Section 的第一行爲:

1
2
//MARK: Node Layout Section
containerNode.frame = FrameCalculator.frameForContainer(featureImageSize: image.size)

最後,使用 FrameCalculator 佈局 backgroundImageNode:

1
2
backgroundImageNode.frame = FrameCalculator.frameForBackgroundImage(
   containerBounds: containerNode.bounds)

這設置 backgroundImageNode 填滿整個 containerNode。

你幾乎完成了新的 Node 層次結構,但首先你須要正確地設置 Layer 層次結構,由於容器 Node 如今是根。

管理容器 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

由於 cell 只須要單獨保留容器 Node ,因此你要移除 backgroundImageNode 屬性。

再也不設置 cell 的 contentLayer 屬性爲  backgroundImageNode 的 Layer,如今將其設置爲 containerNode 的 Layer:

1
2
3
4
// Replace the following line...
// self.contentLayer = backgroundImageNode.layer
// ...with this line:
self.contentLayer = containerNode.layer

給 RainforestCardCell 添加一個可選的 ASDisplayNode 實例存儲爲屬性 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 ,若是你不這麼作它們就會被當即釋放。

回到 configureCellDisplayWithCardInfo(cardInfo:),在 Node Layer and Wrap Up Section 最後,設置 containerNode 屬性爲 containerNode  常量:

1
self.containerNode = containerNode

編譯並運行。模糊的圖像將會再此顯示!但還有最後一件事要去改變,由於如今有了新的 Node 層次結構。回憶以前 cell 重用時你將圖像中止顯示。如今你須要讓整個 Node 層次結構中止顯示。

在新的 Node 層次結構上處理 Cell 重用

繼續 RainforestCardCell.swift ,在 prepareForReuse() 裏,替換設置 backgroundImageNode.preventOrCancelDisplay 爲在 containerNode 上調用 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()
   ...
}

當你要取消整個 Node 層次結構的繪製,就使用 recursiveSetPreventOrCancelDisplay()。這個方法將會設置這個 Node 以及其全部子 Node 的 preventOrCancelDisplay 屬性,不管 true 或 false。

接下來,依然在 prepareForReuse(),用設置  containerNode 爲 nil 替換設置 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?
   ...
}

編譯並運行。這個 App 就如以前同樣,但如今你的圖像 Node 在容器 Node 內,而重用依然和它應有的方式同樣。

09.png

Cell 內容

目前爲止你有了一個 Node 層次結構,但容器內還只有一個 Node——圖像 Node。如今是時候設置 Node 層次結構去複製在添加 AsyncDisplayKit 以前時應用的視圖層次結構了。這意味着添加 text 和一個未模糊的特徵圖像。

添加特徵圖像

咱們要添加特徵圖像了,它是一個未模糊的圖像,顯示在卡片的頂部。

打開 RainforestCardCell.swift  並找到 configureCellDisplayWithCardInfo(cardInfo:)。在 Node Creation Section 的底部,添加以下代碼:

1
2
3
4
let featureImageNode = ASImageNode()
featureImageNode.layerBacked =  true
featureImageNode.contentMode = .ScaleAspectFit
featureImageNode.image = image

這會建立並配置一個叫作 featureImageNode 的 ASImageNode 常量。它被設置爲 Layer 支持的,放大以適用,並設置顯示圖像,此次不須要模糊。

在 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)

編譯並運行。你就會看到特徵圖像在卡片的頂部出現,位於模糊圖像的上方。注意特徵圖像和模糊圖像是如何在同一時間跳出。這是你以前添加的 shouldRasterizeDescendants 在起做用。

10.png

添加 Title 文本

接下來添加文字 Label,以顯示動物的名字和描述。首先來動物名字吧。

繼續 configureCellDisplayWithCardInfo(cardInfo:),找到 Node Creation Section 。添加下列代碼到這節尾部,就在建立 featureImageNode 以後:

1
2
3
4
let titleTextNode = ASTextNode()
titleTextNode.layerBacked =  true
titleTextNode.backgroundColor = UIColor.clearColor()
titleTextNode.attributedString = NSAttributedString.attributedStringForTitleText(cardInfo.name)

這就建立了一個叫作 titleTextNode 的 ASTextNode 常量。

ASTextNode 是另外一個 AsyncDisplayKit 提供的 Node 子類,其用於顯示文本。它是一個具備 UILabel 效果的 Node。它接受一個 attributedString,由 TextKit 支持,有許多特性如文本連接。要學到更多關於這個 Node 的功能,去看 ASTextNode.h 吧。

初始項目包含有一個 NSAttributedString 的擴展,它提供了一個工廠方法去生成一個屬性字符串用於 Title 和 Description 文本以顯示在雨林卡片上。上面的代碼使用了這個擴展的 attributedStringForTitleText(...) 方法。

如今,在 Node Hierarchy Section 底部,添加以下代碼:

1
containerNode.addSubnode(titleTextNode)

這就添加了 titleTextNode 到 Node 層次結構裏。它將位於特徵圖像和背景圖像之上,由於它在它們以後添加。

在 Node Layout Section 底部添加以下代碼:

1
2
3
titleTextNode.frame = FrameCalculator.frameForTitleText(
   containerBounds: containerNode.bounds,
   featureImageFrame: featureImageNode.frame)

同樣使用 FrameCalculator 佈局 titleTextNode,就像 backgroundImageNode 和 featureImageNode 那樣。

編譯並運行。你就有了一個 Title 顯示在特徵圖像的頂部。再次說明, Label 只會在整個 cell 準備好渲染時才渲染。

11.png

添加 Description 文本

添加一個有着 Description 文本的 Node 和添加 Title 文本的 Node 相似。

回到 configureCellDisplayWithCardInfo(cardInfo:) ,在 Node Creation Section 最後,添加以下代碼。就在以前建立 titleTextNode 的語句以後:

1
2
3
4
5
let descriptionTextNode = ASTextNode()
descriptionTextNode.layerBacked =  true
descriptionTextNode.backgroundColor = UIColor.clearColor()
descriptionTextNode.attributedString = 
   NSAttributedString.attributedStringForDescriptionText(cardInfo.description)

這就建立並配置了一個叫作 descriptionTextNode 的 ASTextNode 實例。

在  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)

編譯並運行。如今你能看到 Description 文本了。

12.png

Custom Node Subclasses 自定義 Node 子類

目前爲止,你使用了 ASImageNode 和 ASTextNode。這會帶你走很遠,但有些時候你須要你本身的 Node,就如同某些時候在傳統的 UIKit 編程裏你須要本身的 View 同樣。

建立梯度 Node 類

接下來,你將給 GradientView.swift 添加 Core Graphics 代碼來構建一個自定義的梯度 Display Node。這會被用於建立一個繪製梯度的自定義 Node 。梯度圖會顯示在特徵圖像的底部以便讓 Title 看起來更加明顯。

打開 Layers-Bridging-Header.h 並添加以下代碼:

1
#import

需這一步是由於這個類沒有包含在庫的主頭文件裏。你在子類化任何 ASDisplayNode 或 _ASDisplayLayer 時都須要訪問這個類。

菜單 File\New\File… 。選擇 iOS\Source\Cocoa Touch Class 。命名類爲 GradientNode 並使其做爲 ASDisplayNode 的子類。選擇 Swift 語言並點擊 Next 。保存文件再打開 GradientNode.swift 。

添加以下方法到這個類:

1
2
3
4
class func drawRect(bounds: CGRect, withParameters parameters: NSObjectProtocol!,
     isCancelled isCancelledBlock: asdisplaynode_iscancelled_block_t!, isRasterizing: Bool) {
 
}

如同 UIView 或 CALayer,你能夠子類化 ASDisplayNode 去作自定義繪製。你可使用如同用於 UIView 的 Layer 或單獨的 CALayer 的繪製代碼,這取決於客戶 Node 如何配置 Node。查看 ASDisplayNode+Subclasses.h 獲取更多關於子類化 ASDisplayNode 的信息。

進一步,ASDisplayNode 的繪製方法比在 UIView 和 CALayer 裏的接受更多參數,給你提供方法少作工做,並更有效率。

要爲你的自定義 Display Node 填充內容,你須要實現來自 _ASDisplayLayerDelegate 協議的 drawRect(...) 或 displayWithParameters(...)。在繼續以前,看看 _ASDisplayLayer.h 獲得這個方法和它們參數的信息。搜索 _ASDisplayLayerDelegate。重點看看頭文件註釋裏關於 drawRect(...) 的描述。

由於梯度圖位於特徵圖的上方,使用 Core Graphics 繪製,因此你須要使用 drawRect(...) 。

打開 GradientView.swift 並拷貝 drawRect(...) 的內容到 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,編譯並確保沒有錯誤。

添加梯度 Node

打開 RainforestCardCell.swift 並找到 configureCellDisplayWithCardInfo(cardInfo:)。在 Node Creation Section 底部,添加以下代碼,就在建立 descriptionTextNode 的代碼以後:

1
2
3
let gradientNode = GradientNode()
gradientNode.opaque =  false
gradientNode.layerBacked =  true

這就建立了一個叫作 gradientNode 的 GradientNode 常量。

在 Node Hierarchy Section,在添加 featureImageNode 那樣下面,添加 gradientNode 到 containerNode:

1
2
3
4
5
6
//MARK: Node Hierarchy Section
containerNode.addSubnode(backgroundImageNode)
containerNode.addSubnode(featureImageNode)
containerNode.addSubnode(gradientNode)  ///< ADD THIS LINE
containerNode.addSubnode(titleTextNode)
containerNode.addSubnode(descriptionTextNode)

梯度 Node 須要這個位置才能在特徵圖之上,Title 之下。

而後添加以下代碼到 Node Layout Section 底部:

1
2
gradientNode.frame = FrameCalculator.frameForGradient(
   featureImageFrame: featureImageNode.frame)

編譯並運行。你將看到梯度在特徵圖的底部。Title 確實看得更清楚了!

13.png

爆米花特效

如以前提到的,cell 的 Node 內容會在完成繪製時「彈出」。這不是很理想。因此讓咱們繼續,以修復這個問題。但首先,更加深刻 AsyncDisplayKit 以看看它是如何工做的。

在 configureCellDisplayWithCardInfo(cardInfo:) 的 Container Node Creation Section ,關閉容器 Node 的 shouldRasterizeDescendants:

1
containerNode.shouldRasterizeDescendants =  false

編譯並運行。你會注意到如今容器層次結構裏不一樣的 Node 一個接一個的彈出。你會看到文字彈出,而後是特徵圖,而後是模糊背景圖。

當 shouldRasterizeDescendants 關閉後,AsyncDisplayKit 就不是繪製一個容器 Layer 了,它會建立一個鏡像卡片 Node 層次結構的 Layer 樹。記得爆米花特效存在是由於每一個 Layer 都在它繪製結束後當即出現,而某些 Layer 比另一個花費更多時間在繪製上。

這不是咱們所須要的,但它描述了 AsyncDisplayKit 的工做方式。咱們不想要這個行爲,因此仍是將 shouldRasterizeDescendants 打開:

1
containerNode.shouldRasterizeDescendants =  true

編譯並運行。又回到整個 cell 在其渲染結束後彈出了。

該從新思考如何擺脫爆米花特效了。但首先,讓咱們看看 Node 在後臺如何構造。

在後臺構造 Node

除了異步地繪製,使用 AsyncDisplayKit,你一樣能夠異步地建立、配置以及佈局。深呼吸一下,由於這就是你接下來要作的事情。

建立一個 Node 構造操做(Operation)

你要將 Node 層次結構的構造包裝到一個 NSOperation 中。這樣作很棒,由於這個操做能很容易的在不一樣的操做隊列上執行,包括後臺隊列。

打開  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
}

繪製並非惟一會拖慢主線程的操做。對於複雜的屏幕,佈局計算也有可能變的昂貴。目前爲止,本教程當前狀態的項目,一個緩慢的 Node 佈局會引發  Collection View 丟幀。

60 FPS 意味着你有大約 17ms 的時間讓你的 cell 準備好顯示,不然一個或多個幀就會被丟掉。這在 Table View 和 Collection View 有很複雜的 cell 時是很是常見的,滑動時丟幀就是這個緣由。

AsyncDisplayKit 前來救援!

你將使用上面的 nodeConstructionOperation 將全部 Node 層次結構構造以及佈局從主線程剝離並放入後臺 NSOperationQueue,進一步確保 Collection View 能儘可能以接近 60 FPS 的幀率滑動。

警告:你能夠在後臺訪問並設置 Node 的屬性,但只能在 Node 的 Layer 或 View 被建立以前,也就是當你第一次訪問 Node 的 Layer 或 View 屬性時。

一旦 Node 的 Layer 或 View 被建立,你必須在主線程才能訪問和設置 Node 的屬性,由於 Node 將會轉發這些調用到它的 Layer 或 View。若是你獲得一個崩潰 log 說「Incorrect display node thread affinity」,那就意味着在建立 Node 的 Layer 或 View 以後,你依然嘗試在後臺訪問或設置 Node 的屬性。

修改 nodeConstructionOperation 操做 Block 的內容以下:

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
   }
}

在這個操做運行時,cell 可能已經被釋放了。在那種狀況下,你不須要作任何工做。相似的,若是操做被取消了,那同樣也沒有工做要作了。

之因此對 nodeConstructionOperation` 使用 unowned  引用是爲了不在操做和執行閉包之間產生保留環。

如今找到 configureCellDisplayWithCardInfo(cardInfo:)。將任何在 Image Size Section 以後的代碼移動到 nodeConstructionOperation 的執行閉包裏。將代碼放在 strongSelf 的條件語句裏,即TODO的位置。以後 configureCellDisplayWithCardInfo(cardInfo:) 將看起來以下:

1
2
3
4
5
func configureCellDisplayWithCardInfo(cardInfo: RainforestCardInfo) {
   //MARK: Image Size Section
   let image = UIImage(named: cardInfo.imageName)!
   featureImageSizeOptional = image.size
}

目前,你會有一些編譯錯誤。這是由於操做 Block 裏的 self 是 weak 引用,所以是可選的。但你有一個 self 的 strong 引用,由於代碼在可選綁定語句內。因此替換錯誤的幾行成下面的樣子:

1
2
3
strongSelf.contentView.layer.addSublayer(containerNode.layer)
strongSelf.contentLayer = containerNode.layer
strongSelf.containerNode = containerNode

最後,添加以下代碼到你剛改動的三行之下:

1
containerNode.setNeedsDisplay()

編譯確保沒有錯誤。若是你如今運行,那麼只有佔位圖會顯示,由於 Node 的建立操做尚未實際使用。讓咱們來添加它。

使用 Node 建立操做

打開 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
   ...
}

這就添加了一個叫作 nodeConstructionOperation 的可選屬性

當 cell 準備回收時,你會使用這個屬性去取消 Node 的構造。這會在用戶很是快速地滑動 Collection View 時發生,特別是若是佈局還須要一些計算時間的話。

在 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 重用時取消了操做,因此若是 Node 建立還沒完成,它也不會完成。

如今找到 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
}

這個 cell 如今會在它準備重用並開始配置時,取消任何進行中的 Node 構造操做。這確保了操做被取消,即便 cell 在準備好重用前就被從新配置。

編譯並確保沒有錯誤。

在主線程運行

AsyncDisplayKit 容許你在非主線程作許多工做。但當它要面對 UIKit 和 CoreAnimation 時,你仍是須要在主線程作。目前爲止,你從主線程移走了全部的 Node 建立。但還有一件事須要被放在主線程——即設置 CoreAnimation 的 Layer 層次結構。

在 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
// 1
dispatch_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. 回憶到當 Node 的 Layer 屬性被第一個訪問時,全部的 Layer 會被建立。這就是爲什麼你必須運行 Node Layer 並在主線程包裝小節,所以代碼訪問 Node 的 Layer。

2. 操做被檢查以肯定是否在添加 Layer 以前就已經取消了。在操做完成前,cell 被重用或者從新配置,就極可能會出現這樣的狀況,那你就不該該添加 Layer 了。

3. 做爲一個保險,確保 Node 當前的 nodeConstructionOperation 和調度此閉包的操做是同一個 NSOperation 。

4. 若是 containerNode 的 preventOrCancel 是 true 就當即返回。若是構造操做完成,但 Node 的繪製尚未被取消,你依然不想 Node 的 Layer 顯示在 cell 裏。

5. 最後,添加 Node 的 Layer 到層次結構中,若是必要,這將建立 Layer。

編譯確保沒有錯誤。

開始 Node 建立操做

你依然沒有 實際 建立和開始操做。讓咱們如今來來吧。

繼續在 RainforestCardCell.swift 裏,改變 configureCellDisplayWithCardInfo(cardInfo:) 的方法簽名爲:

1
2
3
func configureCellDisplayWithCardInfo(
   cardInfo: RainforestCardInfo,
   nodeConstructionQueue: NSOperationQueue)

這裏添加了一個新的參數 nodeConstructionQueue。它就是一個用於 Node 建立操做的入隊的 NSOperationQueue 。

在 configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:) 底部,添加以下代碼:

1
2
3
let newNodeConstructionOperation = nodeConstructionOperationWithCardInfo(cardInfo, image: image)
nodeConstructionOperation = newNodeConstructionOperation
nodeConstructionQueue.addOperation(newNodeConstructionOperation)

這就建立了一個 Node 構造操做,將其保留在 nodeConstructionOperation 屬性,並將其添加到傳入的隊列。

最後,打開 RainforestViewController.swift 。給 RainforestViewController 添加一個叫作 nodeConstructionQueue 的初始化爲常量的屬性,以下:

1
2
3
4
5
class RainforestViewController: UICollectionViewController {
   let rainforestCardsInfo = getAllCardInfo()
   let nodeConstructionQueue = NSOperationQueue()  ///< ADD THIS LINE
   ...
}

接下來,在 collectionView(collectionView:cellForItemAtIndexPath indexPath:) 裏,傳遞 View Controller 的 nodeConstructionQueue 到 configureCellDisplayWithCardInfo(cardInfo:nodeConstructionQueue:) :

1
cell.configureCellDisplayWithCardInfo(cardInfo, nodeConstructionQueue: nodeConstructionQueue)

cell 將會建立一個新的 Node 構造操做並將其添加到 View Controller 的操做隊列裏併發運行。記住在 cell 出隊時就會建立一個新 Node 層次結構。這並不理想,但足夠好。若是你要緩存 Node 的重用,看看 ASRangeController 吧。

哦呼,OK,如今編譯並運行!你將看到和以前同樣的效果,但如今佈局和渲染都沒在主線程執行了。牛!我打賭裏你重來沒有想過你會看到這一天你所作的 事情。這就是 AsyncDisplayKit 的威力。你能夠將更多更多不須要在主線程的操做從主線程移除,這將給主線程更多機會處理用戶交互,讓你的 App 摸起來如黃油般順滑。

14.png

淡入 Cell

如今是有趣的部分。在這個簡短的小節,你將學到:

· 用自定義 Display Layer 子類來支持 Node;

· 觸發 Node Layer 的隱式動畫。

這將會確保你移除爆米花特效並最終帶來良好的淡入動畫。

建立一個新的 Layer 子類。

菜單 File\New\File… ,選擇 iOS\Source\Cocoa Touch Class 並單擊 Next 。命名類爲 AnimatedContentsDisplayLayer 並使其做爲 _ASDisplayLayer 的子類。選擇 Swift 語言並單擊 Next。最後保存並打開 AnimatedContentsDisplayLayer.swift 。

如今添加以下方法到類:

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 有一個 contents 屬性,它告訴系統爲這個 Layer 繪製什麼。AsyncDisplayKit 經過在後臺渲染 contents 並最後在主線程設置 contents。

這個代碼將會添加一個過渡動畫,這樣 contents 就會淡如到 View 中。你能夠在 Apple 的 Core Animation Programming Guide 找到更多關於隱式 Layer 動畫以及 CAAction 的信息.。

編譯並確保沒有錯誤。

淡入容器 Node

你已經設置好一個 Layer 會在其 contents 被設置時淡入,你如今就要使用這個 Layer。

打開 RainforestCardCell.swift 。在 nodeConstructionOperationWithCardInfo(cardInfo:image:) 裏,在 Container Node Creation Section 開頭,改動以下行:

1
2
3
4
// REPLACE THIS LINE...
// let containerNode = ASDisplayNode()
// ...WITH THIS LINE:
let containerNode = ASDisplayNode(layerClass: AnimatedContentsDisplayLayer.self)

這會告訴容器 Node 使用 AnimatedContentsDisplayLayer 實例做爲其支持 Layer,所以自動帶來淡入的效果。

注意:只有 _ASDisplayLayer 的子類才能被異步地繪製。

編譯並運行。你將看到容器 Node 會在其繪製好以後淡入。

15.png

又往何處去?

恭喜!在你須要高性能地滑動你的用戶界面的時候,你有了另一個工具在手。

在本教程裏,你經過替換視圖層次結構爲一個柵格化的 AsyncDisplayKit Node 層次結構,顯著改善了一個性能不好的 Collection View 的滑動性能。多麼使人激動!

這只是一個例子而已。AsyncDisplayKit 保有提升 UI 性能到必定水平的承諾,這經過日常的 UIKit 優化每每難以達到。

實際說來,要充分利用 AsyncDisplayKit,你須要對標準 UIKit 的真正性能瓶頸的所在有足夠的瞭解。AsyncDisplayKit 很棒的一點是它引起咱們探討這些問題並思考咱們的 App 能如何在物理的極限上更快以及更具響應性。

AsyncDisplayKit 是探討此性能前沿的一個很是強大的工具。明智地使用它,並步步逼近超級響應UI的極限。

這僅僅是 AsyncDisplayKit 的一個開始!它做者和貢獻者天天都在構建新的特性。請關注 1.1 版的 ASCollectionView 以及 ASMultiplexImageNode。從頭文件中可看到「ASMultiplexImageNode 是一個圖像 Node,它能加載並顯示一個圖像的多個版本。例如,它能夠在高分辨率的圖像還在渲染時先顯示一個低分辨率的圖像。」 很是酷,對吧 :]

你能夠在此下載最終的 Xcode 項目。

AsyncDisplayKit 的指導在這裏,AsyncDisplayKit 的 Github 倉庫在這裏。

這個庫的做者在收集 API 設計的反饋。你能夠在 Facebook 上 的 Paper Engineering Community group 分享你的想法,或者直接參與到 AsyncDisplayKit 的開發中,經過 GitHub 貢獻你的 pull request。

相關文章
相關標籤/搜索