WWDC2018 之 高性能 Auto Layout
1. 关于 Auto Layout 的历史渊源
上世纪 90 年代,名叫 Cassowary的布局算法,通过将布局问题抽象成线性不等式,并分解成多个位置间的约束,解决了用户界面的布局问题。
Apple 自从 iOS 6 引入了 Auto Layout 的布局概念,其实就是对 Cassowary布局算法的一种实现。在使用 Auto Layout 进行布局时,可以指定一系列的约束,比如视图的高度、宽度等等。而每一个约束其实都是一个简单的线性等式或不等式,整个界面上的所有约束在一起就明确地(没有冲突)定义了整个系统的布局。
对于 Auto Layout 算法部分,本文不做展开。在这里我们仅仅需要知道,Auto Layout 的原理,就是在对 Layout 问题抽象的方程组求解,就可以继续向下阅读。
以下就是 WWDC 220 Session - 高性能 Auto Layout 高度脱水版。
2. iOS 上的性能表现
下图是 Ken Ferry 在 Session 现场的演示,可以比较清晰的看出,左图自使用布局的 CollectionView 上下滑动较右图而言更加流畅,Ken 在描述中也说到 iOS 12 在该例中的所有滑动事件是满帧状态。(左 iOS 12,右 iOS 11)
下图是官方测试后得到的 iOS 12 和 iOS 11 在特定场景下时间开销的对比图。可以明显的看到 iOS 12 具有很大的优势。
那么究竟是如何做到这个优化的呢?
3. 内部实现和感观体验
我们首先来通过一个例子整体的了解一下。分析一下这个简单的 Layout 场景:
下面我们在 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 次。这个工程分成三步:
- 更新约束:从子视图向外层逐级更新约束;
- Layout 调整:从外部向内,逐级视图获得自身的 Layout;
- 渲染与展示:与 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 调整。而这些其实是不需要的。
对于一次约束的增加过程至此也就大体讲完了。我们来总结一下这里我提到的一些主要内容:
- 不要由于自身的问题从而带来 规则搅动的错误;
- Auto Layout 的数学原理,就是基本的代数运算。
- Auto Layout 计算引擎是一个布局缓存和关系依赖的跟踪器;
- 需要什么就对什么做出约束,不要增加额外的约束,避免造成不必要的开销;
4. 建立一个有效的 Layout
4.1 使用 Instrument 来捕捉规则搅动
在使用 Auto Layout 布局来实现 UITableView
,我们经常会发现滑动卡顿的问题。这些问题在开发的时候很难查出原因所在。为了方便的解决并排查问题,新版的 Xcode 增加了一个新的工具 - Instrument for Layout。
这个工具的第一行 Layout Time 反应了 CPU 的使用情况,通过运算时间可以和后面的异常值进行比对。
第二行用于检测我们上文提到的 规则搅动的问题,当代码中出现大量的重复添加相同约束的错误时,会以直方图时间复杂度的形式呈现出来,便于我们做进一步的代码排查。
第三行来显示约束的增、删、改的操作。
最后一行,我们会对 UILabel
这个控件的 Layout 占中单独展示出来。因为我们的示例 App 中只有 UILabel
,当然如果你的应用中有其他的视图,也会按照类型来分行呈现。
其实纵观这个工具,他能够帮助我们的仅仅是查看约束的计算耗时以及是否出现了 规则搅动。但是这些都是我们在代码中可以直接避免的。这里有几个关于避免 规则搅动的 Tips 告诉大家:
- 尽量不要删除所有的约束(Avoid removing all constraints);
- 若是一个静态约束,仅做一次添加操作即可;
- 仅改变需要改变的约束;
- 尽量不要做删除视图的操作,反之用
hide()
方法替代;
一般做到这四点,可以避免绝大多数的 规则搅动代码层面的错误。
某些控件是十分特殊的,例如 UIImageView
、UILabel
这种,他们都有一个自适应的尺寸,这里我们称之为固有尺寸(Intrinsic Content Size),当我们不对其作出特殊化的 height 和 width 限制时,UIView
会直接用他们的固有尺寸(UIImageView
即图片尺寸,UILabel
即文本尺寸)来当做约束条件。
4.2 Override intrinsicContentSize 来调整 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)
}
复制代码
4.3 不要过度使用 systemLayoutSizeFitting()
systemLayoutSizeFitting()
虽然能帮助我们根据 Layout 来自动计算其约束,但是纵观整个 Layout 过程,其计算的时间开销是十分大的。这个方法调用,其目的是从计算引擎中重新获得调用方法对应视图的 Size。然而这个过程较为复杂。
也许整个流程并不复杂,但是对于我们 Render Loop 过程,相当于作出了一次重复步骤。在 iOS 12 中,Apple 再次对自适应 Cell 作出了优化,所以在大多数情况下,减少 systemLayoutSizeFitting()
的调用可以使得时间开销再次削减。
5 总述
以上便是笔者对于这个 Session 的所有记录和脱水叙述。如同 Ken 所说,也许简单的对于 Auto Layout 中约束的 Tips 并不能满足于你,这里还有一些资料可以供你去继续学习。
- 可以前往 WWDC 2015 查看 Session 219 - Mysteries of Auto Layout, Part 2,为你带来 Auto Layout 实现及原理。
- https://techblog.toutiao.com/2018/06/19/untitled-46/
WWDC2018 之 高性能 Auto Layout的更多相关文章
- [Android开发学iOS系列] Auto Layout
[Android开发学iOS系列] Auto Layout 内容: 介绍什么是Auto Layout. 基本使用方法 在代码中写约束的方法 Auto Layout的原理 尺寸和优先级 Auto Lay ...
- 【Auto Layout】Xcode6及以上版本,创建Auto Layout 约束时产生的一些变化【iOS开发教程】
[#Auto Layout#]Xcode6创建Auto Layout 约束时产生的一些变化 通过两个小Demo来展示下变化: Demo1需求: 为控制器的根视图(图中的“控制器View”)的子 ...
- iOS 8 Auto Layout界面自动布局系列2-使用Xcode的Interface Builder添加布局约束
http://blog.csdn.net/pucker/article/details/41843511 上一篇文章<iOS 8界面自动布局系列-1>简要介绍了iOS界面布局方式的前世今生 ...
- 手写代码自动实现自动布局,即Auto Layout的使用
手写代码自动实现自动布局,即Auto Layout的使用,有需要的朋友可以参考下. 这里要注意几点: 对子视图的约束,若是基于父视图,要通过父视图去添加约束. 对子视图进行自动布局调整,首先对UIVi ...
- Auto Layout
Auto Layout XCode5+ Auto Layout Concepts 核心的概念是约束. Constraint Basics Constant value Relation Priorit ...
- 使用Auto Layout中的VFL(Visual format language)--代码实现自动布局【转】
本文将通过简单的UI来说明如何用VFL来实现自动布局.在自动布局的时候避免不了使用代码来加以优化以及根据内容来实现不同的UI. 一:API介绍 NSLayoutConstraint API 1 2 3 ...
- 转载自@机智的新手:使用Auto Layout中的VFL(Visual format language)--代码实现自动布局
本文将通过简单的UI来说明如何用VFL来实现自动布局.在自动布局的时候避免不了使用代码来加以优化以及根据内容来实现不同的UI. 一:API介绍 NSLayoutConstraint API 1 2 3 ...
- ios auto layout demystified (二)
Constraints Constraint Types Layout constraints (NSLayoutConstraint class, public)—这些规则指定了view的几何学.他 ...
- ios auto layout demystified (一)
Ambiguous Layout 在开发过程中,你可以通过调用hasAmbiguousLayout 来测试你的view约束是否足够的.这个会返回boolean值.如果有一个不同的frame就会返回ye ...
随机推荐
- Nacos 发布 1.0.0 GA 版本,可大规模投入到生产环境
经过 3 个 RC 版本的社区体验之后,Nacos 正式发布 1.0.0 GA 版本,在架构.功能和 API 设计上进行了全方位的重构和升级. 1.0.0 版本的发布标志着 Nacos 已经可以大规模 ...
- 微信小程序中支持es7的async语法
最近在原生的微信小程序项目中需要把原来es6的promise方法改成es7的async await,这样代码看起来更直观,也方便以后的兄弟维护,但是改了代码之后项目就报错了. 提示的错误是:regen ...
- Jmeter非命令行执行脚本
这次我们可以清晰地看到每个线程的执行情况. 这里是我们使用非 GUI 模式运行测试脚本时可以使用的一些命令: -h 帮助 -> 打印出有用的信息并退出 -n 非 GUI 模式 -& ...
- <肖申克的救赎>观后感
肖申克的救赎主要讲述了银行家安迪在不健全的法律制度下被陷害进入了--鲨堡监狱,最后为了重见光明.追求自由,实现“自我救赎”的故事. 1.希望是件好东西,也许是世上最好的东西.好东西从来不会流逝. Ho ...
- php服务端允许跨域访问
>>php服务端允许跨域访问<< >>同源策略和跨域解决方案<<
- day6_python序列化之 json & pickle & shelve 模块
一.json & pickle & shelve 模块 json,用于字符串 和 python数据类型间进行转换pickle,用于python特有的类型 和 python的数据类型间进 ...
- Eclipse(Maven) web项目更改项目名称
1. 右键工程:Refactor->Rename,更改项目名称: 2. 修改项目目录下:.project文件 <?xml version="1.0" encoding= ...
- HTML5有哪些新特性?移除了哪些元素?
HTML5新特性: 拖放(Drag and drop)API 语义化标签(header.nav.footer.section.article.aside) 音频.视频(audio.video)API ...
- hdu 3339 In Action(迪杰斯特拉+01背包)
In Action Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total S ...
- win7 debug 工具
x86 处理器中的 CS 与 IP 寄存器介绍与调试: http://blog.sina.com.cn/s/blog_54f82cc2010121yj.html https://www.jianshu ...