[貝聊科技]AsyncDisplayKit近一年的使用體會及疑難點

歡迎關注個人微博以便交流:輕墨node

一個第三方庫能作到像新產品同樣,值得你們去寫寫使用體會的,並很少見,AsyncDisplayKit卻徹底能夠,由於AsyncDisplayKit不只僅是一個工具,它更像一個系統UI框架,改變整個編碼體驗。也正是這種極強的侵入性,致使很多聽過、star過,甚至下過demo跑過AsyncDisplayKit的你我,望而卻步,駐足觀望。但列表界面稍微複雜時,煩人的高度計算,由於性能不得不放棄Autolayout而選擇上古時代的frame layout,使人精疲力盡,這時AsyncDisplayKit總會不天然浮現眼前,讓你躍躍欲試。git

去年10月份,咱們入坑了。github

當時還只是拿簡單的列表頁試水,基本上手後,去年末在稍微空閒的時候用AsyncDisplayKit重構了帖子詳情,今年三月份,又藉着公司聊天增長羣聊的契機,用AsyncDisplayKit重構整個聊天。林林總總,從簡單到複雜,踩過的坑大大小小,將近一年的時光轉眼飛逝,能夠寫寫總結了。shell

學習曲線

先說說學習曲線,這是你們都比較關心的問題。swift

跟大多人同樣,一開始我覺得AsyncDisplayKit會像RxswiftMVVM框架同樣,有着陡峭的學習曲線。但事實上,AsyncDisplayKit的學習曲線還算平滑。api

主要是由於AsyncDisplayKit只是對UIKit的再一次封裝,基本沿用了UIKitAPI設計,大部分狀況下,只是將view改爲nodeUI前綴改成AS,寫着寫着,恍惚間,你覺得本身仍是在寫UIKit呢。緩存

好比ASDisplayNodeUIView安全

let nodeA = ASDisplayNode()
let nodeB = ASDisplayNode()
let nodeC = ASDisplayNode()
nodeA.addSubnode(nodeB)
nodeA.addSubnode(nodeC)
nodeA.backgroundColor = .red
nodeA.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
nodeC.removeFromSupernode()

let viewA = UIView()
let viewB = UIView()
let viewC = UIView()
viewA.addSubview(viewB)
viewA.addSubview(viewC)
viewA.backgroundColor = .red
viewA.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
viewC.removeFromSuperview()複製代碼

相信你看兩眼也就摸出門道了,大部分API如出一轍。網絡

真正發生翻天覆地變化的是佈局方式,AsyncDisplayKit用的是flexbox佈局,UIView使用的是Autolayout。用AsyncDisplayKitflexbox佈局替代Autolayout佈局,徹底不亞於用Autolayout替換frame佈局的蛻變,須要比較大的觀念轉變。架構

flexbox佈局被提出已久,且其自己直觀簡單,較容易上手,學習曲線只是略陡峭。

集中精力,總體上兩天便可上手,無須擔憂學習曲線問題。

這裏有一個學習AsyncDisplayKit佈局的小遊戲,簡單有趣,能夠一玩。

體會

當過了上手的艱難階段後,纔是真正開始體會AsyncDisplayKit的時候。用了將近一年,有幾點AsyncDisplayKit的優點至關明顯:

1)cell中不再用算高度和位置等frame信息了
這是很是很是很是很是誘人的,當cell中有動態文本時,文本的高度計算很費神,計算完,還得緩存,若是再加上其餘動態內容,好比有時候沒圖片,那frame算起來,簡直讓人想哭,而若是用AsyncDisplayKit,全部的heightframe計算都煙消雲散,甚至都不知道frame這個東西存在過。

2)一幀不掉
平時界面稍微動態點,元素稍微多點,Autolayout的性能就不堪重用,而上古時代的frame佈局在高效緩存的基礎上確實能夠作到高性能,但frame緩存的維護和計算都不是通常的複雜,而AsyncDisplayKit卻能在保持簡介佈局的同時,作到一幀不掉,這是多麼的讓人感動!

