WWDC 2018:高性能 Auto Layout

WWDC 2018 Session 220: High Performance Auto Layout
做者简介:@冬瓜争作全栈瓜,今日头条 iOS 工程师,Sepicat 做者。git

1. 关于 Auto Layout 的历史渊源

上世纪 90 年代,名叫 Cassowary 的布局算法,经过将布局问题抽象成线性不等式,并分解成多个位置间的约束,解决了用户界面的布局问题。github

Apple 自从 iOS 6 引入了 Auto Layout 的布局概念,其实就是对 Cassowary 布局算法的一种实现。在使用 Auto Layout 进行布局时,能够指定一系列的约束,好比视图的高度、宽度等等。而每个约束其实都是一个简单的线性等式或不等式,整个界面上的全部约束在一块儿就明确地(没有冲突)定义了整个系统的布局。算法

对于 Auto Layout 算法部分,本文不作展开。在这里咱们仅仅须要知道,Auto Layout 的原理,就是在对 Layout 问题抽象的方程组求解,就能够继续向下阅读。缓存

如下就是 WWDC 220 Session - 高性能 Auto Layout 高度脱水版。app

2. iOS 上的性能表现

下图是 Ken Ferry 在 Session 现场的演示,能够比较清晰的看出,左图自使用布局的 CollectionView 上下滑动较右图而言更加流畅,Ken 在描述中也说到 iOS 12 在该例中的全部滑动事件是满帧状态。(左 iOS 12,右 iOS 11)less

下图是官方测试后获得的 iOS 12 和 iOS 11 在特定场景下时间开销的对比图。能够明显的看到 iOS 12 具备很大的优点。ide

那么到底是如何作到这个优化的呢?工具

3. 内部实现和感观体验

咱们首先来经过一个例子总体的了解一下。分析一下这个简单的 Layout 场景:oop

下面咱们在 updateConstraints() 方法中来描述这个 Layout:布局

// Don’t do this! Removes and re-adds constraints potentially at 120 frames per second
override func updateConstraints() {
    // 首先移除约束
    NSLayoutConstraint.deactivate(myConstraints)
    // 而后对约束从新规则
    myConstraints.removeAll()
    // 构造一个 view 字典便于visual format使用
    let views = ["text1":text1, "text2":text2]
    // 为约束增长规则
    myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
                                                    options: [.alignAllFirstBaseline],
                                                    metrics: nil,
                                                    views: views)
    myConstraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
                                                    options: [],
                                                    metrics: nil,
                                                    views: views)
    // 添加约束,与 deactivate 方法对应
    NSLayoutConstraint.activate(myConstraints)
    // 调用父类的 updateConstraints()
    super.updateConstraints()
}
复制代码

至此咱们就实现了这个简单的 Layout 方案。为了继续探究这个 Topic,在这以前先要了解一些预备知识。

3.1 updateConstraints 原理 - Render Loop

Render Loop 这个过程是用来确保全部的 UI 视图在每秒的全部帧中都表现出对应表现,正常状况下每秒会运行 120 次。这个工程分红三步:

  1. 更新约束:从子视图向外层逐级更新约束;
  2. Layout 调整:从外部向内,逐级视图得到自身的 Layout;
  3. 渲染与展现:与 Layout 相同,呈现顺序从外向内,使得视图呈现出来;

固然,这么叙述仍是有些抽象。其实这三个过程在咱们平常开发中也是常常接触的三类方法:

/// Render Loop 过程
/// 过程一:更新约束
func updateConstraints();
func setNeedsUpdateConstraints();
func updateConstraintsIfNeeded();

/// 过程二:Layout 调整
func layoutSubviews();
func setNeedsLayout();
func layoutIfNeeded();

/// 过程三:渲染与展现
func draw(_:);
func setNeedsDisplay();
复制代码

每一次调整都会运行这么一个 Render Loop 步骤。这是一套很精确的 API,目的为了让各个环节中的工做不重不漏,从而除去了不少重复操做。如上例中,若是一个 UILabel 须要有一个约束来描述其大小,可是其中的不少属性例如字条、字号等又会影响这个视图的大小,这套 API 就是这样,每次修改都会根据不一样的属性来肯定其尺寸。开发者能够在其方法内部来指明在渲染前最后的属性值,从而排除了屡次设置的重复操做。

了解了 Render Loop 咱们再来完善以前的代码。会发如今每次在 updateConstraints 的时候,都会从新解除和增长一次约束,这显然会使得性能变差。修改一下代码:

// This is ok! Doesn’t do anything unless self.myConstraints has been nil’d out
override func updateConstraints() {
    if self.myConstraints == nil {
        var constraints = [NSLayoutConstraint]()
        let views = ["text1":text1, "text2":text2]
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "H:|-[text1]-[text2]",
                                                      options: [.alignAllFirstBaseline],
                                                      metrics: nil,
                                                      views: views)
        constraints += NSLayoutConstraint.constraints(withVisualFormat: "V:|-[text1]-|",
                                                      options: [],
                                                      metrics: nil,
                                                      views: views)
        NSLayoutConstraint.activate(constraints)
        self.myConstraints = constraints
    }
    super.updateConstraints()
}
复制代码

这个 nil 的判断意思是若是咱们增长了约束,那么就不用对其再次设置。这个错误也是开发者在客户端开发中较常见的错误,这种无变化的约束设置咱们称之为 规则搅动 (Churning the Constraints) ,这种操做毫无心义且影响性能。

