WWDC 2018 Session 220: High Performance Auto Layout
做者简介:@冬瓜争作全栈瓜,今日头条 iOS 工程师,Sepicat 做者。git
上世纪 90 年代,名叫 Cassowary 的布局算法,经过将布局问题抽象成线性不等式,并分解成多个位置间的约束,解决了用户界面的布局问题。github
Apple 自从 iOS 6 引入了 Auto Layout 的布局概念,其实就是对 Cassowary 布局算法的一种实现。在使用 Auto Layout 进行布局时,能够指定一系列的约束,好比视图的高度、宽度等等。而每个约束其实都是一个简单的线性等式或不等式,整个界面上的全部约束在一块儿就明确地(没有冲突)定义了整个系统的布局。算法
对于 Auto Layout 算法部分,本文不作展开。在这里咱们仅仅须要知道,Auto Layout 的原理,就是在对 Layout 问题抽象的方程组求解,就能够继续向下阅读。缓存
如下就是 WWDC 220 Session - 高性能 Auto Layout 高度脱水版。app
下图是 Ken Ferry 在 Session 现场的演示,能够比较清晰的看出,左图自使用布局的 CollectionView 上下滑动较右图而言更加流畅,Ken 在描述中也说到 iOS 12 在该例中的全部滑动事件是满帧状态。(左 iOS 12,右 iOS 11)less
下图是官方测试后获得的 iOS 12 和 iOS 11 在特定场景下时间开销的对比图。能够明显的看到 iOS 12 具备很大的优点。ide
那么到底是如何作到这个优化的呢?工具
咱们首先来经过一个例子总体的了解一下。分析一下这个简单的 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,在这以前先要了解一些预备知识。
Render Loop 这个过程是用来确保全部的 UI 视图在每秒的全部帧中都表现出对应表现,正常状况下每秒会运行 120 次。这个工程分红三步:
固然,这么叙述仍是有些抽象。其实这三个过程在咱们平常开发中也是常常接触的三类方法:
/// 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 也是高危的,由于它常常会被调用。
下面咱们来深究一下这个过程的原理。
当咱们为空间增长一个约束 Constraint 的时候,经过这些约束会组成一个多元一次方程组,这个方程组的解能够定位那些经过约束可间接计算出的定量。而这个计算过程是 Auto Layout 引擎来完成处理的。求出的解集在 UIView
渲染过程当中,当作其 frame
属性中的值来使用。下图就是反应了这么一个过程。
在计算引擎计算出解集后,计算引擎还有他最后的一个工做,就是发送通知,使得对应的 UIView
调用其父视图的 setNeedsLayout()
方法。这也就是咱们以前提到的更新约束这个步骤,经过向外层调用 setNeedsLayouts()
方法,咱们能够验证这个由内向外的步骤。
在约束更新完成以后,进入了第二个步骤,也就是 Layout 调整阶段。每一个视图会从计算引擎中获取到其子视图所需的全部数据,获取到以后从新为子视图赋值。从这点看出,Layout 调整阶段是自外向内的。
咱们再来思考一下上文说起到的 规则搅动 问题,若是咱们每次将约束规则删除、从新添加,则每一次刷新视图都会重新经历一遍引擎的解集重计算、由内向外的 setNeedsLayout()
、自外向内的 Layout 调整。而这些实际上是不须要的。
对于一次约束的增长过程至此也就大致讲完了。咱们来总结一下这里我提到的一些主要内容:
在使用 Auto Layout 布局来实现 UITableView
,咱们常常会发现滑动卡顿的问题。这些问题在开发的时候很难查出缘由所在。为了方便的解决并排查问题,新版的 Xcode 增长了一个新的工具 - Instrument for Layout。
这个工具的第一行 Layout Time 反应了 CPU 的使用状况,经过运算时间能够和后面的异常值进行比对。
第二行用于检测咱们上文提到的 规则搅动 的问题,当代码中出现大量的重复添加相同约束的错误时,会以直方图时间复杂度的形式呈现出来,便于咱们作进一步的代码排查。
第三行来显示约束的增、删、改的操做。
最后一行,咱们会对 UILabel
这个控件的 Layout 占中单独展现出来。由于咱们的示例 App 中只有 UILabel
,固然若是你的应用中有其余的视图,也会按照类型来分行呈现。
其实纵观这个工具,他可以帮助咱们的仅仅是查看约束的计算耗时以及是否出现了 规则搅动。可是这些都是咱们在代码中能够直接避免的。这里有几个关于避免 规则搅动 的 Tips 告诉你们:
hide()
方法替代;通常作到这四点,能够避免绝大多数的 规则搅动 代码层面的错误。
某些控件是十分特殊的,例如 UIImageView
、UILabel
这种,他们都有一个自适应的尺寸,这里咱们称之为固有尺寸(Intrinsic Content Size),当咱们不对其做出特殊化的 height 和 width 限制时,UIView
会直接用他们的固有尺寸(UIImageView
即图片尺寸,UILabel
即文本尺寸)来当作约束条件。
UILabel
约束性能在不少控件组成的页面中,UILabel
的 Size 计算会在全部的计算开销中占很大的比重。这时候追求极致,咱们能够 Override UILabel
的 intrinsicContentSize
来告诉计算引擎,如何抉择 UILabel
的 Size 问题。若是已知一个 UILabel
的展现 Size,直接 Override 其属性便可,不然对其设置成 UIView.noIntrinsicMetric
。
override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
}
复制代码
systemLayoutSizeFitting()
systemLayoutSizeFitting()
虽然能帮助咱们根据 Layout 来自动计算其约束,可是纵观整个 Layout 过程,其计算的时间开销是十分大的。这个方法调用,其目的是从计算引擎中从新得到调用方法对应视图的 Size。然而这个过程较为复杂。
也许整个流程并不复杂,可是对于咱们 Render Loop 过程,至关于做出了一次重复步骤。在 iOS 12 中,Apple 再次对自适应 Cell 做出了优化,因此在大多数状况下,减小 systemLayoutSizeFitting()
的调用可使得时间开销再次削减。
以上即是笔者对于这个 Session 的全部记录和脱水叙述。如同 Ken 所说,也许简单的对于 Auto Layout 中约束的 Tips 并不能知足于你,这里还有一些资料能够供你去继续学习。