3)更優雅的架構設計
前兩點好處是用AsyncDisplayKit最直接最容易被感覺到的,其實,當深刻使用時,你會發現,AsyncDisplayKit還會給程序架構設計帶來一些改變,會使本來複雜的架構變得更簡單,更優雅,更靈活,更容易維護,更容易擴展,也會使整個代碼更容易理解,而這個影響是深遠的,畢竟代碼是寫給別人看的。

AsyncDisplayKit有一個極其著名的問題,閃爍。

當咱們開始試水使用AsyncDisplayKit時,只要簡單reload一下TableNode,那閃爍,眼睛都瞎了。後來查了官方的issue,才發現不少人都提了這個問題,但官方也沒給出什麼優雅的解決方案。要知道,閃爍是很是影響用戶體驗的。若是非要在不閃爍和帶閃爍的AsyncDisplayKit中選擇,我會絕不猶豫的選擇不閃爍,而放棄使用AsyncDisplayKit。但如今已經不存在這個選擇了,由於通過AsyncDisplayKit的屢次迭代努力加上一些小技巧,AsyncDisplayKit的異步閃爍已經被優雅的解決了。

AsyncDisplayKit不宜普遍使用,那些高度固定、UI簡單的用UIKit就行了,畢竟AsyncDisplayKit並不像UIKit,人人都會。但若是內容和高度複雜又很動態,強烈推薦AsyncDisplayKit,它會簡化太多東西。

疑難點

一年的AsyncDisplayKit使用經驗,踩過了很多坑,遇到了很多值得注意的問題,一併列在這裏,以供參考。

ASNetworkImageNode的緩存

ASNetworkImageNode是對UIImageView須要從網絡加載圖片這一使用場景的封裝,省去了YYWebImage或者SDWebImage等第三方庫的引入,只須要設置URL便可實現網絡圖片的自動加載。

import AsyncDisplayKit

let avatarImageNode = ASNetworkImageNode()
avatarImageNode.url = URL(string: "http://shellhue.github.io/images/log.png")複製代碼

這很是省事便捷,但ASNetworkImageNode默認用的緩存機制和圖片下載器是PinRemoteImage,爲了使用咱們本身的緩存機制和圖片下載器,須要實現ASImageCacheProtocol圖片緩存協議和 ASImageDownloaderProtocol圖片下載器協議兩個協議,而後初始化時,用ASNetworkImageNodeinit(cache: ASImageCacheProtocol, downloader: ASImageDownloaderProtocol)初始化方法,傳入對應的類,方便其間,通常會自定義一個初始化靜態方法。咱們公司緩存機制和圖片下載器都是用的YYWebImage,橋接代碼以下。

import YYWebImage
import AsyncDisplayKit

extension ASNetworkImageNode {
  static func imageNode() -> ASNetworkImageNode {
    let manager = YYWebImageManager.shared()
    return ASNetworkImageNode(cache: manager, downloader: manager)
  }
}

extension YYWebImageManager: ASImageCacheProtocol, ASImageDownloaderProtocol {
  public func downloadImage(with URL: URL, callbackQueue: DispatchQueue, downloadProgress: AsyncDisplayKit.ASImageDownloaderProgress?, completion: @escaping AsyncDisplayKit.ASImageDownloaderCompletion) -> Any? {
    weak var operation: YYWebImageOperation?
    operation = requestImage(with: URL,
                             options: .setImageWithFadeAnimation,
                             progress: { (received, expected) -> Void in
                              callbackQueue.async(execute: {
                                let progress = expected == 0 ? 0 : received / expected
                                downloadProgress?(CGFloat(progress))
                              })
    }, transform: nil, completion: { (image, url, from, state, error) in
      completion(image, error, operation)
    })

    return operation
  }

  public func cancelImageDownload(forIdentifier downloadIdentifier: Any) {
    guard let operation = downloadIdentifier as? YYWebImageOperation else {
      return
    }
    operation.cancel()
  }

  public func cachedImage(with URL: URL, callbackQueue: DispatchQueue, completion: @escaping AsyncDisplayKit.ASImageCacherCompletion) {
    cache?.getImageForKey(cacheKey(for: URL), with: .all, with: { (image, cacheType) in
      callbackQueue.async {
        completion(image)
      }
    })
  }
}複製代碼

