跳到主要内容

UIKit 布局

UIKit 布局决定视图在界面中的位置和尺寸。手工设置几何属性与 Auto Layout 约束是两种相关手段:前者直接给出几何结果,后者描述视图之间必须满足的关系,再由布局系统求解。

页面显示异常时,先判断问题属于坐标、约束、布局时机还是绘制合成。改变一个约束后 frame 暂时没变,与图层绘制成本过高,是不同问题,排查方式也不同。

1. 面试核心​

先记住这几条,再读正文

2. UIView、CALayer 与坐标​

2.1 视图和图层的分工​

UIView 管理界面区域、视图层级、布局和事件响应。CALayer 保存视觉内容与相关属性,并参与动画和合成。UIKit 的 UIView 由图层支持,可以通过 view.layer 配置阴影、边框等效果。

CALayer 不属于 UIResponder 响应链。直接添加一个图层,不能期待它自动获得 UIButton 的触摸处理、无障碍语义或 Auto Layout 约束行为。纯装饰内容可以使用图层,但交互与布局责任仍需要明确。

UIKit 管理着视图与其 backing layer 的协作,不能任意替换这层关系或把视图关联图层的 delegate 当作普通业务代理改写。界面层级操作与布局一般应在主线程执行;把相同 API 移到后台队列并不能自动解决主线程卡顿。

2.2 frame、bounds 与 center​

在没有非单位变换的情况下,frame 描述视图在父视图坐标系中的位置和尺寸;bounds 描述视图自身的坐标区域;center 表示视图中心在父视图坐标系中的位置。它们的单位是 point,不等于固定数量的物理像素。

bounds.origin 默认是 (0, 0),但可以改变。它改变的是视图内部坐标区域,不必让视图在父视图中的位置一起移动。下面通过坐标转换观察差异:

import UIKit

@MainActor
func coordinateExample() -> (CGPoint, CGPoint) {
let parent = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 300))
let child = UIView(frame: CGRect(x: 30, y: 40, width: 100, height: 60))
parent.addSubview(child)

let before = child.convert(CGPoint.zero, to: parent)
child.bounds.origin = CGPoint(x: 10, y: 20)
let after = child.convert(CGPoint.zero, to: parent)
return (before, after)
}

变更前,child 的本地 (0, 0) 对应父坐标 (30, 40);变更后,它对应 (20, 20)。视图在父坐标中的 frame 没有因为这次原点变化而平移,但本地坐标与父坐标之间的映射改变了。

跨层级或跨窗口计算位置时,优先使用 convert 等坐标转换接口,不手工累加几个 frame。滚动、非零 bounds 原点和变换都会让手工计算遗漏条件。

2.3 transform 与布局几何​

transform 对视图施加平移、缩放或旋转等仿射变换。UIView 官方契约要求:当 transform 不是单位变换时,frame 的值不应作为可靠依据使用或修改。需要调整此时的几何,应根据具体操作使用 bounds、center 和坐标转换。

例如一个旋转后的视图,在父坐标里占据的轴对齐包围区域,不等于它在自身坐标里的内容尺寸。不能拿某个观察到的 frame 宽度反推原始内容宽度,再继续用它构造约束。

使用 Auto Layout 时,约束决定布局几何。直接改受约束视图的 frame,可能在下一次布局时被重新计算的结果覆盖。需要改变布局时更新约束;需要在已有布局基础上做视觉动画时,再明确变换的用途。

3. Auto Layout 的约束模型​

3.1 用关系求解尺寸和位置​

约束表达两个布局属性之间的相等或不等关系,例如“按钮左边与容器左边相差 20 point”“两个按钮宽度相同”。关系还可以包含乘数、常量和优先级。

求解需要足够的信息确定目标几何,但这些信息不全来自显式宽高约束。边缘关系、内容自然尺寸、布局引导和容器已有几何,都可能参与求解。因此“一个视图固定写四条约束就够了”不成立:四条互相重复的约束仍可能留下自由度。

