前言
我们都希望自己的app能流畅运行不掉帧,这个topic介绍了
- iOS12苹果都做了哪些针对流畅度的优化
- 相关的底层运行机制
- 怎样高效地使用自动布局
- autolayout相关
iOS12的优化
首先,苹果在iOS12对autolayout做了性能优化,以下是几个参数对比,灰色是iOS11,蓝色是iOS12。
横轴是时间,可以看到Moving Tree的提升非常大,相信大家对于滑动列表时的卡顿(hiccup)都深有感触,以前我针对列表滑动这块也做了不少优化,这次更新iOS12后发现以前一些没有做优化的页面,有轻微卡顿的都不卡了,不过在一些性能敏感页还是有卡顿现象,还是需要自己去优化的。
Render Loop的运行原理
The render loop is the process that runs potentially at 120 times every second that make sure that all the content is ready to go for each frame.
———— by Ken Ferry
render loop是一个可以最多每秒运行120次的过程,用来保证每帧刷新前需要渲染的内容都已经准备好。
这个过程有三个阶段,
先Update Constraints,再Layout,最后Display。
Update Constraints从叶节点view开始执行,直到window;
layoutSubviews则是反过来,从window开始,传递到最终的叶节点view;
最后是drawRect绘制,也是从window开始;
苹果在设计上为了减少布局的重复调用,分了这3个阶段,并提供了平行的类似功能的方法,比如updateConstraints和layoutSubviews,setNeedsUpdateConstraints和setNeedsLayout等
看到这里我感觉有必要重温一下这些布局方法,这里就简单回顾一下Update Constraints的3个方法
updateConstraints()
一般来说要改变某个约束,我们可以在某个action或方法中改变约束就可以了,但我们也可以重写某个view的updateConstraints,在重写的updateConstraints里集中改变一批约束。
updateConstraints通常是布局有变化时,系统给我们调用的,也可以由setNeedsUpdateConstraints触发,由于调用在render loop的第一阶段,他有两个适用场景
- 当我们有许多约束要产生变化,比如横竖屏切换时,我们可以在traitCollectionDidChange里调用setNeedsUpdateConstraints,updateConstraints里集中处理需要变化的约束
- 当我们希望尽早产生这些变化
要注意的问题:
- 这个方法有可能每秒调用120次,所以要避免churning constraints,也就是不要deactivate所有约束,再重新activate他们;只改变需要改变的约束,对于不变的约束只添加一次
- 方法内不要调setNeedsUpdateConstraints,会产生feedback loop,虽然不是死循环但也会降低性能
- 在最后一行调用[super updateConstraints],保证向父view传递
setNeedsUpdateConstraints()
这个方法用来标记当layout即将发生时,让系统调用updateConstraints。
通常我们用它来优化批量的约束变化,这样可以避免重复计算。调用后并不会马上调updateConstraints,而是在下一次render loop的layout即将发生时,调用updateConstraints
updateConstraintsIfNeeded()
当有layout变化时,系统会调用这个方法,更新当前view和他的subviews的所有约束,也可以由我们主动调用,来获得最新的布局,和layoutIfNeeded类似。
禁止重写这个方法。调用后也不会触发updateConstraints。
另外还提到了如果使用interface builder布局,能避免踩到一些降低性能的坑,但我觉得最好还是用纯代码更灵活些。
以下是会降低性能的坑:
好了,我们可以了解一下当激活约束时,到底发生了什么?
Update Phase
view通常是在一个window里,当update Constraint phase开始时, window会实例化一个布局引擎(layout engine),当给view添加约束时,会将约束对应的等式(equation)
firstItem.firstAttribute {=,<=,>=} secondItem.secondAttribute * multiplier + constant
交给布局引擎,布局引擎通过代数运算解出view布局需要知道的相应变量的值,如minX、minY、width、height。
其实就是最简单的代数里的解方程。。。如下
解完后会通知view,有value改变了,
view接着调用 [superView setNeedsLayout],让自己进入layout phase;
Layout Phase
view会将engine里计算好的variable值 copy到自己的frame,subview再调用setBounds,setCenter,完成布局;
所以每一次在updateConstraints方法里deactivate constraints,再reactivate都会重复 实例化布局引擎-解方程-销毁的过程,有可能每秒执行多次,从而导致性能问题;
正确的做法应该是这样
如何高效地使用自动布局
don't pay for what you don't use
为了性能考虑,不是同一个父view下的view是不应该相互产生约束关系的(虽然可以这么做),因为会显著增加布局引擎的计算复杂度(类似于多个光源在多个物体上产生阴影),而目前iOS12里计算复杂度随view增加是线性增长的;
所以也不要写重复约束和没有用的约束;
don't wedge two layouts into one set of constraints
在一个视图里别搞两套布局关系,如下图
总结:我们使用传统的frame布局就是一个布局、绘制、渲染的过程,使用自动布局其实就多了一个布局引擎运算。视频里特别提到用约束来布局并不会比用frame布局消耗太多的性能,因为engine的计算优化了,过程很高效,所以不要避免使用autolayout,你可以频繁地使用它,但不要添加多余或重复的约束。
和autolayout有关的东西
Inequality(也就是>=和<=)会消耗更多性能吗,和等号相比非常小,只是多了一个变量而已;
setConstant 做了最大程度的优化,鼓励使用;
priority 只是告诉引擎一个minimize error,对性能影响不大;
Instrument增加了新的分析layout性能的工具(一片掌声)
然而目前beta 版里没有...
关于intrinsicContentsize
假如你写死宽高,不需要intrinsicContentsize,可以告诉引擎不用计算这个view的intrinsicContentSize,优化性能。
当然也可以返回一个指定的size,都需要重写,做法如下
关于systemLayoutSizeFittingSize
和intrinsicContentsize相反,他是从布局引擎获取size
每次调用该方法都要创建和销毁一个布局引擎,所以比较耗性能,不应该频繁调用。
关于约束冲突(Unsatisfiable Constraints)
会降低性能,也可能掩盖其他的布局问题,所以一定要解决!