閃爍

初次使用AsyncDisplayKit,當享受其一幀不掉如絲般柔滑的手感時,ASTableNodeASCollectionNode刷新時的閃爍必定讓你幾度崩潰,到AsyncDisplayKitgithub上搜索閃爍相關issue,會出來100多個問題。閃爍是AsyncDisplayKit與生俱來的問題,聞名遐邇,而閃爍的體驗很是糟糕。幸運的是,幾經探索,AsyncDisplayKit的閃爍問題已經完美解決,這個完美指的是一幀不掉的同時沒有任何閃爍,同時也沒增長代碼的複雜度。

閃爍能夠分爲四類,

1)ASNetworkImageNode reload時的閃爍

ASCellNode中包含ASNetworkImageNode,則這個cell reload時,ASNetworkImageNode會異步從本地緩存或者網絡請求圖片,請求到圖片後再設置ASNetworkImageNode展現圖片,但在異步過程當中,ASNetworkImageNode會先展現PlaceholderImage,從PlaceholderImage--->fetched image的展現替換致使閃爍發生,即便整個cell的數據沒有任何變化,只是簡單的reloadASNetworkImageNode的圖片加載邏輯依然不變,所以仍然會閃爍,這顯著區別於UIImageView,由於YYWebImage或者SDWebImageUIImageViewimage設置邏輯是,先同步檢查有無內存緩存,有的話直接顯示,沒有的話再先顯示PlaceholderImage,等待加載完成後再顯示加載的圖片,也即邏輯是memory cached image--->PlaceholderImage--->fetched image的邏輯,刷新當前cell時,若是數據沒有變化memory cached image通常都會有,所以不會閃爍。

AsyncDisplayKit官方給的修復思路是:

import AsyncDisplayKit

let node = ASNetworkImageNode()
node.placeholderColor = UIColor.red
node.placeholderFadeDuration = 3複製代碼

這樣修改後,確實沒有閃爍了,但這只是將PlaceholderImage--->fetched image圖片替換致使的閃爍拉長到3秒而已,自欺欺人,並無修復。

既然閃爍是reload時,沒有事先同步檢查有無緩存致使的,繼承一個ASNetworkImageNode的子類,複寫url設置邏輯:

import AsyncDisplayKit

class NetworkImageNode: ASNetworkImageNode {
  override var url: URL? {
    didSet {
      if let u = url,
        let image = UIImage.cachedImage(with: u) else {
        self.image = image
        placeholderEnabled = false
      }
    }
  }
}複製代碼

按道理不會閃爍了,但事實上仍然會,只要是個ASNetworkImageNode,不管怎麼設置,都會閃,這與官方的API說明嚴重不符,很無語。無可奈何之下,當有緩存時,直接用ASImageNode替換ASNetworkImageNode

import AsyncDisplayKit

class NetworkImageNode: ASDisplayNode {
  private var networkImageNode = ASNetworkImageNode.imageNode()
  private var imageNode = ASImageNode()

  var placeholderColor: UIColor? {
    didSet {
      networkImageNode.placeholderColor = placeholderColor
    }
  }

  var image: UIImage? {
    didSet {
      networkImageNode.image = image
    }
  }

  override var placeholderFadeDuration: TimeInterval {
    didSet {
      networkImageNode.placeholderFadeDuration = placeholderFadeDuration
    }
  }

  var url: URL? {
    didSet {
      guard let u = url,
        let image = UIImage.cachedImage(with: u) else {
          networkImageNode.url = url
          return
      }

      imageNode.image = image
    }
  }

  override init() {
    super.init()
    addSubnode(networkImageNode)
    addSubnode(imageNode)
  }

  override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    return ASInsetLayoutSpec(insets: .zero,
                             child: networkImageNode.url == nil ? imageNode : networkImageNode)
  }

  func addTarget(_ target: Any?, action: Selector, forControlEvents controlEvents: ASControlNodeEvent) {
    networkImageNode.addTarget(target, action: action, forControlEvents: controlEvents)
    imageNode.addTarget(target, action: action, forControlEvents: controlEvents)
  }
}複製代碼

使用時將NetworkImageNode當成ASNetworkImageNode使用便可。