代码创建视图后,使用自定义 Auto Layout 约束通常需要把 translatesAutoresizingMaskIntoConstraints 设为 false,避免旧的 autoresizing mask 被转换成另一组约束,与自定义关系冲突。这个设置不代表视图会自动获得完整布局。

safeAreaLayoutGuide 表达相对于当前视图安全区域的布局关系,不是固定的顶部或底部数值。导航容器、设备、窗口大小和系统界面都会影响它。相同内容在不同容器里,不应一律硬编码同一个状态栏高度。

3.2 优先级、冲突与歧义​

优先级决定约束发生竞争时的取舍。required 的优先级为 1000,必须满足;较低优先级允许布局系统在必要时放弃相应要求。比如宽度必须等于 200,同时有一个优先级 999 的“希望宽度等于 100”,可以保留前者而不满足后者。

如果这两个矛盾关系都要求 1000,就构成不可满足约束。UIKit 会报告冲突,并尝试打破部分约束以继续显示;具体打破哪一条不应当成为业务依赖。应根据需求调整约束或优先级,而不是看到界面能用就忽略日志。

布局歧义表示约束允许多个结果,例如普通 UIView 只有中心位置,没有足够信息决定宽高。它和互相矛盾的约束不是同一个问题。可用 hasAmbiguousLayout、约束检查与调试工具定位。应在视图接入实际层级并完成相关布局后检查,同时覆盖不同内容、语言和尺寸条件。

把所有约束都提到 1000,不会自动提升正确性;随意降低所有优先级,也只是让布局丢失意图。应明确哪些关系必须保证,哪些尺寸或间距可以先让步。

3.3 内容自然尺寸与抗拉伸、抗压缩​

intrinsic content size 是视图根据内容提供的自然尺寸,例如 UILabel 的文本和字体会影响它。普通 UIView 通常没有这样的自然宽高,不能把任何子视图都当作自带尺寸。

Content hugging 倾向于不让视图比自然尺寸更大;compression resistance 倾向于不让视图比自然尺寸更小。两个优先级分别作用于水平和垂直方向:空间多时需要决定谁伸展,空间不足时需要决定谁先压缩。

两个标签排在一行时,可以让某个标签更容易拉长,同时让另一个标签更不容易被压缩。多行文本还需要合适的可用宽度才能确定换行后的高度。动态字体、长翻译文本和空内容会改变约束结果,需要在这些输入下检查,而不是只验证默认中文标题。

4. 等分布局与更新时机​

4.1 三个按钮等宽排列​

UIStackView 可以管理 arranged subviews 的排列。横向栈配合 .fillEqually 与固定间距,可以表达三个按钮等宽的要求:

import UIKit

@MainActor
func makeToolbar(in container: UIView) -> UIStackView {
let buttons = ["上一项", "收藏", "下一项"].map { title in
let button = UIButton(type: .system)
button.setTitle(title, for: .normal)
return button
}
let stack = UIStackView(arrangedSubviews: buttons)
stack.axis = .horizontal
stack.distribution = .fillEqually
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(stack)
NSLayoutConstraint.activate([
stack.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 20),
stack.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -20),
stack.topAnchor.constraint(equalTo: container.topAnchor),
stack.heightAnchor.constraint(equalToConstant: 44)
])
return stack
}

若容器宽度是 321 point,可分给按钮的宽度为 321 - 40 - 16 = 265 point,每个按钮约 88.333 point。point 坐标允许小数;最终显示还涉及屏幕比例和像素对齐,不需要先把三个宽度各自截为整数,再让最后一个按钮填补逻辑误差。

这段示例刻意使用容器边缘,假定容器本身已经放在合适的可用区域。真正页面若要避开系统遮挡,可把对应约束连接到 safe area。等宽也不是所有内容都适用:长文本、辅助功能字号和最小点击区域仍要验证。

4.2 约束更新通常延后求解​

改变约束常量、激活状态或视图层级后,系统通常安排后续布局过程,不要求在每次赋值之后立即修改全部 frame。这有助于合并同一轮中的多项变化。

