AutoLayout 本质

388 阅读5分钟

AutoLayout 本质

AutoLayout 本质就是一个线性方程解析引擎——布局引擎

Auto Layout 布局,不再(像 frame 一样)关注视图尺寸、位置的常数。而是关注视图之间关系,描述视图的约束关系来确立一组线性等式或不等式方程组,然后通过解析方程组来确立控件的 frame。

一个 NSLayoutConstraint (约束)对象,本质上是表示两个视图之间(当表示尺寸时只表示视图本身)布局关系的一个线性方程,该方程可以是线性等式、也可以是线性不等式。

多个约束对象组成是一个约束集合,本质上是表示某个界面上多个视图之间布局关系的线性方程组。方程组中的多个线性方程,以数字标识的优先级进行排序。

AutoLayout 原则

坚持一致的布局方式

苹果的建议是选择一种合适的布局方式,并坚持一致性。

创建充分的、可满足的约束

约束集合必须是充分的、可满足的。约束不充分(欠约束)、不可满足(约束冲突)会造成视图错位、视图丢失等。要创建充分的、可满足的约束需要遵循一定的布局原则。

AutoLayout 机制

AutoLayout 布局过程涉及延迟机制,并非一有约束更新就马上进行布局重绘,当有约束更改时,系统的默认做法是延迟更新,目的是实现批量更改约束、绘制视图,避免频繁遍历视图层级,优化性能。当更新约束太慢影响到后序代码逻辑,也可强制马上更新。

  • layoutIfNeeded

使用此方法可强制视图立即更新布局。使用自动布局时,布局引擎根据需要更新视图的位置,以满足约束中的更改。此方法使用接收消息的视图作为根视图,从根开始布局视图子树。如果没有挂起的布局更新,则此方法将退出,而不修改布局或调用任何与布局相关的回调。

  • setNeedsLayout

此方法记录请求并立即返回。由于此方法不强制立即更新,而是等待下一个更新周期,因此可以使用它在更新任何视图之前使多个视图的布局无效。此行为允许您将所有布局更新合并到一个更新周期中,这通常会提高性能。

AutoLayout 流程

App启动后,RunLoop 循环检测图层树中是否存在约束变化;

当 RunLoop 检测到约束变化(直接或间接设置、更新、移除约束);布局引擎就会重新计算布局,然后那些由于约束更新导致位置、尺寸发生改变的视图的父视图会调用setNeedsLayout方法打上需要布局的标记,此时视图新的 frame 已经存在布局引擎中,但是视图还没有更新位置、尺寸;

接着进入延迟布局阶段,该阶段第一步是从子视图到父视图遍历视图层级,调用 UIView 的updateConstraints方法。确保将要发生改变的视图能够在此时更新(在遍历视图树重新摆放视图之前及时更新, 做容错处理)。第二步是从子视图到父视图遍历视图层级,调用更新约束时被标记为上需要布局的视图的lyoutSubviews方法(UIViewController 是对应viewWillLayoutSubviews),让方法调用者重新布局它的子视图。实际上该步骤是从布局引擎中把视图的位置、尺寸的值读取出来设置到对应的视图上,并绘制出来。

AutoLayout 补充

约束变化的两个步骤:

  1. 约束更新;

约束作为输入,以线性方程集合的形式存放在布局引擎中,当涉及到约束方程组的更改,就属于约束变化。例如激活/失效约束对象;修改约束常量 constant;修改约束优先级 priority(改变约束方程组的排序关系);修改视图树的结构。

  1. 布局引擎重新计算布局。

约束表达式是由表示视图的位置、尺寸的变量构成,当约束更新后,布局引擎重新计算布局。重新计算布局后,那些由于约束更新导致位置、尺寸发生改变的视图的父视图会调用setNeedsLayout方法打上需要布局的标记,。此时视图新的 frame 已经存在布局引擎中,但是视图还没有更新位置、尺寸。接下来延迟布局将会被安排执行。

延迟布局的两个步骤:

  1. 约束更新

到了延迟布局阶段,再次更新约束(从子视图到父视图遍历视图层级,调用 UIView 的updateConstraints方法)。确保将要发生改变的视图能够在此时更新,在遍历视图树重新摆放视图之前及时更新。

  1. 重新赋值视图 frames,更新视图的位置、尺寸

从子视图到父视图遍历视图层级,调用更新约束时被标记为上需要布局的视图的lyoutSubviews方法(UIViewController 是对应viewWillLayoutSubviews),让方法调用者重新布局它的子视图。实际上该步骤是从布局引擎中把视图的位置、尺寸的值读取出来设置到对应的视图上,并绘制出来。

视图本身 frame 在layoutSubviews方法调用前已经有值,调用后该值不变;子视图 frame 在该方法调用前是旧值,该方法调用完毕会赋上新值。