01-研究优秀开源框架@UI布局@iOS | Masonry 框架:从使用到源码解析

7 阅读55分钟

本文结合科技文献、学术论文与业界实践,系统介绍 iOS/macOS 下的 Auto Layout DSL 库 Masonry:技术演进、核心原理(含 Cassowary 约束求解)、应用场景、源码架构与设计模式,并配有流程图、泳道图与思维导图。内容涵盖库的源码剖析及大厂使用心得,从基础概念到高级应用形成完整知识体系。


目录


一、Masonry 使用详解

1. 框架概述

Masonry 是面向 Objective-CAuto 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 FormatV:|[a]-[b]|字符串描述,类型不安全,复杂布局难表达。
MasonryObjective-C,Block 链式链式 DSL、可读性高,成为 OC 时代事实标准 [[1]]。
SnapKitSwift,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):支持 requiredpreferred(优先级),在约束冲突时按优先级舍弃或松弛部分约束,避免无解。

约束层次与松弛原理(简述):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.leftsuperview.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 - 8Relation 常见为 Equal、LessThanOrEqual、GreaterThanOrEqual,在 Masonry 中对应 equalTolessThanOrEqualTogreaterThanOrEqualTo。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_remakeConstraintsmas_updateConstraints 配合动画更新约束。
列表 CellprepareForReuse 中避免重复添加约束,可 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 常见实践场景(来自社区与项目总结)

  • 相对父视图edgescentersize 配合 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 源码解析

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.leftmake.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 1make.left 调用 addConstraintWithLayoutAttribute:NSLayoutAttributeLeft
  • Step 2addConstraintWithLayoutAttribute: 内部调 constraint:nil addConstraintWithLayoutAttribute:layoutAttribute(单属性时第一个参数为 nil)。
  • Step 3constraint: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;若传入单个对象,则设置 secondViewAttributereturn 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 协议,树中所有节点(叶子与组合)都实现该协议。
LeafMASViewConstraint:无子约束,对应单条 NSLayoutConstraint。
CompositeMASCompositeConstraint:持有多个 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…)在同一类内部分支,创建 MASViewConstraintMASCompositeConstraint,并统一以 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: 及复合属性的封装(如 edgessize)。

Masonry 并未采用“抽象 Creator + 多个 ConcreteCreator 子类”的经典工厂方法结构,而是在一个 Maker 类内根据请求的布局属性(left、top、edges、size 等)决定创建“单条约束”还是“组合约束”,因此更贴近简单工厂 + 工厂方法思想的融合:创建逻辑集中在 Maker 内部,对外只暴露 make.leftmake.edges 等统一入口,调用方完全依赖 MASConstraint 抽象,不关心具体是 MASViewConstraint 还是 MASCompositeConstraint

3.1.2 Masonry 中的“工厂”是谁、生产什么
角色Masonry 中的对应说明
工厂 / 创建者MASConstraintMakerBlock 中的 make,持有 view 和约束数组;根据访问的属性创建约束。
工厂方法addConstraintWithLayoutAttribute:constraint:addConstraintWithLayoutAttribute:根据 NSLayoutAttribute(Left、Top、Width、Height…)或复合键(edges、size、center)创建并返回 MASConstraint
抽象产品MASConstraint 协议对外统一接口:equalTooffsetpriorityinstall 等,调用方只依赖该协议。
具体产品(单条)MASViewConstraint对应一条 NSLayoutConstraint,如 make.leftmake.width
具体产品(复合)MASCompositeConstraint内部持有多条 MASViewConstraint,如 make.edgesmake.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)**到了子类中。

核心解决的问题: 它解决了客户端代码与具体产品类之间的耦合问题。当系统在编译时无法确定需要创建哪个具体类的对象,或者希望将具体类的实例化逻辑封装在子类中时,该模式尤为适用。

设计优势

  1. 符合开闭原则(Open-Closed Principle):系统对扩展开放,对修改关闭。当需要引入新的具体产品时,只需创建一个新的具体工厂子类,而无需修改现有的客户端代码或工厂接口
  2. 统一接口编程:客户端仅依赖于产品的抽象接口(或抽象基类),而不依赖具体实现。这确保了无论工厂返回哪种具体产品,客户端都能以一致的方式处理。

结论: 相比于在客户端直接使用 new 关键字硬编码具体类,工厂方法模式提供了一种更灵活、更易维护的对象创建策略,特别适用于框架开发或产品族经常变化的场景。