API含义
setNeedsUpdateConstraints()标记约束需要更新
setNeedsLayout()标记需要布局,等待后续处理
layoutIfNeeded()如果接收视图的相关布局需要更新,就执行所需布局
setNeedsDisplay()标记内容需要重新绘制,不负责求解约束

layoutSubviews() 是布局回调,不应靠业务代码直接调用来模拟系统流程。自定义布局时按要求调用 super,只处理本次需要改变的子视图;不要在回调里无条件再次标记相同布局,制造反馈循环。

使用约束动画时,通常先让共同容器完成旧布局,再修改约束,在动画块中让该容器执行新布局。调用层级需要覆盖受影响的共同关系;只对一个子视图调用布局,不一定能更新父级约束决定的全部几何。

几何变化也不一定要求重新执行 draw(_:)。已有内容可以随尺寸或合成属性变化重新显示,是否重新绘制取决于内容与配置。将布局、绘制和屏幕合成都统称为“刷新一次”,会掩盖实际成本来源。

5. Masonry 的约束管理​

Masonry 用 Objective-C 的链式接口组织 Auto Layout 约束,本质上仍创建和管理 NSLayoutConstraint,不是另一套绕过 UIKit 的布局引擎。下面按 Masonry 1.1.0 解释三个入口:

方法对已有约束的处理
mas_makeConstraints为本次描述建立约束,不自动把重复描述解释为更新
mas_updateConstraints查找符合匹配条件的 Masonry 约束,找到后更新常量;找不到时可能创建新约束
mas_remakeConstraints先移除该视图已登记的 Masonry 约束,再安装本次描述

1.1.0 的 update 匹配不只看“都是宽度”:还检查两端对象、属性、关系、乘数和优先级等。因此改变约束对象、关系或乘数后,不能假定 update 会替换旧关系,可能留下旧约束并增加新约束。

remake 也不是删除 UIKit、Storyboard 或其他代码为整个界面建立的所有约束。它处理 Masonry 为相关视图登记的集合;新约束仍需要与其他来源的约束兼容。

动态修改一个高度常量时,保留原约束并更新通常更清楚;整体布局关系改变时,再选择明确的移除与重建。框架封装减少了表达代码,并没有改变冲突、歧义、优先级和延迟布局这些基础规则。

6. 布局与渲染成本​

离屏渲染先生成中间图像,再把该结果用于后续合成,而不是直接把所有内容一次写到最终目标。中间存储、额外处理和结果复用都会影响成本;它属于绘制合成层面的机制,不是 Auto Layout 求解的另一种名称。

某些遮罩、阴影和组合效果可能需要额外中间处理,但不能仅看一个 cornerRadius 就断言所有系统与所有组合都会走同一条昂贵路径。应先确定具体内容、裁剪、透明度和动画条件,再用性能工具观察。

设置 shadowPath 可以直接提供阴影形状,避免始终依赖合成后的透明度轮廓来决定形状;尺寸变化时,路径也需要按实际几何更新。shouldRasterize = true 表示先把图层内容栅格化为位图再合成,不等于无条件开启优化。内容频繁变化时,生成中间结果的成本可能反复发生,缩放与栅格化比例还会影响清晰度。

定位问题时,应分别检查主线程上的约束更新、布局与绘制工作,以及渲染合成成本。只有确认哪一段超过预算,才能判断是减少重复约束、简化视图层级、调整效果,还是改变数据更新方式。

7 · 面试题卡片​

本页题目与参考回答

7.1 UIView 与 CALayer 有什么区别?​

考察重点:界面管理、交互与视觉合成的分工。

相关内容:视图和图层的分工。

参考回答

UIView 管理界面层级、布局和交互,CALayer 管理视觉内容及动画合成相关属性。 UIKit 的视图由图层支持,但 CALayer 不属于 UIResponder 响应链,不能仅添加图层就获得控件事件或无障碍行为。自定义效果可以通过图层实现,仍要明确谁负责布局和交互。

7.2 frame 和 bounds 有什么区别?​

考察重点:坐标系、原点与变换条件。