2)reload 單個cell時的閃爍

reload ASTableNode或者ASCollectionNode的某個indexPathcell時,也會閃爍。緣由和ASNetworkImageNode很像,都是異步惹的禍。當異步計算cell的佈局時,cell使用placeholder佔位(一般是白圖),佈局完成時,才用渲染好的內容填充cellplaceholder到渲染好的內容切換引發閃爍。UITableViewCell由於都是同步,不存在佔位圖的狀況,所以也就不會閃。

先看官方的修改方案,

func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {
  let cell = ASCellNode()
  ... // 其餘代碼

  cell.neverShowPlaceholders = true

  return cell
}複製代碼

這個方案很是有效,由於設置cell.neverShowPlaceholders = true,會讓cell從異步狀態衰退回同步狀態,若reload某個indexPathcell,在渲染完成以前,主線程是卡死的,這與UITableView的機制同樣,但速度會比UITableView快不少,由於UITableView的佈局計算、資源解壓、視圖合成等都是在主線程進行,而ASTableNode則是多個線程併發進行,況且佈局等還有緩存。因此,通常也沒有問題,貝聊的聊天界面只是簡單這樣設置後,就不閃了,並且一幀不掉。但當頁面佈局較爲複雜時,滑動時的卡頓掉幀就變的肉眼可見。

這時,能夠設置ASTableNodeleadingScreensForBatching減緩卡頓

override func viewDidLoad() {
  super.viewDidLoad()
  ... // 其餘代碼

  tableNode.leadingScreensForBatching = 4
}複製代碼

通常設置tableNode.leadingScreensForBatching = 4即提早計算四個屏幕的內容時,掉幀就很不明顯了,典型的空間換時間。但仍不完美,仍然會掉幀,而咱們指望的是一幀不掉,如絲般順滑。這不難,基於上面不閃的方案,刷點小聰明就能解決。

class ViewController: ASViewController {
  ... // 其餘代碼
  private var indexPathesToBeReloaded: [IndexPath] = []

  func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {
    let cell = ASCellNode()
    ... // 其餘代碼

    cell.neverShowPlaceholders = false
    if indexPathesToBeReloaded.contains(indexPath) {
      let oldCellNode = tableNode.nodeForRow(at: indexPath)
      cell.neverShowPlaceholders = true
      oldCellNode?.neverShowPlaceholders = true
      DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
        cell.neverShowPlaceholders = false
        if let indexP = self.indexPathesToBeReloaded.index(of: indexPath) {
          self.indexPathesToBeReloaded.remove(at: indexP)
        }
      })
    }
    return cell
  }

  func reloadActionHappensHere() {
    ... // 其餘代碼

    let indexPath = ... // 須要roload的indexPath
      indexPathesToBeReloaded.append(indexPath)
    tableNode.reloadRows(at: [indexPath], with: .none)
  }
}複製代碼

關鍵代碼是,

if indexPathesToBeReloaded.contains(indexPath) {
  let oldCellNode = tableNode.nodeForRow(at: indexPath)
  cell.neverShowPlaceholders = true
  oldCellNode?.neverShowPlaceholders = true
  DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
    cell.neverShowPlaceholders = false
    if let indexP = self.indexPathesToBeReloaded.index(of: indexPath) {
      self.indexPathesToBeReloaded.remove(at: indexP)
    }
  })
}複製代碼

即,檢查當前的indexPath是否被標記,若是是,則先設置cell.neverShowPlaceholders = true,等待reload完成(一幀是1/60秒,這裏等待0.5秒,足夠渲染了),將cell.neverShowPlaceholders = false。這樣reload時既不會閃爍,也不會影響滑動時的異步繪製,所以一幀不掉。

這徹底是耍小聰明的作法,但確實很是有效。

3)reloadData時的閃爍

在下拉刷新後,列表常常須要從新刷新,即調用ASTableNode或者ASCollectionNodereloadData方法,但會閃,並且很明顯。有了單個cell reload時閃爍的解決方案後,此類閃爍解決起來,就很簡單了。