工厂方法模式通过将实例化逻辑推迟到子类,实现创建者与使用者的解耦。要点如下:

3.3 ✅ 主要优点

  • 开闭原则:新增产品时只需新增具体工厂子类与产品子类,无需修改现有客户端与抽象接口。
  • 单一职责:创建逻辑与业务逻辑分离,客户端只关心“用产品”,不关心“如何造”。
  • 低耦合:客户端依赖抽象 Creator 与 Product,便于替换具体实现(如切换数据库驱动)。
  • 统一入口:所有创建经工厂方法,便于做日志、权限、缓存等集中控制。

3.4 ❌ 主要缺点

  • 类数量增加:每增加一种产品通常需增加一个具体工厂类,产品线大时易产生“类爆炸”。
  • 抽象层次加深:调用链变长(客户端 → 具体工厂 → 抽象工厂 → 具体产品),理解成本上升。
  • 多参数/多产品族:若需根据多参数动态选产品,或需一次创建一族产品,更适合用抽象工厂或建造者。

3.5 ⚖️ 总结与适用场景建议

维度评价
灵活性⭐⭐⭐⭐⭐ (极高,易于扩展新产品)
可维护性⭐⭐⭐⭐ (高,职责分离清晰)
复杂度⭐⭐ (较低,类数量随产品线性增长)
性能开销⭐⭐⭐ (中等,主要是类加载开销,运行时影响小)
3.5.1 💡 什么时候应该使用?
  1. 当你不知道确切需要哪个具体类的对象时:例如,框架开发中,框架本身不知道用户会具体使用哪种控件,由用户子类化框架来指定。
  2. 当你希望将对象的创建逻辑委托给专门的子类时:不同子类可能需要不同的初始化逻辑或上下文环境。
  3. 当系统需要遵循开闭原则,频繁增加新产品时:这是最典型的场景。
3.5.2 💡 什么时候应该使用?
  1. 产品种类非常固定,且几乎不会变化:此时引入工厂模式是过度设计(Over-engineering),直接 new 更简单。
  2. 一个工厂需要负责创建多种差异巨大的产品:此时可能更适合使用抽象工厂模式(Abstract Factory)或建造者模式(Builder)。
  3. 项目规模很小,追求极致的代码简洁性:简单的脚本或小型工具类应用中,工厂模式带来的类膨胀可能弊大于利。
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];
}

参考资料

读 SnapKit 和 Masonry 自动布局框架源码

iOS开发之Masonry框架源码解析

Masonry 源码解读

Masonry源码解析

链式语法使“多步配置”可以写成一行连贯的调用,如 make.left.equalTo(superview).offset(20).priorityHigh(),读起来接近自然语言。下面从构成要素、实现原理、与 Builder 的关系、多属性链式四方面展开。

3.6.1 链式语法的三要素
要素说明在 Masonry 中的体现
统一返回类型每一步方法返回的类型与“可继续调用的对象”一致,通常是 self 或协议类型。equalTooffsetpriority 等均返回 MASConstraint *(或 id<MASConstraint>),调用方可持续 .xxx
返回 self 或当前对象方法内部完成“设置”后,返回当前对象本身,而不是 void 或无关类型。offset(CGFloat) 内部设置 layoutConstant,然后 return selfequalTo(id) 设置 secondViewAttributereturn self
可选的 Block 封装若参数需要延迟求值或复杂逻辑,可用 Block 作为 getter 的返回值,Block 内再 return self。offsetmultipliedBy 等用“返回 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:前面步骤(leftequalTooffsetpriority)都是配置,最终“产出”发生在 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(单条约束描述);之后的 equalTooffsetpriorityHigh 都是这条 MASViewConstraint 的方法,每次返回 self,形成链。

3.6.4 多属性链式(make.top.left)与委托机制

当写成 make.top.left 时,表示“两条独立约束”:top 一条、left 一条。流程是:

  1. make.top:Maker 创建一条 MASViewConstraint(top),加入 constraints 数组,返回这条 MASViewConstraint
  2. 调用方继续 .left:此时是 MASViewConstraint 的 .left 被调用(因为 MASConstraint 协议也声明了 left、right、top 等属性)。
  3. 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 会捕获外部变量(如 selfotherView),但 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 伪代码 ④
用业务词汇隐藏底层 APIDSL§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 对照阅读。