国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁(yè) > 學(xué)院 > 開(kāi)發(fā)設(shè)計(jì) > 正文

[轉(zhuǎn)]AsyncDisplayKit教程:達(dá)到60FPS的滾動(dòng)幀率

2019-11-14 19:42:56
字體:
來(lái)源:轉(zhuǎn)載
供稿:網(wǎng)友

[原文: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é)保證了文字的清晰易讀。

01.png

在 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ì)看到如下界面:

02.png

試著滑動(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)行,你看到的就全是空空如也的卡片:

03.jpg

現(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)了:

04.png

樸素的 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 Section
backgroundImageNode.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 Section
self.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)界面。

05.png

如果你運(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 = nil
backgroundImageNode = nil

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

編譯并運(yùn)行。這次,沒(méi)有 Sublayer 會(huì)堆積的問(wèn)題,且所有不必要的繪制都會(huì)被取消。

06.png

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

07.png

模糊圖像

要模糊圖像,你要添加一個(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)行,觀察模糊效果:

08.png

注意你可以如何非常流暢地滑動(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í)。

08.png

當(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 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

這就創(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 Section
containerNode.addSubnode(backgroundImageNode)

注意:添加 Node 的順序很重要,就如同 subview 和 sublayer。最先添加的 Node 會(huì)被之后添加的阻擋顯示。

替換 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)

這設(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)有的方式一樣。

09.png

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 = true
featureImageNode.contentMode = .ScaleAspectFit
featureImageNode.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 在起作用。

10.png

添加 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 = true
titleTextNode.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í)才渲染。

11.png

添加 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 = true
descriptionTextNode.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 文本了。

12.png

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 = false
gradientNode.layerBacked = true

這就創(chuàng)建了一個(gè)叫做 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 需要這個(gè)位置才能在特征圖之上,Title 之下。

然后添加如下代碼到 Node Layout Section 底部:

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

編譯并運(yùn)行。你將看到梯度在特征圖的底部。Title 確實(shí)看得更清楚了!

13.png

爆米花特效

如之前提到的,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.layer
strongSelf.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
// 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. 回憶到當(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 = newNodeConstructionOperation
nodeConstructionQueue.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)如黃油般順滑。

14.png

淡入 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ì)在其繪制好之后淡入。

15.png

又往何處去?

恭喜!在你需要高性能地滑動(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。


發(fā)表評(píng)論 共有條評(píng)論
用戶名: 密碼:
驗(yàn)證碼: 匿名發(fā)表
主站蜘蛛池模板: 余干县| 翼城县| 修文县| 团风县| 长岭县| 平泉县| 进贤县| 天柱县| 东平县| 武胜县| 侯马市| 弋阳县| 永城市| 当阳市| 泾源县| 诸暨市| 邹平县| 屏南县| 来安县| 沾化县| 扎兰屯市| 杭锦后旗| 沧州市| 濮阳市| 容城县| 河源市| 沙河市| 乐平市| 库尔勒市| 鹤山市| 甘德县| 安丘市| 页游| 玉田县| 丰城市| 石城县| 元朗区| 江津市| 十堰市| 云霄县| 大连市|