func reloadDataActionHappensHere() {
  ... // 其餘代碼

  let count = tableNode.dataSource?.tableNode?(tableNode, numberOfRowsInSection: 0) ?? 0
  if count > 2 {
    // 將肉眼可見的cell添加進indexPathesToBeReloaded中
    indexPathesToBeReloaded.append(IndexPath(row: 0, section: 0))
    indexPathesToBeReloaded.append(IndexPath(row: 1, section: 0))
    indexPathesToBeReloaded.append(IndexPath(row: 2, section: 0))
  }
  tableNode.reloadData()

  ... // 其餘代碼
}複製代碼

將肉眼可見的cell添加進indexPathesToBeReloaded中便可。

4)insertItems時更改ASCollectionNode的contentOffset引發的閃爍

咱們公司的聊天界面是用AsyncDisplayKit寫的,當下拉加載更多新消息時,爲保持加載後當前消息的位置不變,須要在collectionNode.insertItems(at: indexPaths)完成後,復原collectionNode.view.contentOffset,代碼以下:

func insertMessagesToTop(indexPathes: [IndexPath]) {
  let originalContentSizeHeight = collectionNode.view.contentSize.height
  let originalContentOffsetY = collectionNode.view.contentOffset.y
  let heightFromOriginToContentBottom = originalContentSizeHeight - originalContentOffsetY
  let heightFromOriginToContentTop = originalContentOffsetY
  collectionNode.performBatch(animated: false, updates: {
    self.collectionNode.insertItems(at: indexPaths)
  }) { (finished) in
    let contentSizeHeight = self.collectionNode.view.contentSize.height
    self.collectionNode.view.contentOffset = CGPointMake(0, isLoadingMore ? (contentSizeHeight - heightFromOriginToContentBottom) : heightFromOriginToContentTop)
  }
}複製代碼

遺憾的是,會閃爍。起初覺得是AsyncDisplayKit異步繪製致使的閃爍,一度還想放棄AsyncDisplayKit,用UITableView重寫一遍,幸運的是,當時項目工期太緊,沒有時間重寫,也沒時間仔細排查,直接帶問題上線了。

最近閒暇,經仔細排查,方知不是AsyncDisplayKit的鍋,但也比較難修,有必定的參考價值,所以一併列在這裏。

閃爍的緣由是,collectionNode insertItems成功後會先繪製contentOffsetCGPoint(x: 0, y: 0)時的一幀畫面,無動畫時這一幀畫面當即顯示,而後調用成功回調,回調中復原了collectionNode.view.contentOffset,下一幀就顯示覆原了位置的畫面,先後有變化所以閃爍。這是作消息類APP一併會遇到的bug,google一下,主要有兩種解決方案,

第一種,經過仿射變換倒置ASCollectionNode,這樣下拉加載更多,就變成正常列表的上拉加載更多,也就無需移動contentOffsetASCollectionNode還特地設置了個屬性inverted,方便你們開發。然而這種方案換湯不換藥,當收到新消息,同時正在查看歷史消息,依然須要插入新消息並復原contentOffset,閃爍依然在其餘情形下發生。

第二種,集成一個UICollectionViewFlowLayout,重寫prepare()方法,作相應處理便可。這個方案完美,簡介優雅。子類化的CollectionFlowLayout以下:

class CollectionFlowLayout: UICollectionViewFlowLayout {
  var isInsertingToTop = false
  override func prepare() {
    super.prepare()
    guard let collectionView = collectionView else {
      return
    }
    if !isInsertingToTop {
      return
    }
    let oldSize = collectionView.contentSize
    let newSize = collectionViewContentSize
    let contentOffsetY = collectionView.contentOffset.y + newSize.height - oldSize.height
    collectionView.setContentOffset(CGPoint(x: collectionView.contentOffset.x, y: contentOffsetY), animated: false)
  }
}複製代碼

當須要insertItems而且保持位置時,將CollectionFlowLayoutisInsertingToTop設置爲true便可,完成後再設置爲false。以下,

class MessagesViewController: ASViewController {
  ... // 其餘代碼
  var collectionNode: ASCollectionNode!
  var flowLayout: CollectionFlowLayout!
  override func viewDidLoad() {
    super.viewDidLoad()
    flowLayout = CollectionFlowLayout()
    collectionNode = ASCollectionNode(collectionViewLayout: flowLayout)
    ... // 其餘代碼
  }