相关内容:frame、bounds 与 center、transform 与布局几何。

参考回答

frame 使用父视图坐标,bounds 使用自身坐标,二者均以 point 表示。 bounds 原点可以改变,进而改变本地到父坐标的映射,不必让父坐标中的视图位置一起移动。transform 非单位变换时,不应继续依赖或修改 frame,应结合 bounds、center 和坐标转换处理。

7.3 Auto Layout 的基本原理与好处是什么?​

考察重点:约束关系、优先级、内容尺寸与适配。

相关内容:Auto Layout 的约束模型。

参考回答

Auto Layout 用约束描述视图属性之间的关系,再结合优先级和内容尺寸求解布局。 它让布局关系随容器尺寸、文本和安全区域变化而调整,减少手工为每种尺寸计算 frame。约束仍需足够且不互相矛盾,也需要验证长文本、动态字号和不同窗口环境。

7.4 约束冲突与布局歧义有什么区别?把优先级全部设为 1000 能解决吗?​

考察重点:可行解、唯一解与约束取舍。

相关内容:优先级、冲突与歧义。

参考回答

冲突表示要求无法同时满足,歧义表示允许多个布局结果。 全部设为 1000 不能补齐缺失信息,还可能让原本可让步的关系变成冲突。应明确必须满足的关系与可让步项,并通过调试和输入场景验证结果。

7.5 Content hugging 与 compression resistance 分别解决什么问题?​

考察重点:自然尺寸、可伸展空间与压缩竞争。

相关内容:内容自然尺寸与抗拉伸、抗压缩。

参考回答

Hugging 抵抗大于自然尺寸,compression resistance 抵抗小于自然尺寸。 空间多时决定谁更容易伸展,空间不足时决定谁更容易压缩;两个方向分别有优先级。它们依赖视图提供的 intrinsic content size,普通 UIView 不会因此自动知道自己的宽高。

7.6 setNeedsLayout、layoutIfNeeded 和 setNeedsDisplay 有什么区别?​

考察重点:延迟布局、按需执行和绘制内容。

相关内容:约束更新通常延后求解。

参考回答

setNeedsLayout 标记后续布局,layoutIfNeeded 按当前需要执行布局,setNeedsDisplay 标记重绘内容。 改约束后不一定立即更新全部 frame,调用布局时还要选对覆盖相关约束的容器。布局完成不等于所有内容都重新绘制,更不等于 GPU 已完成最终显示。

7.7 Masonry 的 make、update 和 remake 有什么区别?​

考察重点:约束创建、匹配更新与登记集合的替换。

相关内容:Masonry 的约束管理。

参考回答

make 建立本次约束,update 尝试匹配已有约束并更新常量,remake 先移除该视图登记的 Masonry 约束再建立新关系。 在 1.1.0 中,update 匹配还要求对象、属性、关系、乘数和优先级等一致,找不到时可能新建,所以不能把它当作任意关系替换。remake 也不会删除整个界面的所有来源约束。

7.8 三个按钮怎样均匀排列?宽度除不尽时怎么办?​

考察重点:相等关系、总宽度、间距和坐标单位。

相关内容:三个按钮等宽排列。

参考回答

可以约束三个按钮等宽并明确边缘和间距,或者使用横向 UIStackView 的 fillEqually。 point 允许小数,不必先把每个宽度截成整数。还要考虑最终像素对齐、动态字号和最小可点击区域,不能只在某个固定宽度下验证。

7.9 什么是离屏渲染?​

考察重点:中间结果、合成成本与效果选择。

相关内容:布局与渲染成本。

参考回答

离屏渲染先生成中间结果,再参与后续合成。 它可能增加中间存储和处理成本,也可能让稳定内容得到复用。具体效果是否触发额外路径要结合组合与实现,不能简单说所有圆角都昂贵;shadowPath 和 shouldRasterize 也需要根据几何、更新频率和清晰度测量取舍。

8. 参考资料​

来源:Coding 101 原文 · 免费阅读,引用请保留出处。内容使用说明