本文结合科技文献、学术论文与业界实践,系统介绍 iOS/macOS 下的 Auto Layout DSL 库 Masonry:技术演进、核心原理(含 Cassowary 约束求解)、应用场景、源码架构与设计模式,并配有流程图、泳道图与思维导图。内容涵盖库的源码剖析及大厂使用心得,从基础概念到高级应用形成完整知识体系。
目录
- 一、Masonry 使用详解
- 二、Masonry 源码解析
- 三、设计模式与延伸
- 四、Masonry 中的优秀编程思想
-
- 流式接口 · 2. DSL · 3. 组合统一接口 · 4. 两阶段处理 · 5. 装箱与类型擦除 · 6. 抽象方法显式失败 · 7. 编程思想小结
-
- 五、编程思想与设计模式提炼总结
- 参考文献
- 延伸阅读
一、Masonry 使用详解
1. 框架概述
Masonry 是面向 Objective-C 的 Auto Layout DSL(领域特定语言),用于在代码中以声明式、链式语法描述视图的布局约束,替代冗长的 NSLayoutConstraint 手写与 Visual Format 字符串 [[1]]。其设计目标可概括为:
- 可读性:约束意图接近自然语言(如“左边等于父视图左边”“宽度等于 100”)。
- 简洁性:用 Block 链式调用替代多参数、多行的系统 API。
- 可维护性:链式调用便于增删约束、设置优先级与调试冲突。
Masonry 由 SnapKit 组织 在 GitHub 上维护,采用 MIT 协议;其 Swift 继任者为 SnapKit,二者共享同一套“链式 DSL 描述约束”的设计哲学 [[2]]。在 Objective-C 时代,Masonry 成为纯代码 Auto Layout 的事实标准之一,被广泛应用于 iOS/macOS 项目。
2. 历史演进
布局方式的演进与 Apple 布局技术、学术成果及开源生态并行,可概括为如下时间线。
flowchart LR
subgraph 学术与系统
A[1997 Cassowary 论文]
B[2011 Auto Layout 引入]
C[iOS 6 正式支持]
end
subgraph 开发方式
D[手写 NSLayoutConstraint]
E[Visual Format]
F[2014 Masonry]
G[2015+ SnapKit]
end
A --> B
B --> C
C --> D
D --> E
E --> F
F --> G
| 阶段 | 代表 | 特点 |
|---|---|---|
| 手写约束 | NSLayoutConstraint(item:attribute:relatedBy:toItem:attribute:multiplier:constant:) | 冗长、易出错、难以阅读 [[3]]。 |
| Visual Format | V:|[a]-[b]| 等 | 字符串描述,类型不安全,复杂布局难表达。 |
| Masonry | Objective-C,Block 链式 | 链式 DSL、可读性高,成为 OC 时代事实标准 [[1]]。 |
| SnapKit | Swift,Closure 链式 | 延续 Masonry 思想,面向 Swift 生态。 |
Apple 于 2011 年在 macOS Lion(及后续 iOS 6)中采用 Cassowary 作为布局引擎 [[4]][[5]],将约束转化为线性方程组求解;第三方 DSL 如 Masonry 正是在系统 API 仍显冗长的背景下流行起来的 [[6]]。
3. 理论基础:Auto Layout 与 Cassowary
Masonry 的约束最终仍通过 Auto Layout 交给系统布局引擎执行。Auto Layout 的数学基础是 Cassowary 约束求解算法,理解其思想有助于理解“约束冲突”“优先级”“内在尺寸”等概念。
3.1 Cassowary 算法简述
Cassowary 是一种 增量式线性约束求解算法,基于对偶单纯形法(dual simplex),用于求解由 线性等式与不等式 组成的约束系统 [[7]][[8]]。其特点包括:
- 线性:约束可写成形如 (a_1 x_1 + a_2 x_2 + \cdots = b) 或 (\le/\ge) 的形式,与“视图 A 的左边 = 视图 B 的右边 + 常数”等布局关系一致。
- 增量:可动态增删约束并高效重新求解,适合交互式 UI(窗口缩放、动画中更新约束)。
- 约束层次(constraint hierarchy):支持 required 与 preferred(优先级),在约束冲突时按优先级舍弃或松弛部分约束,避免无解。
约束层次与松弛原理(简述):Cassowary 将约束按优先级分层(如 required=1000,high=750,low=250)。求解时先满足最高层;若存在冲突则引入松弛变量,允许低优先级约束在“尽量满足”的意义下被违反,从而得到唯一解 [[9]]。例如“宽度 = 父视图一半”与“宽度 ≥ 100”冲突时,若前者优先级较低,则在小屏上会优先保证 width ≥ 100。
参考文献:
- 原始论文:Solving Linear Arithmetic Constraints for User Interface Applications,UIST 1997 [[9]]。
- 扩展与实现:Washington 大学 Cassowary 工具包 [[10]]。
3.2 从约束描述到线性关系(概念)
Auto Layout 将每条约束映射为关于视图几何变量(如 left, right, width, centerX)的线性等式或不等式。Masonry 所写的“左边等于父视图左边 + 20”即对应:
- 变量:
view.left、superview.left - 关系:
view.left = superview.left + 20
多约束组成方程组,由 Cassowary 求解得到每个变量的值,从而得到各视图的 frame。优先级 对应 Cassowary 的强弱约束:高优先级必须满足,低优先级在冲突时可被违反。
约束的线性形式(概念):单条约束可写为线性等式或不等式,例如
( \text{view.left} = \text{superview.left} + 20 )
或带倍数:( \text{view.width} = \text{superview.width} \times 0.5 )。Cassowary 将整套约束表示为 ( A\bm{x} = \bm{b} )(或 (\le/\ge)),在满足约束层次的前提下求 (\bm{x})(各几何变量)[[9]]。
约束求解顺序(概念):系统在布局时并非“从左到右”或“从顶到底”逐视图计算,而是将所有约束汇总为全局线性系统,由 Cassowary 一次性求解;因此修改任意一条约束或某个视图的 intrinsicContentSize,都可能触发整棵视图树的布局重算。Masonry 只负责生成约束,不参与求解顺序。
3.3 流程图:从 Masonry 到屏幕像素(概念层)
flowchart LR
A[Masonry API 调用] --> B[MASConstraintMaker]
B --> C[MASConstraint 描述]
C --> D[NSLayoutConstraint]
D --> E[Auto Layout 引擎]
E --> F[Cassowary 求解]
F --> G[布局结果 / frame]
G --> H[渲染到屏幕]
4. 核心概念
4.1 约束的组成
在 Auto Layout 中,一条约束可抽象为:
Item1.Attribute1 Relation Item2.Attribute2 * Multiplier + Constant
例如:“视图 A 的右边 = 视图 B 的左边 - 8”即 A.right = B.left - 8。Relation 常见为 Equal、LessThanOrEqual、GreaterThanOrEqual,在 Masonry 中对应 equalTo、lessThanOrEqualTo、greaterThanOrEqualTo。Masonry 的链式 API 就是对这五元组(Item1, Attribute1, Relation, Item2, Attribute2, Multiplier, Constant)的封装,并增加 优先级(Priority) 与 标识(Identifier) 等元数据。
4.2 优先级与内在尺寸
| 概念 | 说明 |
|---|---|
| 约束优先级 | UILayoutPriority(0–1000),数值越大越优先;系统在冲突时打破低优先级约束。 |
| Content Hugging | “抗拉伸”:视图不愿比其内在内容尺寸更大。 |
| Compression Resistance | “抗压缩”:视图不愿比其内在内容尺寸更小。 |
Label、Button、ImageView 等有 intrinsicContentSize 的控件依赖 CHCR 与其它约束共同决定最终尺寸;Masonry 可通过 mas_remakeConstraints 等配合系统 API 设置 CHCR。在 Xcode 中可在 Size Inspector 中为视图设置 Content Hugging / Compression Resistance 的优先级(数值越大越“坚持”)。
CHCR 与显式约束的配合原理:布局引擎在确定视图尺寸时,会同时考虑(1)显式约束(如 width = 100)、(2)内在尺寸(如 Label 根据文字算出的宽高)、(3)CHCR 优先级。当“显式约束 + 内在尺寸”存在冗余或冲突时,CHCR 决定谁“让步”:Content Hugging 高则视图不易被拉大,Compression Resistance 高则不易被压小。例如两 Label 横向排列且未固定宽度时,会按 CHCR 分配剩余空间。
flowchart LR
A[显式约束] --> C[布局引擎]
B[内在尺寸 + CHCR] --> C
C --> D[最终 frame]
4.3 约束冲突与满足(概念)
当约束过多或相互矛盾时,系统会按优先级从高到低尝试满足;无法同时满足的约束中,低优先级的会被打破并报错(或在调试时标红)。Masonry 通过 .priority(...) 设置单条约束的优先级,便于在“理想布局”与“保底布局”之间做权衡。
4.4 思维导图:Masonry 概念关系
mindmap
root((Masonry))
使用入口
mas_makeConstraints
mas_remakeConstraints
mas_updateConstraints
描述对象
MASConstraintMaker
MASViewAttribute
MASConstraint
约束属性
left right top bottom
width height centerX centerY
edges size margins
关系与修饰
equalTo mas_equalTo offset multipliedBy priority
底层
NSLayoutConstraint
Auto Layout / Cassowary
5. API 与使用模式
5.1 基本用法(Objective-C)
// 示例:子视图填满父视图边距
[view addSubview:subview];
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(view);
}];
// 示例:水平居中,宽度 100,距顶 20
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.width.mas_equalTo(100);
make.top.equalTo(self.view.mas_top).offset(20);
}];
5.2 常用 API 对照(伪代码语义)
| Masonry 写法 | 含义(伪代码) |
|---|---|
make.left.equalTo(superview) | self.left = superview.left |
make.width.mas_equalTo(100) | self.width = 100 |
make.top.equalTo(other.mas_bottom).offset(8) | self.top = other.bottom + 8 |
make.size.mas_equalTo(CGSizeMake(80, 80)) | self.width = 80, self.height = 80 |
make.edges.equalToSuperview() | 四边与 superview 对齐 |
make.center.equalTo(superview) | centerX/Y 与 superview 对齐 |
make.width.equalTo(superview).multipliedBy(0.5) | self.width = superview.width * 0.5 |
make.priority(MASLayoutPriorityDefaultHigh) | 为该条约束设置优先级 |
5.3 make / remake / update
- mas_makeConstraints:在已有约束基础上追加新约束,不删除旧约束。入口内部会将
translatesAutoresizingMaskIntoConstraints设为NO,无需手动设置。 - mas_remakeConstraints:先移除该视图上由 Masonry 管理的约束,再按 Block 重新添加,适合布局整体变化。
- mas_updateConstraints:仅更新Block 中涉及到的约束的 constant(或部分属性),不改变约束条数或关系,适合仅改“间距/常量”的动画或响应式布局。
伪代码(remake 的语义):
function mas_remakeConstraints(block):
uninstallAllMasonryConstraints()
mas_makeConstraints(block)
5.4 使用案例集
以下案例覆盖常见布局需求,便于对照理解 API 与约束语义。
案例 1:内边距与四边对齐
// 子视图相对父视图四周各留 20pt
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(UIEdgeInsetsMake(20, 20, 20, 20));
}];
// 等价于:left = superview.left+20, right = superview.right-20, top/bottom 同理
案例 2:居中 + 固定尺寸
[avatarView mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self.view);
make.size.mas_equalTo(CGSizeMake(80, 80));
}];
案例 3:两视图水平排列,等分宽度
[viewA mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(container.mas_left);
make.top.bottom.equalTo(container);
make.width.equalTo(viewB);
}];
[viewB mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(viewA.mas_right).offset(8);
make.right.equalTo(container.mas_right);
make.top.bottom.equalTo(container);
}];
案例 4:安全区域与 LayoutGuide(避免被导航栏/标签栏遮挡)
[self.scrollView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.mas_topLayoutGuideBottom); // 在导航栏下方
make.left.right.equalTo(self.view);
make.bottom.equalTo(self.mas_bottomLayoutGuideTop); // 在标签栏上方
}];
案例 5:动画中更新约束 constant
// 先 make 建立约束,并保存对某条约束的引用
__block MASConstraint *topConstraint;
[box mas_makeConstraints:^(MASConstraintMaker *make) {
topConstraint = make.top.equalTo(self.view.mas_top).offset(100);
make.centerX.equalTo(self.view);
make.size.mas_equalTo(CGSizeMake(100, 100));
}];
// 后续动画中只改 constant,用 update
[box mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.view.mas_top).offset(200);
}];
[UIView animateWithDuration:0.3 animations:^{ [self.view layoutIfNeeded]; }];
案例 6:列表 Cell 内多子视图(避免重复添加)
- (void)setupConstraints {
[_iconView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.contentView).offset(16);
make.centerY.equalTo(self.contentView);
make.size.mas_equalTo(CGSizeMake(44, 44));
}];
[_titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(_iconView.mas_right).offset(12);
make.centerY.equalTo(self.contentView);
make.right.lessThanOrEqualTo(self.contentView).offset(-16);
}];
}
- (void)prepareForReuse {
[super prepareForReuse];
// 不在此重复 mas_makeConstraints;若布局需随数据巨变,可 mas_remakeConstraints
}
案例 7:优先级与比例(宽度为父视图一半,但最低 100)
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view);
make.width.equalTo(self.view).multipliedBy(0.5).priorityHigh();
make.width.mas_greaterThanOrEqualTo(100).priorityRequired();
make.top.equalTo(self.view).offset(20);
}];
案例 8:与原生 NSLayoutConstraint 对比
// 原生:一条“左边等于父视图左边+20”需整行多参数
[NSLayoutConstraint constraintWithItem:subview
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:20];
// Masonry:语义相同,一行表达
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(superview).offset(20);
}];
6. 应用场景与最佳实践
| 场景 | 建议 |
|---|---|
| 纯代码 UI | 用 Masonry 替代手写 NSLayoutConstraint,可读性和维护性更好。 |
| 动态布局 | 用 mas_remakeConstraints 或 mas_updateConstraints 配合动画更新约束。 |
| 列表 Cell | 在 prepareForReuse 中避免重复添加约束,可 mas_remakeConstraints 或复用约束并只更新 constant。 |
| UIScrollView 内子视图 | 子视图约束需相对 scrollView 的 contentLayoutGuide(或四边 + 明确宽/高以确定 contentSize),避免约束不足导致布局歧义。 |
| 多分辨率/多设备 | 用 multipliedBy、比例、优先级与 CHCR 适配不同宽度与安全区域。 |
| 约束冲突调试 | 为约束设置 identifier(若使用支持该特性的版本),便于在 Xcode 中识别。 |
7. 业界实践与大厂使用心得
Masonry 自 2013 年由 Jonas Budelmann 创建以来 [[14]],在 iOS 社区被广泛采用,其设计影响了后续 SnapKit、SwiftUI 等布局思路;业界总结的实践与“大厂”级项目的使用方式,可作为理论之外的补充参考。
7.1 开发效率与代码量
- 代码量对比:相比原生
NSLayoutConstraint多参数、多行写法,使用 Masonry 可将布局代码量减少约 60%–80%;原本需 20 余行的约束描述,用 Masonry 往往 3–5 行即可表达相同意图 [[14]][[15]]。 - 可读性与错误率:链式语法使“左边等于某视图右边 + 间距”等意图一目了然,强类型接口减少参数顺序错误;新成员更容易理解现有布局逻辑 [[15]][[16]]。
7.2 三个核心 API 的选型(结合源码语义)
| 方法 | 行为(结合源码) | 典型场景 |
|---|---|---|
| mas_makeConstraints: | 不移除已有 Masonry 约束,在 Maker 中追加新约束并 install | 初始布局、逐步添加约束 |
| mas_remakeConstraints: | 先 uninstall 该视图上所有由 Masonry 管理的约束,再执行 block 重新 make 并 install | 布局整体变化(如横竖屏、显隐导致结构变化) |
| mas_updateConstraints: | 只更新已存在约束的 constant(或部分可更新字段),不增删约束条数 | 动画中改间距、响应式微调 |
选型原则:能 update 就不 remake,能 remake 就不在外部手动移除再 make,以降低遗漏或重复约束的风险 [[16]]。
7.3 常见实践场景(来自社区与项目总结)
- 相对父视图:
edges、center、size配合insets/offset实现内边距与居中;安全区域可用mas_topLayoutGuide/mas_bottomLayoutGuide或 Safe Area API 避免视图穿透导航栏/标签栏 [[16]][[17]]。 - 相对兄弟视图:
equalTo(other.mas_left)、equalTo(other.mas_bottom).offset(8)等明确描述视图间关系;列表 Cell 内多视图约束建议在prepareForReuse中统一remake或只更新 constant,避免重复添加 [[17]]。 - 复合约束:
edges(四边)、size(宽高)、center(中心)一次生成多条约束,既减少重复代码又保证语义一致 [[14]]。
7.4 思维导图:API 选型与场景
mindmap
root((Masonry 实践))
初始布局
mas_makeConstraints
只增不减
布局巨变
mas_remakeConstraints
先卸后建
微调/动画
mas_updateConstraints
只改 constant
适配与安全
LayoutGuide / Safe Area
multipliedBy 比例
二、Masonry 源码解析
1. 整体架构与类结构
Masonry 的代码结构可分层为:DSL 入口层 → 约束描述层(Maker + 组合约束) → 约束实体层(MASConstraint) → 系统桥接层(NSLayoutConstraint)。
flowchart TB
subgraph 入口
A[View.mas_makeConstraints]
end
subgraph DSL
B[MASConstraintMaker]
C[MASCompositeConstraint]
D[MASViewAttribute]
end
subgraph 约束实体
E[MASViewConstraint]
F[MASLayoutConstraint]
end
subgraph 系统
G[NSLayoutConstraint]
end
A --> B
B --> C
B --> E
C --> E
E --> D
E --> F
F --> G
- 入口:
UIView+MASAdditions为视图提供mas_makeConstraints:/mas_remakeConstraints:/mas_updateConstraints:,接收(MASConstraintMaker *)Block。 - MASConstraintMaker:Block 中的
make对象,持有当前视图及一组约束描述;调用make.left、make.edges等会返回 MASConstraint(可能是复合或单条)。Maker 提供基础属性(left、top、right、bottom、leading、trailing)、尺寸(width、height)、居中(centerX、centerY、baseline)、边距(*Margin)及复合属性(edges、size、center)[[18]]。 - MASCompositeConstraint:组合多条 MASViewConstraint(如
edges对应 left/right/top/bottom 四条),形成树状结构,对应组合模式。 - MASViewConstraint:描述单条约束(某属性 与 某目标 的 关系、倍数、常量、优先级),最终生成 MASLayoutConstraint(NSLayoutConstraint 子类)并安装。
1.2 源码级调用链:从 make.left 到约束创建
所有“单属性”约束(如 left、width)在 Maker 中最终都通过 addConstraintWithLayoutAttribute: 统一入口创建;复合属性(如 edges)则在该方法上层按多个 NSLayoutAttribute 分别调用。流程可概括为:
flowchart LR
A[make.left] --> B[addConstraintWithLayoutAttribute: Left]
B --> C[constraint: addConstraintWithLayoutAttribute:]
C --> D[MASViewConstraint 创建]
D --> E[加入 Maker 的约束数组]
E --> F[install 时生成 NSLayoutConstraint]
对应源码逻辑(伪代码) [[18]]:
// MASConstraintMaker
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)attr {
return [self constraint:nil addConstraintWithLayoutAttribute:attr];
}
// 若为复合属性(如 edges),则创建 MASCompositeConstraint 并为其添加多条 MASViewConstraint;
// 否则创建单条 MASViewConstraint,存入 constraintMaker 的约束列表,供 install 时统一安装。
1.3 结合掘金文章:从 make 到 install 的完整链路
以下内容综合自掘金文章《Masonry实现原理并没有那么可怕》[[19]],与源码对照便于理解 Maker、链式多属性及 install 的细节。
(1)mas_makeConstraints: 入口与两阶段
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO; // 手写约束前必须关闭 autoresizing 转约束
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker); // 阶段一:Block 内 make.xxx 只往 maker 里“登记”约束
return [constraintMaker install]; // 阶段二:统一创建 NSLayoutConstraint 并添加到视图
}
make 即传入 Block 的 MASConstraintMaker 实例,负责约束的创建与最终的 install [[19]]。
(2)make.left 的三步到 MASViewConstraint
- Step 1:
make.left调用addConstraintWithLayoutAttribute:NSLayoutAttributeLeft。 - Step 2:
addConstraintWithLayoutAttribute:内部调constraint:nil addConstraintWithLayoutAttribute:layoutAttribute(单属性时第一个参数为 nil)。 - Step 3:
constraint:addConstraintWithLayoutAttribute:中创建 MASViewAttribute(封装 View + NSLayoutAttribute)、MASViewConstraint(firstViewAttribute + 后续 secondViewAttribute);若当前 constraint 为 nil,则将 newConstraint 加入 maker 的 constraints 数组并返回。
MASViewAttribute 可理解为“视图 + 布局属性”的可变元组;MASViewConstraint 即一条约束描述,持有 firstViewAttribute 与 secondViewAttribute [[19]]。
(3)make.top.left 的链式多属性:委托与复合替换
make.top 返回的是 MASViewConstraint,而 MASViewConstraint 的父类 MASConstraint 同样定义了 left、right、top 等属性。这些属性的实现会委托回 Maker:
// MASViewConstraint 中
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute]; // delegate 即 Maker
}
此时传入的 constraint 不再为 nil(是当前的 MASViewConstraint)。在 Maker 的 constraint:addConstraintWithLayoutAttribute: 里会创建 MASCompositeConstraint,把“已有约束 + 新约束”包成组合,并调用 constraint:shouldBeReplacedWithConstraint:,在 constraints 数组中找到原约束的位置,用 composite 替换,从而 make.top.left 在数组中表现为一条“组合约束”而非两条独立项 [[19]]。
小结(与掘金文章总结一致):MASConstraintMaker 作为工厂,生产并管理 MASViewConstraint(单条)与 MASCompositeConstraint(组合);二者均遵循 MASConstraint 抽象,对外统一接口;View+MASAdditions 作为与外界交互的入口,把复杂的约束创建与安装封装在内部,仅暴露简单的 mas_makeConstraints: 等 API [[19]]。
(4)equalTo 与 equalToWithRelation
equalTo(...) 内部对应 equalToWithRelation。若传入的是数组(多目标),会复制当前 MASViewConstraint 并为每个目标设置 secondViewAttribute,包装成 MASCompositeConstraint,同样通过 shouldBeReplacedWithConstraint 替换进 maker;若传入单个对象,则设置 secondViewAttribute 并 return self,支持继续 .offset()、.priority() [[19]]。
2. 组合模式与约束树
Masonry 采用 组合设计模式(Composite Pattern):将对象组合成树状结构以表示“部分-整体”的层次结构,使客户端对叶子节点(单条约束)和组合节点(如 edges、size)的使用方式一致 [[11]]。
注意:此处的“组合”指结构型设计模式中的 Composite,而非“组合优于继承”的泛称。
2.1 组合模式三要素
Masonry 采用了经典的 组合设计模式(Composite Pattern)。
2.1.1 定义
将对象组合成树状结构以表示“部分-整体”的层次结构。组合模式使得用户对单个对象(Leaf)和组合对象(Composite)的使用具有一致性。 注意:这个组合模式不是“组合优于继承”的那种组合,是狭义的指代一种特定的场景(树状结构)
2.1.2 三个设定
- Component 协议:树中的组件(Leaf、Composite)都实现同一协议,使客户端可统一对待。
- Leaf:无子节点的叶子组件,对应单条约束。
- Composite:容器组件,持有子节点(Leaf 或其他 Composite),操作时递归子节点。
结构关系见下方 Mermaid 图与角色对照表。
| 角色 | 在 Masonry 中的对应 |
|---|---|
| Component 协议 | MASConstraint 协议,树中所有节点(叶子与组合)都实现该协议。 |
| Leaf | MASViewConstraint:无子约束,对应单条 NSLayoutConstraint。 |
| Composite | MASCompositeConstraint:持有多个 MASConstraint(可再为叶子或组合),如 edges 包含 left/right/top/bottom。 |
flowchart TB
subgraph Composite
A[MASCompositeConstraint edges]
A --> B[MASViewConstraint left]
A --> C[MASViewConstraint right]
A --> D[MASViewConstraint top]
A --> E[MASViewConstraint bottom]
end
subgraph Leaf
B
C
D
E
end
2.2 在 Cocoa Touch 中的类比
UIView 的层级本身也是组合结构:子视图可包含更多子视图,形成树;Masonry 的约束树与视图树解耦,但都采用“统一接口处理单点与集合”的思想。
2.3 Swift 实现示例(组合模式)
import Foundation
// 一:Component协议:树中的组件(Leaf、Composite)都需要实现这个协议
protocol File {
var name: String { get set }
func showInfo()
}
// 二:Leaf:树结构中的一个没有子元素的组件
class TextFile: File {
var name: String
init(name: String) {
self.name = name
}
func showInfo() {
print("(name) is TextFile")
}
}
class ImageFile: File {
var name: String
init(name: String) {
self.name = name
}
func showInfo() {
print("(name) is ImageFile")
}
}
class VideoFile: File {
var name: String
init(name: String) {
self.name = name
}
func showInfo() {
print("(name) is VideoFile")
}
}
// 三:Composite:容器,与Leaf不同的是有子元素,用来存储Leaf和其他Composite
class Fold: File {
var name: String
private(set) var files: [File] = []
init(name: String) {
self.name = name
}
func showInfo() {
print("(name) is Fold")
files.forEach { (file) in
file.showInfo()
}
}
func addFile(file: File) {
files.append(file)
}
}
class Client {
init() {
}
func test() {
let fold1: Fold = Fold.init(name: "fold1")
let fold2: Fold = Fold.init(name: "fold2")
let text1: TextFile = TextFile.init(name: "text1")
let text2: TextFile = TextFile.init(name: "text2")
let image1: ImageFile = ImageFile.init(name: "image1")
let image2: ImageFile = ImageFile.init(name: "image2")
let video1: VideoFile = VideoFile.init(name: "video1")
let video2: VideoFile = VideoFile.init(name: "video2")
fold1.addFile(file: text1)
fold2.addFile(file: text2)
fold1.addFile(file: image1)
fold2.addFile(file: image2)
fold1.addFile(file: video1)
fold2.addFile(file: video2)
fold1.addFile(file: fold2)
fold1.showInfo()
}
}
2.4 参考资料
3. 工厂模式与链式语法
本节单独展开 Masonry 中工厂模式与链式语法的设计与实现:前者负责“按需创建约束对象”,后者负责“让约束描述可连续书写、易读易维护”。
扩展:简单工厂 | 工厂方法 | 抽象工厂 三种模式辨析
在分析 Masonry 的“工厂”角色之前,先对 GoF 及业界常说的三类工厂型创建模式做一统一定义与对比,便于理解 Masonry 更贴近哪一种、以及为何不采用另一种。
1)简单工厂模式(Simple Factory)
定义:由一个具体工厂类根据参数/类型决定创建哪一种具体产品,并返回产品的抽象类型给调用方。不属于 GoF 23 种设计模式之一,但实践中极为常见。
核心特征:
- 一个工厂类:无抽象工厂接口、无工厂子类,所有创建逻辑集中在一个类的一个方法(或若干静态/实例方法)里。
- 根据参数分支:如
create(type)内部用if/switch或字典映射,type == "A"则new ProductA(),否则new ProductB()。 - 返回抽象类型:方法签名返回抽象产品(接口或基类),调用方只依赖抽象,不依赖 ConcreteProductA/B。
结构示意:
flowchart LR
C[Client] --> F[SimpleFactory]
F --> P1[ProductA]
F --> P2[ProductB]
F --> P3[ProductC]
P1 --> I[Product 接口]
P2 --> I
P3 --> I
C --> I
伪代码:
// 抽象产品
interface Product { void doSomething(); }
// 具体产品
class ProductA : Product { ... }
class ProductB : Product { ... }
// 简单工厂:一个类,一个方法,根据参数创建
class SimpleFactory {
Product create(String type) {
if (type == "A") return new ProductA();
if (type == "B") return new ProductB();
throw new UnsupportedTypeException(type);
}
}
// 调用方
Product p = factory.create("A");
p.doSomething();
优点:实现简单、调用方与具体产品解耦(只依赖 Product)。缺点:新增产品必须修改工厂类内部分支,违反开闭原则;工厂类职责随产品增多而膨胀。
2)工厂方法模式(Factory Method,GoF)
定义:定义用于创建对象的抽象方法(工厂方法),由子类决定实例化哪一个具体产品类。将“创建哪种产品”的决策推迟到子类,符合开闭原则。
核心特征:
- 抽象 Creator + 多个 ConcreteCreator:抽象工厂(或基类)声明
createProduct()抽象方法;每个具体产品对应一个具体工厂子类,在子类中return new ConcreteProduct()。 - 一厂一产品:通常一个 ConcreteCreator 只生产一种 ConcreteProduct(或一个产品族中的一种)。
- 调用方依赖抽象:依赖抽象 Creator 和抽象 Product,通过多态获得具体产品,扩展时只需新增子类,无需改原有类。
结构示意:
flowchart TB
subgraph 调用方
Client
end
subgraph 抽象层
Creator["Creator\n+ factoryMethod()"]
Product["Product"]
end
subgraph 具体层
CreatorA["ConcreteCreatorA\n+ factoryMethod() → ProductA"]
CreatorB["ConcreteCreatorB\n+ factoryMethod() → ProductB"]
ProductA[ProductA]
ProductB[ProductB]
end
Client --> Creator
Creator --> CreatorA
Creator --> CreatorB
CreatorA --> ProductA
CreatorB --> ProductB
ProductA --> Product
ProductB --> Product
伪代码:
// 抽象产品
interface Product { void doSomething(); }
class ProductA : Product { ... }
class ProductB : Product { ... }
// 抽象创建者:声明工厂方法
abstract class Creator {
abstract Product factoryMethod();
void someOperation() { Product p = factoryMethod(); p.doSomething(); }
}
// 具体创建者:各负责一种产品
class ConcreteCreatorA : Creator {
Product factoryMethod() { return new ProductA(); }
}
class ConcreteCreatorB : Creator {
Product factoryMethod() { return new ProductB(); }
}
// 调用方依赖 Creator 抽象,由外部注入 ConcreteCreatorA 或 B
Creator c = new ConcreteCreatorA();
c.someOperation();
与简单工厂对比:扩展新产品时,简单工厂要改工厂类内部代码;工厂方法是新增一个 Creator 子类和一个 Product 子类,原有代码不动,符合开闭原则。
3)抽象工厂模式(Abstract Factory,GoF)
定义:为创建一组相关或相互依赖的产品提供一个接口,而不指定具体类。每个具体工厂负责生产一整族产品(如“现代风椅子+现代风桌子”),不同工厂生产不同族(如“古典风椅子+古典风桌子”)。
核心特征:
- 产品族:多个抽象产品(如 Chair、Table),每个抽象产品有多个具体实现(ModernChair、ClassicChair…)。抽象工厂接口中为每个产品提供一个创建方法(如
createChair()、createTable())。 - 一族一起换:ConcreteFactory1 生产 ModernChair + ModernTable,ConcreteFactory2 生产 ClassicChair + ClassicTable;客户端依赖抽象工厂与抽象产品,通过切换具体工厂即可切换整族风格。
- 解决“系列产品”的创建:适合 UI 主题、跨平台控件族、数据库/连接池族等“多产品、多风格/多实现”的场景。
结构示意:
flowchart TB
subgraph 调用方
Client
end
subgraph 抽象工厂与产品
AF["AbstractFactory\n+ createChair()\n+ createTable()"]
Chair["Chair"]
Table["Table"]
end
subgraph 具体工厂与产品族
CF1["ConcreteFactory1\n→ ModernChair, ModernTable"]
CF2["ConcreteFactory2\n→ ClassicChair, ClassicTable"]
MCh[ModernChair]
MTable[ModernTable]
CCh[ClassicChair]
CTable[ClassicTable]
end
Client --> AF
AF --> CF1
AF --> CF2
CF1 --> MCh
CF1 --> MTable
CF2 --> CCh
CF2 --> CTable
MCh --> Chair
MTable --> Table
CCh --> Chair
CTable --> Table
伪代码:
// 抽象产品族
interface Chair { void sit(); }
interface Table { void put(); }
class ModernChair : Chair { ... }
class ModernTable : Table { ... }
class ClassicChair : Chair { ... }
class ClassicTable : Table { ... }
// 抽象工厂:一族产品的创建接口
interface AbstractFactory {
Chair createChair();
Table createTable();
}
// 具体工厂:生产一族产品
class ModernFactory : AbstractFactory {
Chair createChair() { return new ModernChair(); }
Table createTable() { return new ModernTable(); }
}
class ClassicFactory : AbstractFactory {
Chair createChair() { return new ClassicChair(); }
Table createTable() { return new ClassicTable(); }
}
// 调用方:通过换工厂切换整族
AbstractFactory f = new ModernFactory();
Chair c = f.createChair();
Table t = f.createTable();
与工厂方法对比:工厂方法通常是“一个方法生产一种产品”;抽象工厂是“一个工厂接口里多个方法,每个方法生产一种产品,且这一组产品是相关的一族”。抽象工厂可理解为多产品族的工厂方法组合。
4)三种模式对比表
| 维度 | 简单工厂 | 工厂方法 | 抽象工厂 |
|---|---|---|---|
| 工厂形态 | 一个具体工厂类,无子类 | 抽象 Creator + 多个 ConcreteCreator 子类 | 抽象 AbstractFactory + 多个 ConcreteFactory 子类 |
| 创建方式 | 同一方法内根据参数 if/switch 分支 | 子类重写工厂方法,各返回一种产品 | 子类实现多个创建方法,各返回一族中的一种产品 |
| 产品数量 | 可多种产品,由参数决定 | 通常一厂一种产品 | 一厂一族产品(多个相关产品) |
| 扩展方式 | 新增产品需改工厂类内部 | 新增产品 = 新增 Creator 子类 + Product 子类 | 新增产品族 = 新增 Factory 子类 + 该族各 Product 子类 |
| 开闭原则 | 对扩展不友好(需改工厂) | 对扩展开放(加子类即可) | 对扩展开放(加新工厂子类与产品族) |
| 典型场景 | 产品种类少、变化少、图简单 | 框架/插件:由子类决定具体产品 | 主题/风格/平台:整族产品一起换 |
5)Masonry 与三种模式的关系
- Masonry 的 Maker:只有一个具体类
MASConstraintMaker,根据“请求的属性”(left、top、edges、size…)在同一类内部分支,创建MASViewConstraint或MASCompositeConstraint,并统一以MASConstraint抽象返回。形态上最接近简单工厂(一个工厂类、多种产品、参数即“布局属性”)。 - 为何不是典型工厂方法:没有“抽象 Maker + 多个 ConcreteMaker 子类”,也没有“一个子类只生产一种约束”。创建逻辑集中在 Maker 内部,没有把“创建哪种约束”推迟到子类。
- 为何不是抽象工厂:Masonry 不涉及“一族多产品”的切换(如多套 UI 主题、多平台控件族)。只有一类“产品”——约束描述对象(单条/复合),只是根据属性不同产生不同具体类,不涉及多产品族的抽象工厂接口。
结论:Masonry 采用的主要是简单工厂的形态(集中在一个 Maker 内、按属性分支创建),同时吸收了工厂方法的“调用方只依赖抽象产品(MASConstraint)”的优点,便于阅读和扩展约束类型时在 Maker 内增加分支或复合封装,而无需引入 Maker 子类。
3.1 工厂模式在 Masonry 中的完整映射
3.1.1 工厂方法模式(Factory Method)回顾
上文扩展小节已给出简单工厂、工厂方法、抽象工厂三种模式的定义与对比;§3.2 给出 GoF 工厂方法的标准定义与优缺点。此处仅列出 Masonry 中“工厂”角色的直接对应:
GoF 角色:
- Product(抽象产品):约束对象的抽象,对应
MASConstraint协议。 - ConcreteProduct(具体产品):单条约束 →
MASViewConstraint;复合约束 →MASCompositeConstraint。 - Creator(创建者):负责“生产”约束的工厂,对应
MASConstraintMaker。 - Factory Method(工厂方法):Creator 中根据“请求类型”创建具体产品的方法;在 Masonry 中体现为
addConstraintWithLayoutAttribute:及复合属性的封装(如edges、size)。
Masonry 并未采用“抽象 Creator + 多个 ConcreteCreator 子类”的经典工厂方法结构,而是在一个 Maker 类内根据请求的布局属性(left、top、edges、size 等)决定创建“单条约束”还是“组合约束”,因此更贴近简单工厂 + 工厂方法思想的融合:创建逻辑集中在 Maker 内部,对外只暴露 make.left、make.edges 等统一入口,调用方完全依赖 MASConstraint 抽象,不关心具体是 MASViewConstraint 还是 MASCompositeConstraint。
3.1.2 Masonry 中的“工厂”是谁、生产什么
| 角色 | Masonry 中的对应 | 说明 |
|---|---|---|
| 工厂 / 创建者 | MASConstraintMaker | Block 中的 make,持有 view 和约束数组;根据访问的属性创建约束。 |
| 工厂方法 | addConstraintWithLayoutAttribute:、constraint:addConstraintWithLayoutAttribute: 等 | 根据 NSLayoutAttribute(Left、Top、Width、Height…)或复合键(edges、size、center)创建并返回 MASConstraint。 |
| 抽象产品 | MASConstraint 协议 | 对外统一接口:equalTo、offset、priority、install 等,调用方只依赖该协议。 |
| 具体产品(单条) | MASViewConstraint | 对应一条 NSLayoutConstraint,如 make.left、make.width。 |
| 具体产品(复合) | MASCompositeConstraint | 内部持有多条 MASViewConstraint,如 make.edges、make.size。 |
创建时机:调用方写 make.left 时,Maker 并不立刻创建 NSLayoutConstraint,而是先创建一条“约束描述对象”(MASViewConstraint),加入 Maker 的约束数组;等 Block 执行完毕、执行 [maker install] 时,再遍历这些描述对象,逐个生成并激活 NSLayoutConstraint。因此“工厂”生产的是约束描述对象,真正的系统约束在 install 阶段 才生成。
3.1.3 工厂流程示意(从 make.left 到约束对象)
flowchart LR
A[make.left] --> B[MASConstraintMaker]
B --> C{单属性 or 复合?}
C -->|单属性 Left| D[addConstraintWithLayoutAttribute: Left]
C -->|复合 edges| E[创建 left/right/top/bottom 四条]
D --> F[新建 MASViewConstraint]
E --> G[新建 MASCompositeConstraint]
F --> H[加入 maker.constraints]
G --> H
H --> I[返回 MASConstraint 给调用方]
单属性源码级逻辑(伪代码):
// MASConstraintMaker
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)attr {
return [self constraint:nil addConstraintWithLayoutAttribute:attr];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)attr {
MASViewAttribute *firstViewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:attr];
if (!constraint) {
// 当前无“正在组装的约束”,创建新的 MASViewConstraint 并加入数组
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:firstViewAttribute];
[self.constraints addObject:newConstraint];
newConstraint.delegate = self;
return newConstraint; // 返回给调用方,继续链式 .equalTo(...).offset(...)
}
// 已有约束(如 make.top 返回的),再链 .left:创建复合约束并替换
// ... 创建 MASCompositeConstraint,用 composite 替换数组中原来的 constraint
}
复合属性“edges”的工厂行为(伪代码):
// MASConstraintMaker
- (MASConstraint *)edges {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]
.addConstraintWithLayoutAttribute(NSLayoutAttributeRight)
.addConstraintWithLayoutAttribute(NSLayoutAttributeTop)
.addConstraintWithLayoutAttribute(NSLayoutAttributeBottom);
// 内部会创建 MASCompositeConstraint,包含 left/right/top/bottom 四条 MASViewConstraint
}
因此:工厂思想在 Masonry 中的体现 = Maker 根据“请求的属性”创建相应类型的约束对象(单条或复合),调用方只通过 make.xxx 获取 MASConstraint,不直接 alloc/init 任何具体约束类,符合“将对象创建推迟到专门工厂、调用方依赖抽象”的思想 [[12]]。
3.1.4 工厂模式与“简单工厂”的对比
| 对比项 | 经典工厂方法模式 | Masonry 的 Maker |
|---|---|---|
| 创建者 | 抽象 Creator + 多个 ConcreteCreator 子类 | 单一类 MASConstraintMaker,无子类 |
| 工厂方法 | 子类重写 createProduct,返回抽象 Product | 同一类内根据 layoutAttribute 分支,返回 MASViewConstraint 或 MASCompositeConstraint |
| 扩展方式 | 新增产品时新增 ConcreteCreator 子类 | 新增布局语义时在 Maker 内增加属性或复合封装(如 edges、size) |
| 客户端 | 依赖抽象 Product,不依赖具体类 | 同样只依赖 MASConstraint 协议,不依赖 MASViewConstraint / MASCompositeConstraint |
Masonry 把“创建哪种约束”的逻辑收口在 Maker 的 addConstraintWithLayoutAttribute: 及复合属性里,没有为每种约束单独建工厂子类,因此更接近**简单工厂(Simple Factory)**的“一个工厂类、多种产品”的形态;同时返回的是抽象类型 MASConstraint,又具备工厂方法模式“依赖抽象”的优点。
3.2 GoF 工厂方法模式标准定义(对照理解)
工厂方法模式(Factory Method Pattern)
实质:定义一个用于创建对象的接口(或抽象方法),但让实现该接口的子类来决定实例化哪一个类。工厂方法模式将对象的实例化过程**推迟(defer)**到了子类中。
核心解决的问题: 它解决了客户端代码与具体产品类之间的耦合问题。当系统在编译时无法确定需要创建哪个具体类的对象,或者希望将具体类的实例化逻辑封装在子类中时,该模式尤为适用。
设计优势:
- 符合开闭原则(Open-Closed Principle):系统对扩展开放,对修改关闭。当需要引入新的具体产品时,只需创建一个新的具体工厂子类,而无需修改现有的客户端代码或工厂接口。
- 统一接口编程:客户端仅依赖于产品的抽象接口(或抽象基类),而不依赖具体实现。这确保了无论工厂返回哪种具体产品,客户端都能以一致的方式处理。
结论: 相比于在客户端直接使用
new关键字硬编码具体类,工厂方法模式提供了一种更灵活、更易维护的对象创建策略,特别适用于框架开发或产品族经常变化的场景。
工厂方法模式通过将实例化逻辑推迟到子类,实现创建者与使用者的解耦。要点如下:
3.3 ✅ 主要优点
- 开闭原则:新增产品时只需新增具体工厂子类与产品子类,无需修改现有客户端与抽象接口。
- 单一职责:创建逻辑与业务逻辑分离,客户端只关心“用产品”,不关心“如何造”。
- 低耦合:客户端依赖抽象 Creator 与 Product,便于替换具体实现(如切换数据库驱动)。
- 统一入口:所有创建经工厂方法,便于做日志、权限、缓存等集中控制。
3.4 ❌ 主要缺点
- 类数量增加:每增加一种产品通常需增加一个具体工厂类,产品线大时易产生“类爆炸”。
- 抽象层次加深:调用链变长(客户端 → 具体工厂 → 抽象工厂 → 具体产品),理解成本上升。
- 多参数/多产品族:若需根据多参数动态选产品,或需一次创建一族产品,更适合用抽象工厂或建造者。
3.5 ⚖️ 总结与适用场景建议
| 维度 | 评价 |
|---|---|
| 灵活性 | ⭐⭐⭐⭐⭐ (极高,易于扩展新产品) |
| 可维护性 | ⭐⭐⭐⭐ (高,职责分离清晰) |
| 复杂度 | ⭐⭐ (较低,类数量随产品线性增长) |
| 性能开销 | ⭐⭐⭐ (中等,主要是类加载开销,运行时影响小) |
3.5.1 💡 什么时候应该使用?
- 当你不知道确切需要哪个具体类的对象时:例如,框架开发中,框架本身不知道用户会具体使用哪种控件,由用户子类化框架来指定。
- 当你希望将对象的创建逻辑委托给专门的子类时:不同子类可能需要不同的初始化逻辑或上下文环境。
- 当系统需要遵循开闭原则,频繁增加新产品时:这是最典型的场景。
3.5.2 💡 什么时候不应该使用?
- 产品种类非常固定,且几乎不会变化:此时引入工厂模式是过度设计(Over-engineering),直接
new更简单。 - 一个工厂需要负责创建多种差异巨大的产品:此时可能更适合使用抽象工厂模式(Abstract Factory)或建造者模式(Builder)。
- 项目规模很小,追求极致的代码简洁性:简单的脚本或小型工具类应用中,工厂模式带来的类膨胀可能弊大于利。
3.5.3 代码视角对比
不用工厂:客户端用 if/switch + new 具体类,每增加一种产品都要改此处,违反开闭原则。用工厂方法:客户端依赖抽象工厂与产品,factory.createShape() 由具体工厂子类决定实例化哪种产品;新增产品时只需加新子类,客户端不变。详见上文扩展小节伪代码。
3.6 链式语法(Fluent Interface)完整解析
学习三、链式语法
实现的核心:重写Block属性的Get方法,在Block里返回对象本身
#import "ChainProgramVC.h"
@class ChainAnimal;
typedef void(^GeneralBlockProperty)(int count);
typedef ChainAnimal* (^ChainBlockProperty)(int count);
@interface ChainAnimal : NSObject
@property (nonatomic, strong) GeneralBlockProperty eat1;
@property (nonatomic, strong) ChainBlockProperty eat2;
@end
@implementation ChainAnimal
/**
函数返回一个block,block返回void
*/
-(GeneralBlockProperty)eat1 {
return ^(int count) {
NSLog(@"%s count = %d", __func__, count);
};
}
/**
函数返回一个block,block返回ChainAnimal对象
*/
- (ChainBlockProperty)eat2 {
return ^(int count){
NSLog(@"%s count = %d", __func__, count);
return self;
};
}
@end
@interface ChainProgramVC ()
@property (nonatomic, strong) ChainAnimal *dog;
@end
@implementation ChainProgramVC
- (ChainAnimal *)dog {
if (!_dog) {
_dog = [[ChainAnimal alloc] init];
}
return _dog;
}
- (void)viewDidLoad {
[super viewDidLoad];
[super viewDidLoad];
self.dog.eat1(1);
self.dog.eat2(2).eat2(3).eat2(4).eat1(5);
}
@end
学习四、接口简洁
把复杂留给自己,把简单留给别人
学习五、抽象方法小技巧
#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute {
MASMethodNotImplemented();
}
自己实现类似需求的时候,可以采用这个技巧阻止直接使用抽象方法。
实践:实现一个自定义转场动画的基类
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface BaseAnimatedTransiton : NSObject<UIViewControllerAnimatedTransitioning>
@property (nonatomic, assign) NSTimeInterval p_transitionDuration;
+(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration;
-(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration NS_DESIGNATED_INITIALIZER;
@end
#pragma mark - (Abstract)
@interface BaseAnimatedTransiton (Abstract)
// 子类实现,父类NSException
-(void)animate:(nonnull id<UIViewControllerContextTransitioning>)transitionContext;
@end
NS_ASSUME_NONNULL_END
#import "BaseAnimatedTransiton.h"
@implementation BaseAnimatedTransiton
+(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration {
BaseAnimatedTransiton* obj = [[BaseAnimatedTransiton alloc] init];
obj.p_transitionDuration = transitionDuration;
return obj;
}
-(instancetype)initWithTransitionDuration:(NSTimeInterval)transitionDuration {
if (self = [super init]) {
self.p_transitionDuration = transitionDuration;
}
return self;
}
-(instancetype)init {
return [self initWithTransitionDuration:0.25];
}
-(void)animateTransition:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
[self animate:transitionContext];
}
-(NSTimeInterval)transitionDuration:(nullable id<UIViewControllerContextTransitioning>)transitionContext {
return self.p_transitionDuration;
}
-(void)animate:(nonnull id<UIViewControllerContextTransitioning>)transitionContext {
[self throwException:_cmd];
}
/**
在Masonry的源码中使用的是宏(感觉宏不是很直观)
@param aSelector 方法名字
*/
-(void)throwException:(SEL)aSelector {
@throw [NSException exceptionWithName:NSInternalInconsistencyException
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(aSelector)]
userInfo:nil];
}
@end
学习六、包装任何值类型为一个对象
我们添加约束的时候使用equalTo传入的参数只能是id类型的,而mas_equalTo可以任何类型的数据。
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(100, 100));
make.center.equalTo(self.view);
// 下面这句效果与上面的效果一样
//make.center.mas_equalTo(self.view);
}];
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
/**
* Given a scalar or struct value, wraps it in NSValue
* Based on EXPObjectify: https://github.com/specta/expecta
*/
static inline id _MASBoxValue(const char *type, ...) {
va_list v;
va_start(v, type);
id obj = nil;
if (strcmp(type, @encode(id)) == 0) {
id actual = va_arg(v, id);
obj = actual;
} else if (strcmp(type, @encode(CGPoint)) == 0) {
CGPoint actual = (CGPoint)va_arg(v, CGPoint);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(CGSize)) == 0) {
CGSize actual = (CGSize)va_arg(v, CGSize);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
obj = [NSValue value:&actual withObjCType:type];
} else if (strcmp(type, @encode(double)) == 0) {
double actual = (double)va_arg(v, double);
obj = [NSNumber numberWithDouble:actual];
} else if (strcmp(type, @encode(float)) == 0) {
float actual = (float)va_arg(v, double);
obj = [NSNumber numberWithFloat:actual];
} else if (strcmp(type, @encode(int)) == 0) {
int actual = (int)va_arg(v, int);
obj = [NSNumber numberWithInt:actual];
} else if (strcmp(type, @encode(long)) == 0) {
long actual = (long)va_arg(v, long);
obj = [NSNumber numberWithLong:actual];
} else if (strcmp(type, @encode(long long)) == 0) {
long long actual = (long long)va_arg(v, long long);
obj = [NSNumber numberWithLongLong:actual];
} else if (strcmp(type, @encode(short)) == 0) {
short actual = (short)va_arg(v, int);
obj = [NSNumber numberWithShort:actual];
} else if (strcmp(type, @encode(char)) == 0) {
char actual = (char)va_arg(v, int);
obj = [NSNumber numberWithChar:actual];
} else if (strcmp(type, @encode(bool)) == 0) {
bool actual = (bool)va_arg(v, int);
obj = [NSNumber numberWithBool:actual];
} else if (strcmp(type, @encode(unsigned char)) == 0) {
unsigned char actual = (unsigned char)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedChar:actual];
} else if (strcmp(type, @encode(unsigned int)) == 0) {
unsigned int actual = (unsigned int)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedInt:actual];
} else if (strcmp(type, @encode(unsigned long)) == 0) {
unsigned long actual = (unsigned long)va_arg(v, unsigned long);
obj = [NSNumber numberWithUnsignedLong:actual];
} else if (strcmp(type, @encode(unsigned long long)) == 0) {
unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long);
obj = [NSNumber numberWithUnsignedLongLong:actual];
} else if (strcmp(type, @encode(unsigned short)) == 0) {
unsigned short actual = (unsigned short)va_arg(v, unsigned int);
obj = [NSNumber numberWithUnsignedShort:actual];
}
va_end(v);
return obj;
}
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
其中@encode()是一个编译时特性,其可以将传入的类型转换为标准的OC类型字符串
学习七、Block避免循环应用
在Masonry中,Block持有View所在的ViewController,但是ViewController并没有持有Blcok,因此不会导致循环引用。
[self.view mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.otherView.mas_centerY);
}];
源码:仅仅是
block(constrainMaker),没有被self持有
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
参考资料
链式语法使“多步配置”可以写成一行连贯的调用,如 make.left.equalTo(superview).offset(20).priorityHigh(),读起来接近自然语言。下面从构成要素、实现原理、与 Builder 的关系、多属性链式四方面展开。
3.6.1 链式语法的三要素
| 要素 | 说明 | 在 Masonry 中的体现 |
|---|---|---|
| 统一返回类型 | 每一步方法返回的类型与“可继续调用的对象”一致,通常是 self 或协议类型。 | equalTo、offset、priority 等均返回 MASConstraint *(或 id<MASConstraint>),调用方可持续 .xxx。 |
| 返回 self 或当前对象 | 方法内部完成“设置”后,返回当前对象本身,而不是 void 或无关类型。 | offset(CGFloat) 内部设置 layoutConstant,然后 return self;equalTo(id) 设置 secondViewAttribute 后 return self。 |
| 可选的 Block 封装 | 若参数需要延迟求值或复杂逻辑,可用 Block 作为 getter 的返回值,Block 内再 return self。 | offset、multipliedBy 等用“返回 Block 的 getter”,调用方写 .offset(20) 即调用该 Block(20),Block 内设置后 return self。 |
因此链式语法的实现核心可归纳为:Getter 返回 Block 或直接返回 self;Block 的返回值是当前对象,使每次调用后仍可继续点语法调用。
3.6.2 链式调用与 Builder / 流式接口
链式 API 在《领域驱动设计》等文献中常被称为 流式接口(Fluent Interface):通过方法链使调用读起来像一句“句子”,降低认知负担。与 建造者模式(Builder) 的关系:
- Builder:通常有一个“最终步骤”(如
build()、install()),前面步骤只配置内部状态,不产生最终产品;链式调用用于配置。 - Masonry:前面步骤(
left、equalTo、offset、priority)都是配置,最终“产出”发生在 install 阶段(Block 执行完后由 Maker 统一 install)。因此 Masonry 的链式 + 两阶段(描述 → install)与 Builder 的思想一致。
区别在于:Masonry 的“产品”是约束描述对象(MASConstraint),真正的 NSLayoutConstraint 在 install 时由 Maker 遍历描述对象再生成;Builder 模式里通常是 Director 调用 Builder 的 build 得到产品。共同点都是:链式写配置,最后一步才真正“构建”。
3.6.3 完整调用链示意(一步一返回)
以 make.left.equalTo(superview).offset(20).priorityHigh() 为例,每一步的“谁在返回”如下:
sequenceDiagram
participant C as 调用方
participant M as MASConstraintMaker
participant V as MASViewConstraint
C->>M: make.left
M->>M: addConstraintWithLayoutAttribute(Left)
M->>V: 创建并加入 constraints
M-->>C: 返回 V (MASConstraint)
C->>V: .equalTo(superview)
V->>V: 设置 secondViewAttribute
V-->>C: return self (V)
C->>V: .offset(20)
V->>V: 设置 layoutConstant = 20
V-->>C: return self (V)
C->>V: .priorityHigh()
V->>V: 设置 priority
V-->>C: return self (V)
因此:make.left 返回的是 MASViewConstraint(单条约束描述);之后的 equalTo、offset、priorityHigh 都是这条 MASViewConstraint 的方法,每次返回 self,形成链。
3.6.4 多属性链式(make.top.left)与委托机制
当写成 make.top.left 时,表示“两条独立约束”:top 一条、left 一条。流程是:
make.top:Maker 创建一条 MASViewConstraint(top),加入 constraints 数组,返回这条 MASViewConstraint。- 调用方继续
.left:此时是 MASViewConstraint 的.left被调用(因为 MASConstraint 协议也声明了 left、right、top 等属性)。 - MASViewConstraint 的
left实现:不在自身再绑一条 left,而是委托回 Maker:[self.delegate constraint:self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]。Maker 发现传入的 constraint 非 nil(即当前已有一条 top),会创建 MASCompositeConstraint,把“原来的 top”和“新的 left”包在一起,并在 constraints 数组里用 composite 替换原来的 single constraint。
因此 make.top.left 在 Maker 内部表现为:数组里有一条 MASCompositeConstraint,其内部有两条 MASViewConstraint(top、left)。这样既满足“链式写法”,又保证语义是“两条约束”而不是“一条约束有两个属性”。
3.6.5 链式语法的实现核心(代码级)
核心思路:Getter 返回一个 Block,Block 的返回值是当前对象(或约束对象),从而形成链。
// 概念示例:链式 Block 属性
typedef MASConstraint * (^ChainBlock)(CGFloat value);
- (ChainBlock)offset {
return ^MASConstraint *(CGFloat value) {
self.layoutConstant = value;
return self; // 返回自身,支持继续 .priority(...) 等
};
}
调用顺序示例:make.left.equalTo(superview).offset(20).priority(High) → 先确定“左、等于、目标”,再设 constant,再设优先级,每一步返回可链式对象。
与“非链式”的对比(同一语义):
// 非链式:每步无返回值或返回 void,无法连续写
[constraint setSecondViewAttribute:...];
[constraint setLayoutConstant:20];
[constraint setPriority:MASLayoutPriorityDefaultHigh];
// 链式:每步返回 self,可连续写
[[[constraint equalTo:superview] offset:20] priorityHigh];
// 或写成点语法:constraint.equalTo(superview).offset(20).priorityHigh();
3.6.6 自实现简易链式 API 模板(Objective-C)
若在业务中需要类似 Masonry 的链式配置,可参考以下模板(思想与 Masonry 一致):
// 1. 协议或抽象类型:所有“可链”方法返回自身类型
@protocol Chainable <NSObject>
- (id<Chainable>)offset:(CGFloat)value;
- (id<Chainable>)priority:(UILayoutPriority)priority;
@end
// 2. 实现类:每个方法设置后 return self
@interface MyConstraint : NSObject <Chainable>
@end
@implementation MyConstraint
- (id<Chainable>)offset:(CGFloat)value {
self.layoutConstant = value;
return self;
}
- (id<Chainable>)priority:(UILayoutPriority)priority {
self.priorityValue = priority;
return self;
}
@end
// 3. 使用:链式调用
MyConstraint *c = [[MyConstraint alloc] init];
[[c offset:20] priority:UILayoutPriorityDefaultHigh];
// 或若用 Block 属性:c.offset(20).priority(High);
3.7 equalTo / offset 的链式返回原理(源码级)
链式得以成立的前提是:每一步方法返回的都是“可继续调用的对象”。在 Masonry 中:
- equalTo(id):在 MASViewConstraint 中,会设置 secondViewAttribute(目标视图与属性),并 return self(即当前 MASConstraint),因此可继续写
.offset(20)。 - offset(CGFloat):内部设置 constraint 的 layoutConstant,同样 return self,故可再写
.priority(...)。 - priority(...):设置优先级后仍 return self,便于需要时再链其他修饰。
因此 make.left 返回的是一条“未完成”的 MASViewConstraint;.equalTo(superview) 补全“关系与目标”并仍返回这条约束;.offset(20) 补全 constant 并仍返回同一条约束。同一条约束对象在 Block 执行过程中被逐步“填满”,最后在 Maker 的 install 阶段统一生成 NSLayoutConstraint。若 secondItem 为 nil(如 make.width.mas_equalTo(100)),则对应系统约束的 toItem 为 nil、secondAttribute 为 NSLayoutAttributeNotAnAttribute,表示“与常量比较”。
4. 约束的生成与安装
4.1 安装流程(泳道图)
sequenceDiagram
participant U as 开发者
participant V as View
participant M as MASConstraintMaker
participant C as MASConstraint
participant S as 系统 Auto Layout
U->>V: mas_makeConstraints:
V->>V: translatesAutoresizingMaskIntoConstraints = NO
V->>M: 创建 Maker(view)
V->>M: 执行 block(maker)
loop 每条约束描述
U->>M: make.xxx.equalTo(...).offset(...)
M->>C: 添加/创建 MASConstraint
end
M->>C: install
loop 每条 MASConstraint
C->>S: 创建并激活 NSLayoutConstraint
end
S-->>V: 布局更新
4.2 约束收集与安装算法(伪代码)
阶段一:收集(Block 执行过程中不立即创建 NSLayoutConstraint,只记录描述)
// UIView+MASAdditions
function mas_makeConstraints(block):
self.translatesAutoresizingMaskIntoConstraints = NO
maker = [[MASConstraintMaker alloc] initWithView:self]
block(maker) // 执行过程中,make.left 等向 maker 内部数组追加 MASConstraint
return [maker install]
// MASConstraintMaker -install
function install:
constraints = 本 Maker 已收集的 MASConstraint 列表(单条 + 复合展开后的叶子)
for each constraint in constraints:
constraint.install // 复合约束递归调用子约束的 install
return constraints
阶段二:安装(将每条 MASViewConstraint 转为系统约束并激活)
// MASViewConstraint -install
function install:
if alreadyInstalled then return
layoutConstraint = [NSLayoutConstraint constraintWithItem: firstViewAttribute.view
attribute: firstViewAttribute.layoutAttribute
relatedBy: self.layoutRelation
toItem: secondViewAttribute.view
attribute: secondViewAttribute.layoutAttribute
multiplier: self.layoutMultiplier
constant: self.layoutConstant]
layoutConstraint.priority = self.priority
layoutConstraint.active = YES // 或 addConstraint: 到公共 ancestor
self.installedConstraint = layoutConstraint
说明:复合约束(如 edges)在 install 时遍历其子 MASViewConstraint 并逐一执行上述安装逻辑,保证与单条约束同一套路径,符合组合模式“统一接口”的语义。
4.3 mas_updateConstraints 只更新 constant 的原理
mas_updateConstraints: 与 mas_makeConstraints: 共用同一个 Maker 类型,但行为不同:
- make:每次在 Block 里调用
make.xxx都会新增一条 MASConstraint 并加入列表,install 时全部新建 NSLayoutConstraint 并激活。 - update:Masonry 会为当前视图维护“已由 Masonry 安装的约束”的引用;执行 update 的 Block 时,对
make.xxx的调用会匹配到已有约束(按布局属性等匹配),仅修改该约束的 constant(以及 multiplier/priority 等可写字段),而不再创建新的 NSLayoutConstraint。
因此“只改 constant”的语义在源码层体现为:根据 Block 中访问的属性(如 make.top)找到之前 install 时生成的那条 MASViewConstraint,调用其 setLayoutConstant: 或等价方法,并同步到已存在的 NSLayoutConstraint 的 constant 属性。若 Block 里写了之前 make 时从未出现过的属性,部分版本会新建一条约束(行为以官方实现为准)。这也解释了为何“布局结构不变、只改间距或动画”时推荐用 update,可避免重复约束或多余约束对象。
4.4 与系统 Auto Layout 的衔接
Masonry 不实现自己的布局引擎,而是 生成并激活 NSLayoutConstraint(或其子类 MASLayoutConstraint),完全依赖系统 Auto Layout(及底层 Cassowary 求解器)。约束在 install 时会被添加到合适的视图上:若约束涉及两个视图(firstItem、secondItem),通常添加到二者的公共祖先或 firstItem 的父视图上,以便布局引擎正确参与计算。因此与 Interface Builder、手写约束可混用;约束冲突、无法满足等仍由系统报错。调试时可为约束设置 identifier,在 Xcode 的约束列表与控制台报错中会显示该标识,便于定位冲突约束。
4.5 约束挂载视图与 install 细节(据掘金等源码分析)
结合掘金文章 [[19]] 与源码,install 阶段还有以下要点,便于理解“约束到底加在哪个 view 上”。
Maker 的 install 入口
- 若为 remake(removeExisting = YES),会先通过
[MASViewConstraint installedConstraintsForView:self.view]取出该视图上已由 Masonry 安装的约束,逐个 uninstall,再执行后续 install。 - 遍历 maker 的 constraints 数组,对每条 MASConstraint 调用
constraint.install;install 完成后会清空 maker 的数组,避免重复使用。
MASViewConstraint 的 install:决定 installedView
- 仅尺寸约束(width/height):约束只涉及当前视图自身,没有 secondItem。此时将 当前视图的父视图 作为约束的“关联视图”(secondLayoutItem),以便系统正确解析;约束会添加到当前视图或父视图上(源码中 firstViewAttribute.isSizeAttribute 时 installedView = firstViewAttribute.view)。
- 存在相对视图(如
equalTo(otherView.mas_top)):会求两个视图的 最近公共父视图(closestCommonSuperview),把 NSLayoutConstraint 添加在该公共祖先 上,这样布局引擎才能同时约束到两个子视图。 - 其他情况(如只与 superview 某边对齐):通常将约束添加在 firstViewAttribute.view.superview 上。
伪代码(installedView 的选取逻辑) [[19]]:
if (self.secondViewAttribute.view != nil) {
installedView = [firstView mas_closestCommonSuperview:secondView];
NSAssert(installedView, @"couldn't find a common superview for %@ and %@", firstView, secondView);
} else if (firstViewAttribute.isSizeAttribute) {
installedView = firstViewAttribute.view;
} else {
installedView = firstViewAttribute.view.superview;
}
// 最后将创建的 NSLayoutConstraint 添加到 installedView,并记录到 mas_installedConstraints
update 与 add:若是更新已有约束(updateExisting = YES),会先查找已安装的约束中匹配的那条,只修改其 constant(或 multiplier/priority 等),不新增;否则创建新的 NSLayoutConstraint 并 add 到 installedView,同时记录到视图的 mas_installedConstraints 以便后续 update/uninstall 使用。
5. 关键实现技巧
5.1 包装标量与结构体:mas_equalTo 与 MASBoxValue
系统 API 的 equalTo: 等往往需要 id 类型;而开发中常需传入 CGFloat、CGSize、CGPoint 等。Masonry 通过 mas_equalTo(...) 宏将标量/结构体装箱为 NSValue/NSNumber,再交给内部 equalTo:。
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
MASBoxValue 利用 @encode(__typeof__(value)) 获取类型编码,再根据类型将 C 标量或结构体包装为 NSNumber/NSValue,从而统一走 id 接口。这样即可写出:
make.size.mas_equalTo(CGSizeMake(100, 100));
make.center.mas_equalTo(CGPointZero);
5.2 Block 与循环引用
Masonry 的 Block 会捕获外部变量(如 self、otherView),但 Block 本身并未被 self 长期持有:仅在 mas_makeConstraints: 执行期间调用一次 block(maker),执行完毕即结束,因此不会形成 self → Block → self 的循环引用 [[13]]。
// 源码中仅是 block(constraintMaker),没有被 self 持有
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
5.3 抽象方法小技巧:MASMethodNotImplemented
基类中“必须由子类实现”的方法,若直接空实现容易导致静默错误。Masonry 使用宏在未重写时抛异常,明确约定子类必须重写:
#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]
三、设计模式与延伸
| 模式/技巧 | 在 Masonry 中的体现 |
|---|---|
| 组合模式 | MASConstraint 协议 + MASViewConstraint(叶子)+ MASCompositeConstraint(组合),形成约束树。详见 §2。 |
| 工厂思想 | Maker 根据属性(left/edges/…)创建对应约束对象,调用方不直接 new;角色映射、单属性/复合创建流程见 §3.1;与简单工厂对比见 §3.1.4。 |
| 链式/流式接口 | Block 属性 getter 返回“带返回值的 Block”,Block 内 return self,形成链式调用;三要素、多属性链式与自实现模板见 §3.6。 |
| 装箱(BoxValue) | 标量/结构体通过 @encode 与 va_arg 统一装箱为 id,供 equalTo 使用。 |
| 抽象方法 | MASMethodNotImplemented 宏在基类中抛异常,强制子类重写。 |
提炼与串联:上述模式与思想在 Masonry 中的协作关系、伪代码模板及“按目标选模式”的清单,见 §五、编程思想与设计模式提炼总结(思维导图、流程图、可复用伪代码)。
四、Masonry 中的优秀编程思想
Masonry 在 API 设计与源码实现中体现了一系列可复用的编程思想,理解这些思想有助于在业务代码或自研 DSL 中借鉴其设计。
1. 流式接口(Fluent Interface):把复杂留给自己,把简单留给调用方
思想:每次调用返回“可继续操作的对象”,使多步操作在调用方看来像一句连贯的“句子”,读起来接近自然语言,写起来不易漏参数、不易顺序错。
在 Masonry 中的体现:make.left.equalTo(superview).offset(20).priorityHigh() 中,每一步都返回 MASConstraint(或 self),从而可以持续链下去。链式语法的三要素、完整调用链与多属性链式(如 make.top.left)的委托机制详见 §3.6 链式语法完整解析。
代码案例:自实现简易链式 API(思想与 Masonry 一致)
// 思想:getter 返回 Block,Block 内完成“设置 + 返回 self”,调用方即可继续链
@interface MyConstraint : NSObject
@property (nonatomic, assign) CGFloat constant;
- (MyConstraint * (^)(CGFloat))offset;
@end
@implementation MyConstraint
- (MyConstraint * (^)(CGFloat))offset {
return ^MyConstraint *(CGFloat value) {
self.constant = value;
return self; // 返回自身,支持 .priority(...) 等后续调用
};
}
@end
// 使用方式与 Masonry 一致:make.left.equalTo(sv).offset(20).priority(High);
2. 领域特定语言(DSL):用“业务语言”描述约束
思想:不暴露底层概念(如 NSLayoutAttribute、multiplier、constant),而是提供贴近“布局意图”的词汇(left、equalTo、offset),让代码即文档。
在 Masonry 中的体现:开发者写的是“左边等于某视图”“偏移 20”“优先级高”,而不是“item1.attributeLeft relation item2.attributeLeft multiplier 1 constant 20”。
代码案例:Masonry 写法 vs 系统写法
// 系统 API:意图被冗长参数淹没
[NSLayoutConstraint constraintWithItem:subview
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:20];
// Masonry DSL:意图一目了然
[subview mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(superview).offset(20);
}];
3. 组合模式统一接口:单条与复合用同一套 API
思想:调用方不区分“单条约束”还是“多条约束的集合”,都通过同一类型(MASConstraint)操作;复合约束(如 edges)在内部展开为多条,但对外呈现一致。
在 Masonry 中的体现:make.left 返回 MASConstraint,make.edges 也返回 MASConstraint(实为 MASCompositeConstraint),都可继续 .equalTo(...).offset(...)。组合模式在 Masonry 中的角色与树状结构见 二、2. 组合模式与约束树;可复用伪代码见 五、5.3 伪代码 ①。
4. 延迟执行与两阶段处理:先描述,再安装
思想:Block 执行阶段只“收集意图”,不立刻产生副作用(不立刻 addConstraint);等 Block 结束后再统一 install。这样便于做约束去重、批量激活、与系统 API 的对接。
在 Masonry 中的体现:block(maker) 时只往 Maker 内部数组追加 MASConstraint;[maker install] 时才创建 NSLayoutConstraint 并激活。
代码案例:两阶段伪代码
// 阶段一:描述(无副作用)
- (NSArray *)mas_makeConstraints:(void (^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *maker = [[MASConstraintMaker alloc] initWithView:self];
block(maker); // 仅填充 maker 的约束数组,未修改视图层级
return [maker install]; // 阶段二:统一安装
}
5. 装箱与类型擦除:统一标量与对象入口
思想:系统 API 往往只接受 id(对象),而业务中大量使用 CGFloat、CGSize、CGPoint 等值类型。通过“装箱”把值类型包成对象,对外提供统一接口(如 mas_equalTo),内部再根据类型解码。
在 Masonry 中的体现:mas_equalTo(100)、mas_equalTo(CGSizeMake(80, 80)) 通过 MASBoxValue 转为 NSNumber/NSValue,再走 equalTo:。
代码案例:MASBoxValue 思想简化版
// 宏:任意类型都先装箱再交给 equalTo
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
// 使用:调用方无需区分“传对象”还是“传标量”
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(100, 100)); // 结构体
make.width.mas_equalTo(200); // 标量
make.center.equalTo(otherView); // 对象
}];
6. 抽象基类与“必须重写”的明确约定
思想:基类定义模板方法,子类必须实现某一步;若子类未实现就调用,应立刻失败并给出清晰原因,而不是静默错误或未定义行为。
在 Masonry 中的体现:MASConstraint 的抽象方法用 MASMethodNotImplemented 宏,在未重写时抛异常并指明“必须在子类中重写 xxx”。
代码案例:自实现基类中的“必须重写”
#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]
@interface MASAbstractConstraint : NSObject
- (void)install; // 子类实现
@end
@implementation MASAbstractConstraint
- (void)install {
MASMethodNotImplemented(); // 若子类未重写,调用此处即崩溃并提示
}
@end
7. 编程思想小结(可复用清单)
| 思想 | 核心要点 | 可复用于 |
|---|---|---|
| 流式接口 | 每步返回 self/可链对象,形成连贯调用 | 构建器、配置 API、链式校验 |
| DSL | 用领域词汇封装底层概念,代码即文档 | 配置、查询、布局、路由 |
| 组合统一接口 | 单元素与集合同一类型,透明展开 | 树形结构、批量操作 |
| 两阶段 | 先收集描述再统一执行,便于优化与扩展 | 批量网络请求、事务、布局 |
| 装箱/类型擦除 | 值类型统一为对象接口,内部再解码 | 跨类型容器、序列化、API 兼容 |
| 抽象方法显式失败 | 未重写时抛异常并说明,避免静默错误 | 模板方法、插件、子类契约 |
五、编程思想与设计模式提炼总结
本节对 Masonry 中使用的编程思想与设计模式做统一提炼:用思维导图总览、用流程图串联协作关系、用伪代码与模板固化“可迁移”的写法,便于在其它 DSL、配置类 API 或自研框架中复用。
5.1 思维导图:Masonry 编程思想与设计模式总览
mindmap
root((Masonry 思想与模式))
设计模式
组合模式
Component: MASConstraint 协议
Leaf: MASViewConstraint
Composite: MASCompositeConstraint
统一接口 单条与复合一致
工厂思想
Creator: MASConstraintMaker
Product: MASConstraint
工厂方法: addConstraintWithLayoutAttribute
按需创建 调用方不 new
建造者思想
两阶段: 描述 → install
链式配置 最后统一构建
编程思想
流式接口
每步 return self
Block 返回自身 形成链
领域特定语言 DSL
业务词汇 隐藏底层概念
left equalTo offset
两阶段处理
阶段一 收集描述
阶段二 统一安装
装箱与类型擦除
mas_equalTo MASBoxValue
标量/结构体 → id
抽象方法显式失败
MASMethodNotImplemented
未重写即抛异常
协作关系
入口: mas_makeConstraints
Maker 工厂 生产 Constraint
Constraint 链式 配置 再 install
5.2 流程图:从 API 调用到约束生效(模式协作)
下图展示“一次完整布局”中,各模式与思想如何串联:入口 → 工厂创建 → 链式配置 → 两阶段 install → 组合展开 → 系统约束。
flowchart TB
subgraph 入口与两阶段
A[开发者 mas_makeConstraints block]
A --> B[阶段一: block maker]
B --> C[阶段二: maker install]
end
subgraph 工厂与产品
B --> D[Maker 工厂]
D --> E{请求属性?}
E -->|单属性 left/width| F[创建 MASViewConstraint]
E -->|复合 edges/size| G[创建 MASCompositeConstraint]
F --> H[返回 MASConstraint]
G --> H
end
subgraph 链式与组合
H --> I[链式 equalTo offset priority]
I --> J[每步 return self]
J --> C
C --> K[遍历 constraints]
K --> L{当前项类型?}
L -->|Leaf| M[单条 install → NSLayoutConstraint]
L -->|Composite| N[递归子约束 逐一 install]
N --> M
end
subgraph 系统层
M --> O[添加到公共祖先 / view]
O --> P[Auto Layout 引擎]
P --> Q[布局生效]
end
提炼要点:
- 两阶段:描述(block)与执行(install)分离,便于批量、去重、与系统 API 对接。
- 工厂:Maker 根据“请求”生产单条或组合约束,调用方只依赖
MASConstraint。 - 链式:配置过程每步返回 self,形成一句“句子”。
- 组合:install 时对 Leaf 与 Composite 统一调用
install,Composite 内部递归子约束。
5.3 设计模式与编程思想提炼表(含伪代码)
下表将每种模式/思想抽象为:解决的问题、核心做法、Masonry 对应、可复用伪代码、适用场景,便于直接迁移到其它项目。
| 模式/思想 | 解决的问题 | 核心做法 | Masonry 对应 | 伪代码骨架 | 适用场景 |
|---|---|---|---|---|---|
| 组合模式 | 单条与集合使用方式不一致 | 定义统一 Component 接口,Leaf 与 Composite 都实现;Composite 持有子节点,操作时递归 | MASConstraint / MASViewConstraint / MASCompositeConstraint | 见下文伪代码 ① | 树形结构、批量操作、配置项分组 |
| 工厂思想 | 调用方与具体产品类耦合 | 由“工厂”根据请求创建具体产品,调用方只依赖抽象产品 | Maker + addConstraintWithLayoutAttribute | 见下文伪代码 ② | 多种产品、按参数/类型创建、隐藏构造细节 |
| 流式接口 | 多步配置冗长、易漏参数 | 每步方法返回 self(或可链对象),形成链式调用 | equalTo / offset / priority 均 return self | 见下文伪代码 ③ | 构建器、配置 API、校验链、DSL |
| 两阶段处理 | 边描述边执行难以优化、易产生重复副作用 | 阶段一仅收集描述(不执行),阶段二统一执行 | block(maker) 只填充数组;install 时再创建并添加 | 见下文伪代码 ④ | 批量请求、事务、布局、表单校验 |
| DSL | 底层概念暴露、意图不直观 | 用领域词汇封装底层 API,让“写什么像什么” | left、equalTo、offset、edges | 见下文伪代码 ⑤ | 配置、查询、布局、路由、规则引擎 |
| 装箱/类型擦除 | 系统 API 只接受 id,业务多用值类型 | 将标量/结构体装箱为对象,统一入口,内部再解码 | mas_equalTo、MASBoxValue | 见下文伪代码 ⑥ | 跨类型容器、序列化、多态参数 |
| 抽象方法显式失败 | 子类未重写导致静默错误 | 基类“必须重写”的方法内抛异常并说明 | MASMethodNotImplemented | 见下文伪代码 ⑦ | 模板方法、插件接口、子类契约 |
伪代码 ① 组合模式:
protocol Component { func install() }
class Leaf: Component { func install() { /* 执行单条逻辑 */ } }
class Composite: Component {
var children: [Component]
func install() { children.forEach { $0.install() } }
}
// 调用方:component.install(),不关心是 Leaf 还是 Composite
伪代码 ② 工厂思想:
class Maker {
func left() -> Product { return create(.left) }
func edges() -> Product { return composite([.left, .right, .top, .bottom]) }
private func create(_ attr: Attribute) -> Product {
let p = ConcreteProduct(attr)
constraints.append(p)
return p
}
}
// 调用方:let c = maker.left(); 不 new ConcreteProduct
伪代码 ③ 流式接口:
func offset(_ value: T) -> Self {
self.value = value
return self
}
func priority(_ p: P) -> Self {
self.priority = p
return self
}
// 调用:obj.offset(20).priority(high)
伪代码 ④ 两阶段处理:
func make(block: (Maker) -> Void) -> Result {
let maker = Maker()
block(maker) // 阶段一:只填充 maker 内部结构
return maker.build() // 阶段二:统一执行、产生副作用
}
伪代码 ⑤ DSL 封装:
// 底层:setAttribute(Left, relation: Equal, to: view, attribute: Left, constant: 20)
// DSL:make.left.equalTo(view).offset(20)
// 实现:left 返回约束描述对象,equalTo 设目标,offset 设 constant,均 return self
伪代码 ⑥ 装箱:
func box(_ value: Any) -> Id {
if value is CGFloat { return NSNumber(value) }
if value is CGSize { return NSValue(value) }
// ...
}
func equalTo(_ id: Id) { /* 内部根据类型解码 */ }
伪代码 ⑦ 抽象方法显式失败:
func mustOverride() {
throw Exception("You must override \(method) in a subclass.")
}
// 基类中:func install() { mustOverride() }
5.4 流程图:六大思想在“一句话布局”中的分工
以一句 make.left.equalTo(superview).offset(20) 为例,下图标出每一步对应的思想或模式,便于记忆与迁移。
flowchart LR
A[make] --> B[left]
B --> C[equalTo]
C --> D[offset]
D --> E[install]
subgraph 对应思想
A1[两阶段入口]
B1[工厂: 按 left 创建约束]
C1[DSL: 业务语汇]
D1[流式: return self]
E1[两阶段: 统一 install]
end
A -.-> A1
B -.-> B1
C -.-> C1
D -.-> D1
E -.-> E1
5.5 可复用设计清单(按“想实现什么”选模式)
若要在业务中实现类似 Masonry 的体验,可按目标选择对应模式与伪代码模板。
| 目标 | 推荐模式/思想 | 参考伪代码 |
|---|---|---|
| 让“单条”与“一组”用同一套 API | 组合模式 | §5.3 伪代码 ① |
| 根据“请求类型”创建不同对象,调用方不 new | 工厂思想 | §5.3 伪代码 ② |
| 多步配置写成一句链式调用 | 流式接口 | §5.3 伪代码 ③ |
| 先收集再统一执行(批量、事务、布局) | 两阶段处理 | §5.3 伪代码 ④ |
| 用业务词汇隐藏底层 API | DSL | §5.3 伪代码 ⑤ |
| 值类型与对象统一入口 | 装箱/类型擦除 | §5.3 伪代码 ⑥ |
| 基类要求子类必须实现某方法 | 抽象方法显式失败 | §5.3 伪代码 ⑦ |
5.6 小结:提炼后的编程思想一句话
- 组合:单条与复合同一接口,操作时递归子节点。
- 工厂:谁要谁造,调用方只拿抽象产品。
- 流式:每步 return self,链成一句“话”。
- 两阶段:先描述后执行,便于优化与扩展。
- DSL:用领域词汇说话,代码即文档。
- 装箱:值类型进“盒子”,统一走对象接口。
- 显式失败:该子类实现的没实现,立刻报错不隐瞒。
上述思想与模式在 Masonry 中同时存在、相互配合:入口用两阶段,Maker 用工厂,约束用流式与组合,标量用装箱,基类用显式失败。理解并提炼后,可在任意“配置型、构建型、DSL 型”的 API 设计中按需复用。
参考文献
[1] SnapKit. Masonry. GitHub. github.com/SnapKit/Mas…
[2] SnapKit. SnapKit. GitHub. github.com/SnapKit/Sna…
[3] Apple. Auto Layout Guide. Developer Documentation.
[4] Sarunw. History of Auto Layout constraints. sarunw.com/posts/histo…
[5] Wikipedia. Cassowary (software). en.wikipedia.org/wiki/Cassow…
[6] Larder. What's in your Larder: iOS layout DSLs. larder.io/blog/larder…
[7] Cassowary. Solving constraint systems. cassowary.readthedocs.io/en/latest/t…
[8] University of Washington. Cassowary Constraint Solving Toolkit. constraints.cs.washington.edu/cassowary/
[9] Badros, G. J., Borning, A., & Marriott, K. (1997). Solving Linear Arithmetic Constraints for User Interface Applications. Proceedings of the 1997 ACM Symposium on User Interface Software and Technology (UIST).
[10] University of Washington. Cassowary TOCHI. constraints.cs.washington.edu/solvers/cas…
[11] 设计模式:组合模式(Composite Pattern). Runoob. www.runoob.com/design-patt…
[12] 设计模式:工厂方法. Runoob. www.runoob.com/design-patt…
[13] 读 SnapKit 和 Masonry 自动布局框架源码. 戴铭. ming1016.github.io/2018/04/07/…
[14] Masonry:iOS AutoLayout的革命性简化框架. CSDN. blog.csdn.net/gitblog_005…
[15] 源码解读——Masonry. 楚权的世界. chuquan.me/2019/10/02/…
[16] iOS中Masonry的使用总结. 星星的博客. smileasy.github.io/2019/04/01/…
[17] iOS自动布局框架之Masonry. 腾讯云开发者社区. cloud.tencent.com/developer/a…
[18] 浅析Masonry. HelloBit. www.hellobit.com.cn/doc/2020/6/…
[19] Mcyboy. Masonry实现原理并没有那么可怕. 掘金. juejin.cn/post/684490…
[20] 掘金. Masonry 相关文章. juejin.cn/post/684490…
延伸阅读
- SnapKit:Masonry 的 Swift 继任者,本系列《04-SnapKit框架:从使用到源码解析》可对照学习。
- Auto Layout 内在尺寸:Content Hugging 与 Compression Resistance 在 Apple《Auto Layout Guide》中的说明。
- Cassowary 论文:深入理解约束层次与增量求解,便于分析复杂布局冲突与性能。
- iOS 设计模式 Swift 实现(组合模式、工厂模式):可参考开源仓库如 iOS_Design_Patterns_Swift 等。
- Masonry 官方源码:github.com/SnapKit/Mas… ,建议结合本文“源码解析”章节对照阅读 MASConstraintMaker、MASViewConstraint、MASCompositeConstraint 等实现。
- 掘金《Masonry实现原理并没有那么可怕》 [[19]]:从 makeConstraints、make(Maker)、install、equalTo 四条线梳理原理,含链式多属性(make.top.left)的委托与复合替换、约束挂载视图(closestCommonSuperview)等,可与本文 §1.3、§4.5 对照阅读。