  ... // 其餘代碼

  func insertMessagesToTop(indexPathes: [IndexPath]) {
    flowLayout.isInsertingToTop = true
    collectionNode.performBatch(animated: false, updates: {
      self.collectionNode.insertItems(at: indexPaths)
    }) { (finished) in
      self.flowLayout.isInsertingToTop = false
    }
  }

  ... // 其餘代碼
}複製代碼

佈局

AsyncDisplayKit採用的是flexbox的佈局思想,很是高效直觀簡潔,但畢竟迥異於AutoLayoutframe layout的佈局風格,咋一上手,很不習慣,有些小技巧仍是須要慢慢積累,有些概念也須要逐漸熟悉深刻,下面列舉幾個筆者以爲比較重要的概念

1)設置任意間距

AutoLayout實現任意間距,比較容易直觀,由於AutoLayout的約束,原本就是個人邊離你的邊有多遠的概念,而AsyncDisplayKit並無,AsyncDisplayKit裏面的概念是,我本身的前面有多少空白距離,我本身的後面有多少空白距離,更強調本身。假若有三個元素,怎麼約束它們之間的間距?

AutoLayout是這樣的:

import Masonry
class SomeView: UIView {
  override init() {
    super.init()
    let viewA = UIView()
    let viewB = UIView()
    let viewC = UIView()
    addSubview(viewA)
    addSubview(viewB)
    addSubview(viewC)

    viewB.snp.makeConstraints { (make) in
      make.left.equalTo(viewA.snp.right).offset(15)
    }

    viewC.snp.makeConstraints { (make) in
      make.left.equalTo(viewB.snp.right).offset(5)
    }
  }
}複製代碼

AsyncDisplayKit是這樣的:

import AsyncDisplayKit
class SomeNode: ASDisplayNode {
  let nodeA = ASDisplayNode()
  let nodeB = ASDisplayNode()
  let nodeC = ASDisplayNode()
  override init() {
    super.init()
    addSubnode(nodeA)
    addSubnode(nodeB)
    addSubnode(nodeC)
  }
  override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    nodeB.style.spaceBefore = 15
    nodeC.stlye.spaceBefore = 5

    return ASStackLayoutSpec(direction: .horizontal, spacing: 0, justifyContent: .start, alignItems: .start, children: [nodeA, nodeB, nodeC])
  }
}複製代碼

若是是拿ASStackLayoutSpec佈局,元素之間的任意間距通常是經過元素本身的spaceBefore或者spaceBefore style實現,這是自我包裹性,更容易理解,若是不是拿ASStackLayoutSpec佈局,能夠將某個元素包裹成ASInsetsLayoutSpec,再設置UIEdgesInsets,保持本身的四周任意邊距。

能任意設置間距是自由佈局的基礎。

2)flexGrow和flexShrink

flexGrowflexShrink是至關重要的概念,flexGrow是指當有多餘空間時,拉伸誰以及相應的拉伸比例(當有多個元素設置了flexGrow時),flexShrink相反,是指當空間不夠時,壓縮誰及相應的壓縮比例(當有多個元素設置了flexShrink時)。
靈活使用flexGrowspacer(佔位ASLayoutSpec)能夠實現不少效果,好比等間距,

實現代碼以下,

import AsyncDisplayKit
class ContainerNode: ASDisplayNode {
  let nodeA = ASDisplayNode()
  let nodeB = ASDisplayNode()
  override init() {
    super.init()
    addSubnode(nodeA)
    addSubnode(nodeB)
  }
  override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    let spacer1 = ASLayoutSpec()
    let spacer2 = ASLayoutSpec()
    let spacer3 = ASLayoutSpec()
    spacer1.stlye.flexGrow = 1
    spacer2.stlye.flexGrow = 1
    spacer3.stlye.flexGrow = 1

    return ASStackLayoutSpec(direction: .horizontal, spacing: 0, justifyContent: .start, alignItems: .start, children: [spacer1, nodeA,spacer2, nodeB, spacer3])
  }
}複製代碼

