AsyncDisplayKit近一年的使用体会及疑难点

lzxys 7年前
   <p>一个第三方库能做到像新产品一样,值得大家去写写使用体会的,并不多见, AsyncDisplayKit 却完全可以,因为 AsyncDisplayKit 不仅仅是一个工具,它更像一个系统UI框架,改变整个编码体验。也正是这种极强的侵入性,导致不少听过、star过,甚至下过demo跑过 AsyncDisplayKit 的你我,望而却步,驻足观望。但列表界面稍微复杂时,烦人的高度计算,因为性能不得不放弃 Autolayout 而选择上古时代的 frame layout ,令人精疲力尽,这时 AsyncDisplayKit 总会不自然浮现眼前,让你跃跃欲试。</p>    <p>去年10月份,我们入坑了。</p>    <p>当时还只是拿简单的列表页试水,基本上手后,去年底在稍微空闲的时候用 AsyncDisplayKit 重构了帖子详情,今年三月份,又借着公司聊天增加群聊的契机,用 AsyncDisplayKit 重构整个聊天。林林总总,从简单到复杂,踩过的坑大大小小,将近一年的时光转眼飞逝,可以写写总结了。</p>    <h2>学习曲线</h2>    <p>先说说学习曲线,这是大家都比较关心的问题。</p>    <p>跟大多人一样,一开始我以为 AsyncDisplayKit 会像 Rxswift 等 MVVM 框架一样,有着陡峭的学习曲线。但事实上, AsyncDisplayKit 的学习曲线还算平滑。</p>    <p>主要是因为 AsyncDisplayKit 只是对 UIKit 的再一次封装,基本沿用了 UIKit 的 API 设计,大部分情况下,只是将 view 改成 node , UI 前缀改为 AS ,写着写着,恍惚间,你以为自己还是在写 UIKit 呢。</p>    <p>比如 ASDisplayNode 与 UIView :</p>    <pre>  <code class="language-objectivec">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()  </code></pre>    <p>相信你看两眼也就摸出门道了,大部分API一模一样。</p>    <p>真正发生翻天覆地变化的是布局方式, AsyncDisplayKit 用的是 flexbox 布局, UIView 使用的是 Autolayout 。用 AsyncDisplayKit 的 flexbox 布局替代 Autolayout 布局,完全不亚于用 Autolayout 替换 frame 布局的蜕变,需要比较大的观念转变。</p>    <p>但 flexbox 布局被提出已久,且其本身直观简单,较容易上手,学习曲线只是略陡峭。</p>    <p>这里有一个学习 AsyncDisplayKit 布局的 <a href="/misc/goto?guid=4959751246687307237" rel="nofollow,noindex">小游戏</a> ,简单有趣,可以一玩。</p>    <p>整体上两天即可上手,无须担心学习曲线问题。</p>    <h2>体会</h2>    <p>当过了上手的艰难阶段后,才是真正开始体会 AsyncDisplayKit 的时候。用了将近一年,有几点 AsyncDisplayKit 的优势相当明显:</p>    <p>1) cell 中再也不用算高度和位置等 frame 信息了</p>    <p>这是非常非常非常非常诱人的,当 cell 中有动态文本时,文本的高度计算很费神,计算完,还得缓存,如果再加上其他动态内容,比如有时候没图片,那 frame 算起来,简直让人想哭,而如果用 AsyncDisplayKit ,所有的 height 、 frame 计算都烟消云散,甚至都不知道 frame 这个东西存在过,很酸爽。</p>    <p>2)一帧不掉</p>    <p>平时界面稍微动态点,元素稍微多点, Autolayout 的性能就不堪重用,而上古时代的 frame 布局在高效缓存的基础上确实可以做到高性能,但 frame 缓存的维护和计算都不是一般的复杂,而 AsyncDisplayKit 却能在保持简介布局的同时,做到一帧不掉,这是多么的让人感动!</p>    <p>3)更优雅的架构设计</p>    <p>前两点好处是用 AsyncDisplayKit 最直接最容易被感受到的,其实,当深入使用时,你会发现, AsyncDisplayKit 还会给程序架构设计带来一些改变,会使原本复杂的架构变得更简单,更优雅,更灵活,更容易维护,更容易扩展,也会使整个代码更容易理解,而这个影响是深远的,毕竟代码是写给别人看的。</p>    <p>但 AsyncDisplayKit 有一个极其著名的问题,闪烁。</p>    <p>当我们开始试水使用 AsyncDisplayKit 时,只要简单 reload 一下 TableNode ,那闪烁,眼睛都瞎了。后来查了官方的 issue ,才发现很多人都提了这个问题,但官方也没给出什么优雅的解决方案。要知道,闪烁是非常影响用户体验的。如果非要在不闪烁和带闪烁的 AsyncDisplayKit 中选择,我会毫不犹豫的选择不闪烁,而放弃使用 AsyncDisplayKit 。但现在已经不存在这个选择了,因为经过 AsyncDisplayKit 的多次迭代努力加上一些小技巧, AsyncDisplayKit 的异步闪烁已经被优雅的解决了。</p>    <p>但 AsyncDisplayKit 不宜广泛使用,那些高度固定、 UI 简单用 UIKit 更好一些,毕竟 AsyncDisplayKit 并不像 UIKit ,人人都会,如果内容和高度复杂又很动态,强烈推荐 AsyncDisplayKit ,它会简化太多东西。</p>    <h2>疑难点</h2>    <p>一年的 AsyncDisplayKit 使用经验,踩过了不少坑,遇到了不少值得注意的问题,一并列在这里,以供参考。</p>    <h2>ASNetworkImageNode的缓存</h2>    <p>ASNetworkImageNode 是对 UIImageView 需要从网络加载图片这一使用场景的封装,省去了 YYWebImage 或者 SDWebImage 等第三方库的引入,只需要设置 URL 即可实现网络图片的自动加载。</p>    <pre>  <code class="language-objectivec">import AsyncDisplayKit    let avatarImageNode = ASNetworkImageNode()  avatarImageNode.url = URL(string: "http://shellhue.github.io/images/log.png")  </code></pre>    <p>这非常省事便捷,但 ASNetworkImageNode 默认用的缓存机制和图片下载器是 PinRemoteImage ,为了使用我们自己的缓存机制和图片下载器,需要实现 ASImageCacheProtocol 图片缓存协议和 ASImageDownloaderProtocol 图片下载器协议两个协议,然后初始化时,用 ASNetworkImageNode 的 init(cache: ASImageCacheProtocol, downloader: ASImageDownloaderProtocol) 初始化方法,传入对应的类,方便其间,一般会自定义一个初始化静态方法。我们公司缓存机制和图片下载器都是用的 YYWebImage ,桥接代码如下。</p>    <pre>  <code class="language-objectivec">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)        }      })    }  }  </code></pre>    <h2>闪烁</h2>    <p>初次使用 AsyncDisplayKit ,当享受其一帧不掉如丝般柔滑的手感时, ASTableNode 和 ASCollectionNode 刷新时的闪烁一定让你几度崩溃,到 AsyncDisplayKit 的 github 上搜索闪烁相关issue,会出来100多个问题。闪烁是 AsyncDisplayKit 与生俱来的问题,闻名遐迩,而闪烁的体验非常糟糕。幸运的是,几经探索, AsyncDisplayKit 的闪烁问题已经完美解决,这个完美指的是一帧不掉的同时没有任何闪烁,同时也没增加代码的复杂度。</p>    <p>闪烁可以分为四类,</p>    <h3>1)ASNetworkImageNode reload时的闪烁</h3>    <p>当 ASCellNode 中包含 ASNetworkImageNode ,则这个 cell reload 时, ASNetworkImageNode 会异步从本地缓存或者网络请求图片,请求到图片后再设置 ASNetworkImageNode 展示图片,但在异步过程中, ASNetworkImageNode 会先展示 PlaceholderImage ,从 PlaceholderImage —> fetched image 的展示替换导致闪烁发生,即使整个 cell 的数据没有任何变化,只是简单的 reload , ASNetworkImageNode 的图片加载逻辑依然不变,因此仍然会闪烁,这显著区别于 UIImageView ,因为 YYWebImage 或者 SDWebImage 对 UIImageView 的 image 设置逻辑是,先同步检查有无内存缓存,有的话直接显示,没有的话再先显示 PlaceholderImage ,等待加载完成后再显示加载的图片,也即逻辑是 memory cached image —> PlaceholderImage —> fetched image 的逻辑,刷新当前 cell 时,如果数据没有变化 memory cached image 一般都会有,因此不会闪烁。</p>    <p>AsyncDisplayKit 官方给的修复思路是:</p>    <pre>  <code class="language-objectivec">import AsyncDisplayKit    let node = ASNetworkImageNode()  node.placeholderColor = UIColor.red  node.placeholderFadeDuration = 3  </code></pre>    <p>这样修改后,确实没有闪烁了,但这只是将 PlaceholderImage —> fetched image 图片替换导致的闪烁拉长到3秒而已,自欺欺人,并没有修复。</p>    <p>既然闪烁是 reload 时,没有事先同步检查有无缓存导致的,继承一个 ASNetworkImageNode 的子类,复写 url 设置逻辑:</p>    <pre>  <code class="language-objectivec">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        }      }    }  }  </code></pre>    <p>按道理不会闪烁了,但事实上仍然会,只要是个 ASNetworkImageNode ,无论怎么设置,都会闪,这与官方的API说明严重不符,很无语。迫不得已之下,当有缓存时,直接用 ASImageNode 替换 ASNetworkImageNode 。</p>    <pre>  <code class="language-objectivec">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)    }  }  </code></pre>    <p>使用时将 NetworkImageNode 当成 ASNetworkImageNode 使用即可。</p>    <h3>2)reload 单个cell时的闪烁</h3>    <p>当 reload ASTableNode 或者 ASCollectionNode 的某个 indexPath 的 cell 时,也会闪烁。原因和 ASNetworkImageNode 很像,都是异步惹的祸。当异步计算 cell 的布局时, cell 使用 placeholder 占位(通常是白图),布局完成时,才用渲染好的内容填充 cell , placeholder 到渲染好的内容切换引起闪烁。 UITableViewCell 因为都是同步,不存在占位图的情况,因此也就不会闪。</p>    <p>先看官方的修改方案,</p>    <pre>  <code class="language-objectivec">func tableNode(_ tableNode: ASTableNode, nodeForRowAt indexPath: IndexPath) -> ASCellNode {    let cell = ASCellNode()    ... // 其他代码          cell.neverShowPlaceholders = true        return cell  }  </code></pre>    <p>这个方案非常有效,因为设置 cell.neverShowPlaceholders = true ,会让 cell 从异步状态衰退回同步状态,若 reload 某个 indexPath 的 cell ,在渲染完成之前,主线程是卡死的,这与 UITableView 的机制一样,但速度会比 UITableView 快很多,因为 UITableView 的布局计算、资源解压、视图合成等都是在主线程进行,而 ASTableNode 则是多个线程并发进行,何况布局等还有缓存。所以,一般也没有问题,贝聊的聊天界面只是简单这样设置后,就不闪了,而且一帧不掉。但当页面布局较为复杂时,滑动时的卡顿掉帧就变的肉眼可见。</p>    <p>这时,可以设置 ASTableNode 的 leadingScreensForBatching 减缓卡顿</p>    <pre>  <code class="language-objectivec">override func viewDidLoad() {    super.viewDidLoad()    ... // 其他代码          tableNode.leadingScreensForBatching = 4  }  </code></pre>    <p>一般设置 tableNode.leadingScreensForBatching = 4 即提前计算四个屏幕的内容时,掉帧就很不明显了,典型的空间换时间。但仍不完美,仍然会掉帧,而我们期望的是一帧不掉,如丝般顺滑。这不难,基于上面不闪的方案,刷点小聪明就能解决。</p>    <pre>  <code class="language-objectivec">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)    }  }  </code></pre>    <p>关键代码是,</p>    <pre>  <code class="language-objectivec">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)      }    })  }  </code></pre>    <p>即,检查当前的 indexPath 是否被标记,如果是,则先设置 cell.neverShowPlaceholders = true ,等待 reload 完成(一帧是1/60秒,这里等待0.5秒,足够渲染了),将 cell.neverShowPlaceholders = false 。这样 reload 时既不会闪烁,也不会影响滑动时的异步绘制,因此一帧不掉。</p>    <p>这完全是耍小聪明的做法,但确实非常有效。</p>    <h3>3)reloadData时的闪烁</h3>    <p>在下拉刷新后,列表经常需要重新刷新,即调用 ASTableNode 或者 ASCollectionNode 的 reloadData 方法,但会闪,而且很明显。有了单个 cell reload 时闪烁的解决方案后,此类闪烁解决起来,就很简单了。</p>    <pre>  <code class="language-objectivec">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()          ... // 其他代码  }  </code></pre>    <p>将肉眼可见的 cell 添加进 indexPathesToBeReloaded 中即可。</p>    <h3>4)insertItems时更改ASCollectionNode的contentOffset引起的闪烁</h3>    <p>我们公司的聊天界面是用 AsyncDisplayKit 写的,当下拉加载更多新消息时,为保持加载后当前消息的位置不变,需要在 collectionNode.insertItems(at: indexPaths) 完成后,复原 collectionNode.view.contentOffset ,代码如下:</p>    <pre>  <code class="language-objectivec">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)    }  }  </code></pre>    <p>遗憾的是,会闪烁。起初以为是 AsyncDisplayKit 异步绘制导致的闪烁,一度还想放弃 AsyncDisplayKit ,用 UITableView 重写一遍,幸运的是,当时项目工期太紧,没有时间重写,也没时间仔细排查,直接带问题上线了。</p>    <p>最近闲暇,经仔细排查,方知不是 AsyncDisplayKit 的锅,但也比较难修,有一定的参考价值,因此一并列在这里。</p>    <p>闪烁的原因是, collectionNode insertItems 成功后会先绘制 contentOffset 为 CGPoint(x: 0, y: 0) 时的一帧画面,无动画时这一帧画面立即显示,然后调用成功回调,回调中复原了 collectionNode.view.contentOffset ,下一帧就显示复原了位置的画面,前后有变化因此闪烁。这是做消息类APP一并会遇到的bug,google一下,主要有两种解决方案,</p>    <p>第一种,通过仿射变换倒置 ASCollectionNode ,这样下拉加载更多,就变成正常列表的上拉加载更多,也就无需移动 contentOffset 。 ASCollectionNode 还特意设置了个属性 inverted ,方便大家开发。然而这种方案换汤不换药,当收到新消息,同时正在查看历史消息,依然需要插入新消息并复原 contentOffset ,闪烁依然在其他情形下发生。</p>    <p>第二种,集成一个 UICollectionViewFlowLayout ,重写 prepare() 方法,做相应处理即可。这个方案完美,简介优雅。子类化的 CollectionFlowLayout 如下:</p>    <pre>  <code class="language-objectivec">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)    }  }  </code></pre>    <p>当需要 insertItems 并且保持位置时,将 CollectionFlowLayout 的 isInsertingToTop 设置为 true 即可,完成后再设置为 false 。如下,</p>    <pre>  <code class="language-objectivec">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      }    }        ... // 其他代码  }  </code></pre>    <h2>布局</h2>    <p>AsyncDisplayKit 采用的是 flexbox 的布局思想,非常高效直观简洁,但毕竟迥异于 AutoLayout 和 frame layout 的布局风格,咋一上手,很不习惯,有些小技巧还是需要慢慢积累,有些概念也需要逐渐熟悉深入,下面列举几个笔者觉得比较重要的概念</p>    <p>1)设置任意间距</p>    <p>AutoLayout 实现任意间距,比较容易直观,因为 AutoLayout 的约束,本来就是我的边离你的边有多远的概念,而 AsyncDisplayKit 并没有, AsyncDisplayKit 里面的概念是,我自己的前面有多少空白距离,我自己的后面有多少空白距离,更强调自己。假如有三个元素,怎么约束它们之间的间距?</p>    <p>AutoLayout 是这样的:</p>    <pre>  <code class="language-objectivec">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)      }    }  }  </code></pre>    <p>而 AsyncDisplayKit 是这样的:</p>    <pre>  <code class="language-objectivec">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])    }  }  </code></pre>    <p>如果是拿 ASStackLayoutSpec 布局,元素之间的任意间距一般是通过元素自己的 spaceBefore 或者 spaceBefore style 实现,这是自我包裹性,更容易理解,如果不是拿 ASStackLayoutSpec 布局,可以将某个元素包裹成 ASInsetsLayoutSpec ,再设置 UIEdgesInsets ,保持自己的四周任意边距。</p>    <p>能任意设置间距是自由布局的基础。</p>    <p>2)flexGrow和flexShrink</p>    <p>flexGrow 和 flexShrink 是相当重要的概念, flexGrow 是指当有多余空间时,拉伸谁以及相应的拉伸比例(当有多个元素设置了 flexGrow 时), flexShrink 相反,是指当空间不够时,压缩谁及相应的压缩比例(当有多个元素设置了 flexShrink 时)。</p>    <p>灵活使用 flexGrow 和 spacer (占位 ASLayoutSpec )可以实现很多效果,比如等间距,</p>    <p><img src="https://simg.open-open.com/show/c731eecf9b628e6fbfe31ffd7f728504.png"></p>    <p>实现代码如下,</p>    <pre>  <code class="language-objectivec">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])    }  }  </code></pre>    <p>如果 spacer 的 flexGrow 不同就可以实现指定比例的布局,再结合 width 样式,轻松实现以下布局</p>    <p><img src="https://simg.open-open.com/show/f7a690397e54adf874af09c74c6e7a5b.png"></p>    <p>布局代码如下,</p>    <pre>  <code class="language-objectivec">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])  }  </code></pre>    <p>相同的布局如果用 Autolayout ,麻烦去了。</p>    <p>3)constrainedSize的理解</p>    <p>constrainedSize 是指某个 node 的大小取值范围,有 minSize 和 maxSize 两个属性。比如下图的布局:</p>    <p><img src="https://simg.open-open.com/show/5833bc84010cd3844b9fc6c571e7feab.png"></p>    <pre>  <code class="language-objectivec">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)    }  }  </code></pre>    <p>其中方法 override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec 中的 constrainedSize 所指是 ContainerNode 自身大小的取值范围。给定 constrainedSize , AsyncDisplayKit 会根据 ContainerNode 在 layoutSpecThatFits(_:) 中施加在 nodeA、nodeB 的布局规则和 nodeA、nodeB 自身属性计算 nodeA、nodeB 的 constrainedSize 。</p>    <p>假如 constrainedSize 的 minSize 是 CGSize(width: 0, height: 0) , maxSize 为 CGSize(width: 375, height: Inf+) ( Inf+ 为正无限大),则:</p>    <p>1)根据布局规则和 nodeA 自身样式属性 maxWidth 、 minWidth 、 width 、 height 、 preferredSize ,可计算出 nodeA 的 constrainedSize 的 minSize 和 maxSize 均为其 preferredSize 即 CGSize(width: 100, height: 100) ,因为布局规则为水平向的 ASStackLayout ,当空间富余或者空间不足时, nodeA 即不压缩又不拉伸,所以会取其指定的 preferredSize 。</p>    <p>2)根据布局规则和 nodeB 自身样式属性 maxWidth 、 minWidth 、 width 、 height 、 preferredSize ,可以计算出其 constrainedSize 的 minSize 是 CGSize(width: 0, height: 0) , maxSize 为 CGSize(width: 375 - 100 - b - e - d, height: Inf+) ,因为 nodeB 的 flexShrink 和 flexGrow 均为1,也即当空间富余或者空间不足时, nodeB 添满富余空间或压缩至空间够为止。</p>    <p>如果不指定 nodeB 的 flexShrink 和 flexGrow ,那么当空间富余或者空间不足时, AsyncDisplayKit 就不知道压缩和拉伸哪一个布局元素,则 nodeB 的 constrainedSize 的 maxSize 就变为 CGSize(width: Inf+, height: Inf+) ,即完全无大小限制,可想而知, nodeB 的子 node 的布局将完全不对。这也说明另外一个问题, node 的 constrainedSize 并不是一定大于其子 node 的 constrainedSize 。</p>    <p>理解 constrainedSize 的计算,才能熟练利用 node 的样式 maxWidth 、 minWidth 、 width 、 height 、 preferredSize 、 flexShrink 和 flexGrow 进行布局。如果发现布局结果不对,而对应 node 的布局代码确是正确无误,一般极有可能是因为此 node 的父布局元素不正确。</p>    <h2>动画</h2>    <p>因为 AsyncDisplayKit 的布局方式有两种, frame 布局和 flexbox 式的布局,相应的动画方式也有两种</p>    <p>1)frame布局</p>    <p>如果采用的是 frame 布局,动画跟普通的 UIView 相同</p>    <pre>  <code class="language-objectivec">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      }    }  }  </code></pre>    <p>不要觉得用了 AsyncDisplayKit 就告别了 frame 布局, ViewController 中主要元素个数很少,布局简单,因此,一般也还是采用 frame layout ,如果只是做一些简单的动画,直接采用 UIView 的动画 API 即可</p>    <p>2)flexbox式的布局</p>    <p>这种布局方式,是在某个子 node 中常用的,如果 node 内部布局发生了变化,又需要做动画时,就需要复写 AsyncDisplayKit 的动画 API ,并基于提供的动画上下文类 context ,做动画:</p>    <pre>  <code class="language-objectivec">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      }    }  }  </code></pre>    <p>系统默认的动画是渐隐渐显,可以获取 animate 前后布局信息,比如某个子 node 两种布局中的 frame ,然后再自定义动画类型。如果想触发动画,主动调用 SomeNode 的触发方法 transitionLayout(withAnimation:shouldMeasureAsync:measurementCompletion:) 即可。</p>    <h2>内存泄漏</h2>    <p>为了方便将一个 UIView 或者 CALayer 转化为一个 ASDisplayNode ,系统提供了用 block 初始化 ASDisplayNode 的简便方法:</p>    <pre>  <code class="language-objectivec">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)  </code></pre>    <p>需要注意的是所传入的 block 会被要创建的 node 持有。如果 block 中反过来持有了这个 node 的持有者,则会产生循环引用,导致内存泄漏:</p>    <pre>  <code class="language-objectivec">class SomeNode {    var nodeA: ASDisplayNode!    let color = UIColor.red    override init() {      super.init()      nodeA = ASDisplayNode {        let view = UIView()        view.backgroundColor = self.color // 内存泄漏        return view      }    }  }  </code></pre>    <h2>子线程崩溃</h2>    <p>AsyncDisplayKit 的性能优势来源于异步绘制,异步的意思是有时候 node 会在子线程创建,如果继承了一个 ASDisplayNode ,一不小心在初始化时调用了 UIKit 的相关方法,则会出现子线程崩溃。比如以下 node ,</p>    <pre>  <code class="language-objectivec">class SomeNode {    let iconImageNode: ASDisplayNode    let color = UIColor.red    override init() {      iconImageNode = ASImageNode()      iconImageNode.image = UIImage(named: "iconName") // 需注意SomeNode有时会在子线程初始化,而UIImage(named:)并不是线程安全        super.init()      }  }  </code></pre>    <p>但在 node 初始化时调用 UIImage(named:) 创建图片是不可避免的,用 methodSwizzle 将 UIImage(named:) 置换成安全的即可。</p>    <p>其实在子线程初始化 node 并不多见,一般都在主线程。</p>    <h2>总结</h2>    <p>一年的实践下来,闪烁是 AsyncDisplayKit 遇到的最大的问题,修复起来也颇为费神。其他bug,有时虽然很让人头疼,但由于 AsyncDisplayKit 是对UIKit的再封装,实在不行,仍然可以越过 AsyncDisplayKit 用 UIKit 的方法修复。</p>    <p>学习曲线也不算很陡峭。</p>    <p>考虑到 AsyncDisplayKit 的种种好处,非常推荐 AsyncDisplayKit ,当然还是仅限于用在比较复杂和动态的页面中。</p>    <p> </p>    <p>来自:http://qingmo.me/2017/07/21/asyndisplaykit/</p>    <p> </p>