虽然 Render Loop 过程具备明确的目的性,可是这套 API 也是高危的,由于它常常会被调用。

下面咱们来深究一下这个过程的原理。

3.2 增长约束的内部实现

当咱们为空间增长一个约束 Constraint 的时候,经过这些约束会组成一个多元一次方程组,这个方程组的解能够定位那些经过约束可间接计算出的定量。而这个计算过程是 Auto Layout 引擎来完成处理的。求出的解集在 UIView 渲染过程当中,当作其 frame 属性中的值来使用。下图就是反应了这么一个过程。

在计算引擎计算出解集后,计算引擎还有他最后的一个工做,就是发送通知,使得对应的 UIView 调用其父视图的 setNeedsLayout() 方法。这也就是咱们以前提到的更新约束这个步骤,经过向外层调用 setNeedsLayouts() 方法,咱们能够验证这个由内向外的步骤。

在约束更新完成以后,进入了第二个步骤,也就是 Layout 调整阶段。每一个视图会从计算引擎中获取到其子视图所需的全部数据,获取到以后从新为子视图赋值。从这点看出,Layout 调整阶段是自外向内的。

咱们再来思考一下上文说起到的 规则搅动 问题,若是咱们每次将约束规则删除、从新添加,则每一次刷新视图都会重新经历一遍引擎的解集重计算、由内向外的 setNeedsLayout()、自外向内的 Layout 调整。而这些实际上是不须要的。

对于一次约束的增长过程至此也就大致讲完了。咱们来总结一下这里我提到的一些主要内容:

  1. 不要因为自身的问题从而带来 规则搅动 的错误;
  2. Auto Layout 的数学原理,就是基本的代数运算。
  3. Auto Layout 计算引擎是一个布局缓存和关系依赖的跟踪器;
  4. 须要什么就对什么作出约束,不要增长额外的约束,避免形成没必要要的开销;

4. 创建一个有效的 Layout

4.1 使用 Instrument 来捕捉规则搅动

在使用 Auto Layout 布局来实现 UITableView,咱们常常会发现滑动卡顿的问题。这些问题在开发的时候很难查出缘由所在。为了方便的解决并排查问题,新版的 Xcode 增长了一个新的工具 - Instrument for Layout

这个工具的第一行 Layout Time 反应了 CPU 的使用状况,经过运算时间能够和后面的异常值进行比对。

第二行用于检测咱们上文提到的 规则搅动 的问题,当代码中出现大量的重复添加相同约束的错误时,会以直方图时间复杂度的形式呈现出来,便于咱们作进一步的代码排查。

第三行来显示约束的增、删、改的操做。

最后一行,咱们会对 UILabel 这个控件的 Layout 占中单独展现出来。由于咱们的示例 App 中只有 UILabel,固然若是你的应用中有其余的视图,也会按照类型来分行呈现。

其实纵观这个工具,他可以帮助咱们的仅仅是查看约束的计算耗时以及是否出现了 规则搅动。可是这些都是咱们在代码中能够直接避免的。这里有几个关于避免 规则搅动 的 Tips 告诉你们:

  1. 尽可能不要删除全部的约束(Avoid removing all constraints);
  2. 如果一个静态约束,仅作一次添加操做便可;
  3. 仅改变须要改变的约束;
  4. 尽可能不要作删除视图的操做,反之用 hide() 方法替代;

通常作到这四点,能够避免绝大多数的 规则搅动 代码层面的错误。

某些控件是十分特殊的,例如 UIImageViewUILabel 这种,他们都有一个自适应的尺寸,这里咱们称之为固有尺寸(Intrinsic Content Size),当咱们不对其做出特殊化的 height 和 width 限制时,UIView 会直接用他们的固有尺寸(UIImageView 即图片尺寸,UILabel 即文本尺寸)来当作约束条件。

4.2 Override intrinsicContentSize 来调整 UILabel 约束性能

在不少控件组成的页面中,UILabel 的 Size 计算会在全部的计算开销中占很大的比重。这时候追求极致,咱们能够 Override UILabelintrinsicContentSize 来告诉计算引擎,如何抉择 UILabel 的 Size 问题。若是已知一个 UILabel 的展现 Size,直接 Override 其属性便可,不然对其设置成 UIView.noIntrinsicMetric

override var intrinsicContentSize: CGSize {
    return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
复制代码

4.3 不要过分使用 systemLayoutSizeFitting()

systemLayoutSizeFitting() 虽然能帮助咱们根据 Layout 来自动计算其约束,可是纵观整个 Layout 过程,其计算的时间开销是十分大的。这个方法调用,其目的是从计算引擎中从新得到调用方法对应视图的 Size。然而这个过程较为复杂。

也许整个流程并不复杂,可是对于咱们 Render Loop 过程,至关于做出了一次重复步骤。在 iOS 12 中,Apple 再次对自适应 Cell 做出了优化,因此在大多数状况下,减小 systemLayoutSizeFitting() 的调用可使得时间开销再次削减。

5 总述

以上即是笔者对于这个 Session 的全部记录和脱水叙述。如同 Ken 所说,也许简单的对于 Auto Layout 中约束的 Tips 并不能知足于你,这里还有一些资料能够供你去继续学习。

  • 学习 Auto Layout 中的日志能够有效地帮助你 Debug;
  • 学习 Debug 的相关 Session;
  • 能够前往 WWDC 2015 查看 Session 219 - Mysteries of Auto Layout, Part 2,为你带来 Auto Layout 实现及原理。
相关文章
相关标签/搜索