Layout 拾遗

布局的概念

所谓布局,就是无论屏幕外在尺寸,还是视图内容内在变化时在给一个视图指定一个大小和坐标;
那 Auto Layout (自动布局),就是在一定条件下,自动去为视图指定他的大小和位置。

如何布局

  • 手动布局:直接显式的通过设置视图的 Frame 的方式来直接指定视图的大小和位置

UIView *view = [[UIView alloc] initWithFrame:CGRectMake(10, 20, 30, 40)];

or

view.Frame = CGRectMake(10, 20, 30, 40);

  • Auto Layout (自动布局):Auto Layout 本质就是一个线性方程解析 Engine。基于 Auto Layout 的布局, 不需要像手动布局时代那样去一一的为每个视图去指定他的坐标和大小,转而是通过约束来描述视图之间的关系,进而形成一系列的方程组,然后让这个 Engine 来解方程组得到视图的大小和坐标(如果对于新手来说,没看懂无所谓,不要放弃,继续往下看,自然会懂的)

Auto Layout

  • 约束(NSLayoutConstraint)

表示两个视图之间(当表示尺寸时只表示视图本身)布局关系的一个线性方程,该方程可以是线性等式、也可以是线性不等式,所以,一个约束就可以理解为一个线性方程。举个栗子:

NSLayoutConstraint
  • 原理

由此可见,如果屏幕上的多个 View 之间都可以有约束,约束对象组成一个约束集合,那其实本质上是多个视图之间布局关系的线性方程组,Auto Layout Engine 根据按照线性方程的优先级(UILayoutPriority)从高到底对线性方程组进行解析,求得方程组的解。 所以自然而然想到方程组肯定有两种异常情况,即约束错误:

多解:设置的约束欠缺,导致线性方程没有唯一解。

无解:设置的约束过多,当设置的约束过多,存在多个优先级相同的描述同一个关系的线性方程,并且约束产生的效果不同(例如 View1.left = View2.right + 10 ; View1.left = View2.right + 20,优先级都为1000),线程方程组无解。

布局处理流程

The Layout Cycle

这是一个上层的抽象综合描述

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

  • 当发生 Constrints Change(直接or间接设置、更新、移除约束),RunLoop检测到约束变化;

  • RunLoop 发现约束变化后,就会进入Deferred Layout阶段,视图的位置,这个阶段会将试图的尺寸,位置计算出来并设置到对应视图上,然后绘制出来;

  • 完成之后,Runloop 则会继续检查约束更新情况,当再次发现时,继续走一遍流程

1. Constraints Change

注意,这是一个过程,你可以理解为检测到约束变化后的的第一个过程(即约束变化了,导致出现了”约束变化“这样一个过程)前面已经提到,我们所有的约束其实最终都会转换为一系列的线性表达式放在 Auto Layout Engine 中,所以凡是能影响表达式的都算导致约束变化比如添加或删除约束,修改约束的优先级 Priority,添加或者移除试图。

当约束变化之后,Layout Engine 便会为重新去计算各个试图的的 Layout , 然后算出各个 View 的大小或者坐标,最后在调用 superView.setNeedsLayout(),触发下一个过程 Deferred Layout Pass。注意此时界面还是没有变化的,还没将更新渲染到界面上。

Constraints Change

Deferred Layout Pass

Deferred Layout Pass

Deferred Layout Pass过程包括两个步骤 Update Constraints ;Reassign View Frames。

  • Update Constraints

这里又出现一个更新约束,前面的 Constraints Change 过程已经更新约束,到了 Deferred Layout Pass 阶段,又需要更新约束。其实,这一步是一个保护操作,确保上一次更新约束到接下来真正重新布局之间发生的约束上的变化能及时的取到。在这一步,每个视图会通过调用 setNeedsUpdateConstraints()的方式来调用每个视图的 updateConstraints方法。这一步会给开发者最后的机会来修改你的约束,并且非常的及时。。。。。。。。But,正常来说,这是没有必要的,也是不推荐的,但总要说说什么时候建议不用,什么时候建议用吧,其实核心在于性能:

Update Constraints

在这个地方修改约束,比在别的地方修改约束更快,也就是你发现你的代码在别处更新约束很慢的时候,这里可以试一下;当然,还有一种情况就是,你的约束是随着某个配置动态变化的,在变化较多的情况下,可以在这里面写。比其他方式有效多了

当这一步的更新约束完成之后,所有的约束都是最新的,接下来就是真正的 Reposition Views了

  • Reassign view frames
Reassign view frames

该步骤从上到下遍历视图层级,调用更新约束时被标记为 needing layout 的视图的 layoutSubviews 方法,让方法调用者重新布局它的子视图(注意不是本身)。可以重写 layoutSubviews 进行监听。实际上这个阶段是从 Layout Engine 中把视图的位置、尺寸的值读取出来设置到对应的视图上

Summary

By the way: 很多人其实喜欢在这儿重写 layoutSubviews 来做自定义的 layout , 来自 Apple 的工程师是建议只有当你的 layout 不能用约束来表达的时候在这里自定义;其实你通过约束来实现布局,会给你少去很多问题重写 layoutSubviews 需要记住几点

  • 必须调用 super 的实现 ([super layoutSubviews]);
  • 如果想要 invalidate 子视图的布局,需要在调用super的实现之前;
  • 不要在此调用 setNeedsUpdateConstraints,因为 update constraints pass 已经过了,在此调用为时已晚;
  • 不要 invalidate 视图对应是子视图树以外的视图,该方法只应该对子视图树负责,操作子视图树以外的视图可能会造成循环布局;

小结

本文旨在对 Layout 中涉及到的一些知识点进行梳理,有些个人的理解也穿插在其中,如果个人理解有误,还望探讨指正

参考资料

推荐阅读更多精彩内容