[贝聊科技]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/
欢迎关注个人微博以便交流:轻墨

相关文章
相关标签/搜索