若是spacerflexGrow不一樣就能夠實現指定比例的佈局,再結合width樣式,輕鬆實現如下佈局

佈局代碼以下,

override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
  let spacer1 = ASLayoutSpec()
  let spacer2 = ASLayoutSpec()
  let spacer3 = ASLayoutSpec()
  spacer1.stlye.flexGrow = 2
  spacer2.stlye.width = ASDimensionMake(100)
  spacer3.stlye.flexGrow = 1

  return ASStackLayoutSpec(direction: .horizontal, spacing: 0, justifyContent: .start, alignItems: .start, children: [spacer1, nodeA,spacer2, nodeB, spacer3])
}複製代碼

相同的佈局若是用Autolayout,麻煩去了。

3)constrainedSize的理解

constrainedSize是指某個node的大小取值範圍,有minSizemaxSize兩個屬性。好比下圖的佈局:

import AsyncDisplayKit
class ContainerNode: ASDisplayNode {
  let nodeA = ASDisplayNode()
  let nodeB = ASDisplayNode()
  override init() {
    super.init()
    addSubnode(nodeA)
    addSubnode(nodeB)
    nodeA.style.preferredSize = CGSize(width: 100, height: 100)
  }

  override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
    nodeB.style.flexShrink = 1
    nodeB.style.flexGrow = 1
    let stack = ASStackLayoutSpec(direction: .horizontal, spacing: e, justifyContent: .start, alignItems: .start, children: [nodeA, nodeB])
    return ASInsetLayoutSpec(insets: UIEdgeInsetsMake(a, b, c, d), child: stack)
  }
}複製代碼

其中方法override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec中的constrainedSize所指是ContainerNode自身大小的取值範圍。給定constrainedSizeAsyncDisplayKit會根據ContainerNodelayoutSpecThatFits(_:)中施加在nodeA、nodeB的佈局規則和nodeA、nodeB自身屬性計算nodeA、nodeBconstrainedSize

假如constrainedSizeminSizeCGSize(width: 0, height: 0)maxSizeCGSize(width: 375, height: Inf+)(Inf+爲正無限大),則:

1)根據佈局規則和nodeA自身樣式屬性maxWidthminWidthwidthheightpreferredSize,可計算出nodeAconstrainedSizeminSizemaxSize均爲其preferredSizeCGSize(width: 100, height: 100),由於佈局規則爲水平向的ASStackLayout,當空間富餘或者空間不足時,nodeA即不壓縮又不拉伸,因此會取其指定的preferredSize

2)根據佈局規則和nodeB自身樣式屬性maxWidthminWidthwidthheightpreferredSize,能夠計算出其constrainedSizeminSizeCGSize(width: 0, height: 0)maxSizeCGSize(width: 375 - 100 - b - e - d, height: Inf+),由於nodeBflexShrinkflexGrow均爲1,也即當空間富餘或者空間不足時,nodeB添滿富餘空間或壓縮至空間夠爲止。

若是不指定nodeBflexShrinkflexGrow,那麼當空間富餘或者空間不足時,AsyncDisplayKit就不知道壓縮和拉伸哪個佈局元素,則nodeBconstrainedSizemaxSize就變爲CGSize(width: Inf+, height: Inf+),即徹底無大小限制,可想而知,nodeB的子node的佈局將徹底不對。這也說明另一個問題,nodeconstrainedSize並非必定大於其子nodeconstrainedSize

理解constrainedSize的計算,才能熟練利用node的樣式maxWidthminWidthwidthheightpreferredSizeflexShrinkflexGrow進行佈局。若是發現佈局結果不對,而對應node的佈局代碼確是正確無誤,通常極有多是由於此node的父佈局元素不正確。

動畫

由於AsyncDisplayKit的佈局方式有兩種,frame佈局和flexbox式的佈局,相應的動畫方式也有兩種

1)frame佈局

若是採用的是frame佈局,動畫跟普通的UIView相同

class ViewController: ASViewController {
  let nodeA = ASDisplayNode()
  override func viewDidLoad() {
    super.viewDidLoad()
    nodeA.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
    ... // 其餘代碼
  }

  ... // 其餘代碼
  func animateNodeA() {
    UIView.animate(withDuration: 0.5) { 
      let newFrame = ... // 新的frame
      nodeA.frame = newFrame
    }
  }
}複製代碼

不要以爲用了AsyncDisplayKit就告別了frame佈局,ViewController中主要元素個數不多,佈局簡單,所以,通常也仍是採用frame layout,若是隻是作一些簡單的動畫,直接採用UIView的動畫API便可

2)flexbox式的佈局

這種佈局方式,是在某個子node中經常使用的,若是node內部佈局發生了變化,又須要作動畫時,就須要複寫AsyncDisplayKit的動畫API,並基於提供的動畫上下文類context,作動畫:

class SomeNode: ASDisplayNode {
  let nodeA = ASDisplayNode()

  override func animateLayoutTransition(_ context: ASContextTransitioning) {
    // 利用context能夠獲取animate先後佈局信息

    UIView.animate(withDuration: 0.5) { 
      // 不使用系統默認的fade動畫,採用自定義動畫
      let newFrame = ... // 新的frame
      nodeA.frame = newFrame
    }
  }
}複製代碼

系統默認的動畫是漸隱漸顯,能夠獲取animate先後佈局信息,好比某個子node兩種佈局中的frame,而後再自定義動畫類型。若是想觸發動畫,主動調用SomeNode的觸發方法transitionLayout(withAnimation:shouldMeasureAsync:measurementCompletion:)便可。

內存泄漏

爲了方便將一個UIView或者CALayer轉化爲一個ASDisplayNode,系統提供了用block初始化ASDisplayNode的簡便方法:

public convenience init(viewBlock: @escaping AsyncDisplayKit.ASDisplayNodeViewBlock)
public convenience init(viewBlock: @escaping AsyncDisplayKit.ASDisplayNodeViewBlock, didLoad didLoadBlock: AsyncDisplayKit.ASDisplayNodeDidLoadBlock? = nil)
public convenience init(layerBlock: @escaping AsyncDisplayKit.ASDisplayNodeLayerBlock)
public convenience init(layerBlock: @escaping AsyncDisplayKit.ASDisplayNodeLayerBlock, didLoad didLoadBlock: AsyncDisplayKit.ASDisplayNodeDidLoadBlock? = nil)複製代碼

須要注意的是所傳入的block會被要建立的node持有。若是block中反過來持有了這個node的持有者,則會產生循環引用,致使內存泄漏:

class SomeNode {
  var nodeA: ASDisplayNode!
  let color = UIColor.red
  override init() {
    super.init()
    nodeA = ASDisplayNode {
      let view = UIView()
      view.backgroundColor = self.color // 內存泄漏
      return view
    }
  }
}複製代碼

子線程崩潰

AsyncDisplayKit的性能優點來源於異步繪製,異步的意思是有時候node會在子線程建立,若是繼承了一個ASDisplayNode,一不當心在初始化時調用了UIKit的相關方法,則會出現子線程崩潰。好比如下node

class SomeNode {
  let iconImageNode: ASDisplayNode
  let color = UIColor.red
  override init() {
    iconImageNode = ASImageNode()
    iconImageNode.image = UIImage(named: "iconName") // 需注意SomeNode有時會在子線程初始化,而UIImage(named:)並非線程安全

    super.init()

  }
}複製代碼

但在node初始化時調用UIImage(named:)建立圖片是不可避免的,用methodSwizzleUIImage(named:)置換成安全的便可。

其實在子線程初始化node並很少見,通常都在主線程。

總結

一年的實踐下來,閃爍是AsyncDisplayKit遇到的最大的問題,修復起來也頗爲費神。其餘bug,有時雖然很讓人頭疼,但因爲AsyncDisplayKit是對UIKit的再封裝,實在不行,仍然能夠越過AsyncDisplayKitUIKit的方法修復。

學習曲線也不算很陡峭。

考慮到AsyncDisplayKit的種種好處,很是推薦AsyncDisplayKit,固然仍是僅限於用在比較複雜和動態的頁面中。

我的博客原文連接:qingmo.me/
歡迎關注個人微博以便交流:輕墨

相關文章
相關標籤/搜索