纯 frame 布局的年代,改一个间距要翻三个方法,加一个控件要重算一整屏坐标;到了约束时代,NSLayoutConstraint那一长串constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:又让人怀疑人生。Masonry 框架就是在这种撕裂感里被大量 Objective-C 工程选中的:它把 Auto Layout 的约束声明包装成一段可读的链式调用,make.left.equalTo(view).offset(16)这种写法,几乎不用查文档就能看懂意图。这个库解决的核心问题很具体——让约束式布局的代码写起来像句子,改起来像改数字。适合谁看?正在维护 Objective-C 老工程、被NSLayoutConstraint折磨过的开发者;从 frame 时代过渡过来、想系统搞懂约束优先级和更新机制的中间层同学;以及准备自研一套布局 DSL、想先弄清成熟方案怎么设计的人。往下我会拆它的设计思路、内部机制、实操代码、复杂场景处理,以及那些文档里不会写的冲突排查经验。
1. 布局这件事为什么值得单独单独聊:Masonry 的定位与思路
1.1 从手写 frame 到约束式布局的演进代价
手写 frame 的时代,布局代码本质是一堆坐标算术。父视图宽度变了、字体变了、文案从两行变三行,所有下游的CGRect都得跟着重算,layoutSubviews里塞满减法和除法,稍微复杂一点的卡片列表,一个文件里能出现十几个魔法数字。它的优点是快——没有约束求解器,纯 CPU 算术;缺点是脆——需求一动,整块代码推倒重来。
Auto Layout 把"位置"从"结果"变成了"关系",你声明的是控件之间的相对关系,求解器负责算出最终坐标。这是思维方式的迁移,代价是声明成本陡增。一个宽度等于父视图一半、居中、距离顶部 12 点的视图,原生写法要三行NSLayoutConstraint,还得手动设置translatesAutoresizingMaskIntoConstraints = NO。四条边加一个尺寸,代码量直接翻倍,而且几乎不可读——你没法一眼看出这段代码描述的是什么形状。
Masonry 站在中间。它没有绕开 Auto Layout,也没有自己实现一套布局引擎,只是把约束声明这件事的书写成本压到了接近自然语言的程度。这个定位很关键:它解决的是表达效率问题,不是性能问题。理解这一点,后面所有关于性能、关于要不要换方案的讨论才有基准。
1.2 链式 DSL 到底省掉了什么
把两种写法并排放,差异一目了然。原生方案里,每个约束都是独立对象,firstItem、firstAttribute、relation、secondItem、secondAttribute、multiplier、constant七个参数按位置传,写错了编译器未必报错,运行时才崩。Masonry 把"设置谁"、"什么关系"、"相对谁"、"差多少"拆成四段连续的调用:
// 原生 NSLayoutConstraint *c = [NSLayoutConstraint constraintWithItem:self.card attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:16]; [self.view addConstraint:c]; // Masonry [self.card mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.view).offset(16); }];省掉的不只是行数。链式调用的每一段都是一个语义单元,make.left说明改的是哪条边,.equalTo(self.view)说明参照物,.offset(16)说明偏移量。中间任何一段想调整,直接改对应的调用就行,不用重新数参数位置。这种"一段一语义"的结构,直接决定了后期维护时定位问题的速度。
还有一个隐性的好处:mas_makeConstraints:的 block 结束时统一安装约束,这给了框架一个批量处理的窗口。同一个 block 里的约束可以互相校验、可以整体设置优先级,甚至可以等到 block 结束再决定装到哪个视图上。原生写法做不到这一点,每条约束都是立即生效的。
1.3 同名不同物:先分清你说的是哪个 Masonry
这里必须插一句,因为搜索"Masonry"会撞出两个完全不同的东西。一个是 Objective-C 生态里的 Auto Layout 封装库,跑在苹果设备上;另一个是前端的瀑布流布局库,跑在浏览器里,通过绝对定位把不等高的卡片塞进列里。两者只是名字相同,解决的问题、技术栈、API 形态毫无关系。
判断方法很简单:看上下文有没有UIView、UIViewController、mas_makeConstraints这些词。如果有,是前者;如果看到的是new Masonry('.grid', {itemSelector: '.item'})这种 JS 调用,那就是后者。这篇聊的是前者,也就是移动端约束布局那一套。为什么要专门提这一嘴?因为我在技术社区里见过太多次提问者把两边的资料混着看,然后困惑于"为什么我的 Masonry 没有 offset 方法"。
1.4 影响范围:什么项目适合用它
先说结论:Objective-C 为主的、界面复杂度中等偏上的、需要长期维护的工程,收益最大。纯 Swift 工程没必要为了它桥接一层,Swift 生态里有同类思路的链式库,用起来更顺手;界面极其简单的工具类应用,手写 frame 反而更省事,约束求解的开销和调试成本不划算;而对帧率极致敏感的滚动列表,约束布局本身就要谨慎使用,这跟用不用 Masonry 没关系。
还有一类容易被忽略的场景:老工程渐进式改造。一个纯 frame 的历史项目不可能推倒重来,但新页面可以逐步引入约束布局。Masonry 在这里的优势是侵入性低——它不改动 UIKit 的任何行为,只是给UIView加了一批 category 方法,没有全局 hook,没有 method swizzling(除了极少量的辅助逻辑),新旧代码可以共存于同一个界面。这种"可以局部采用"的特性,在真实工程里的价值往往比 API 优雅更高。
2. 内部机制拆解:一条链式语句怎么变成约束
2.1 三个核心对象与调用时序
搞懂 Masonry 只需要抓住三个角色:MASConstraintMaker、MASConstraint(实际装配时是MASViewConstraint)、以及UIView上的一批 category 属性。
你写下[view mas_makeConstraints:^(MASConstraintMaker *make) {...}]的时候,框架先创建一个 maker,把当前 view 塞进去。block 里每一次make.left、make.width这样的调用,本质是在 maker 内部创建一条约束的描述对象,并把它追加到一个数组里。注意,这时候约束还没真正生效——数组只是在收集。
block 执行完,maker 调用install,遍历数组,把每条描述翻译成真正的NSLayoutConstraint,找到两个视图最近的公共父视图,addConstraint:上去。整个过程是"先收集、后装配",这就是为什么你在 block 里可以随意引用其他视图的mas_left,框架会等到装配阶段才去解析。
UIView那一侧的 category 属性同样值得说明。view.mas_left、view.mas_centerY这些不是存储属性,是每次访问时现算出来的MASViewAttribute对象,内部只记录"哪个视图的哪个NSLayoutAttribute"。所以它很轻,不需要缓存,也不会有生命周期问题。理解这一点,你就不会纠结"为什么我不能把mas_left存起来复用"——能存,但没必要。
2.2 equalTo 与 mas_equalTo 的装箱差异
这是新手最容易混的一对 API,也是我见过最多"约束没报错但布局不对"的源头。
equalTo接收的是id,框架会根据传入对象的类型做不同处理:传视图,取它的对应属性;传NSNumber,当作常量。但如果你直接传一个CGFloat,比如.equalTo(16.0),16.0是基本类型,装箱成NSNumber的过程在id参数上会出问题——有些场景下能过,有些场景下值会丢。
mas_equalTo是一个宏,内部调用了装箱函数,专门处理CGFloat、NSInteger、CGPoint、CGSize、UIEdgeInsets这些值类型。它会先把值包成NSValue或NSNumber,再交给equalTo处理。
我的习惯很简单,也推荐你照做:凡是传数值、尺寸、内边距的,一律用mas_equalTo;凡是传视图或MASViewAttribute的,用equalTo。这条规则不用记原理,照着执行就不会踩坑。
make.width.mas_equalTo(120); // 数值 make.height.mas_equalTo(CGSizeMake(100, 44).height); // 表达式 make.left.equalTo(self.view); // 视图 make.right.equalTo(otherView.mas_left).offset(-8); // 属性 make.edges.equalTo(self.view).insets(UIEdgeInsetsMake(8, 8, 8, 8));顺带说个细节:make.edges.equalTo(self.view).insets(...)这一句等价于四条边各写一遍。insets的值是分别作用到 top、left、bottom、right 上的,UIEdgeInsetsMake(8, 8, 8, 8)就是四周各缩进 8。这个复合写法在容器视图内边距的场景里特别省事。
2.3 make、update、remake 的语义边界
这三个方法长得像,行为差别很大,用错了会出现"约束越积越多"或者"改了没生效"两类典型问题。
mas_makeConstraints:是新增。对同一个视图反复调用它,约束会叠加,而不是替换。两条width == 100和width == 120同时存在,就是冲突,日志里会看到求解器随机断掉一条,表现是布局时对时错。
mas_updateConstraints:是更新。框架会遍历该视图上已有的约束,按 attribute 匹配,找到对应的那条,只改constant。它不会新增约束,也不会删除约束。所以它只能改"已存在的那条约束的数值",改不了关系,也没法把一个lessThanOrEqualTo变成equalTo。
mas_remakeConstraints:是先清空再重建。所有该视图上由 Masonry 创建的约束先移除,再按新 block 重新装配。它最彻底,也最费——每条约束都要重新计算、重新装载。
选择的判断标准很清晰:运行时只改数值(比如高度随内容变化、间距随状态切换),用update;布局结构本身变了(比如横竖屏切换、控件增减),用remake;初始化第一次,用make。
注意:
mas_updateConstraints匹配的是 attribute,不是你以为的"那条约束"。同一个视图如果有两条都关于height的约束(比如一个有优先级、一个没有),更新时匹配到哪一条是有歧义的。这种场景老老实实用remake,或者给约束加key做精确查找。
3. 落地实操:把 Masonry 接进工程并跑通第一个界面
3.1 依赖引入与编译配置
主流方式还是依赖管理工具。Podfile 里加一行:
platform :ios, '11.0' target 'DemoApp' do pod 'Masonry', '~> 1.1.0' end版本号建议锁小版本,~> 1.1.0表示允许 1.1.x,不允许跳到 1.2。布局库这种底层依赖,一旦大版本升级改了内部实现,排查成本极高,锁住更稳妥。
工程里的引入方式有两种。省事的是在全局前缀头文件里加一行#import "Masonry.h",所有文件直接用,不用逐个 import。缺点是编译依赖变重,前缀头改动会触发大面积重编。我个人偏向另一种:用到的文件单独 import,配合模块化改造的工程甚至可以做成私有库的子模块。这点取舍看你工程的编译时间敏不敏感。
Swift 与 Objective-C 混编的工程要注意桥接头。Masonry 是纯 Objective-C 实现,Swift 侧要用必须先通过 bridging header 暴露出来,而且链式 API 在 Swift 里的调用体验会打折扣(mas_makeConstraints的 block 语法转换后不算优雅)。这种工程我一般建议新代码走原生 Anchor,老代码继续用 Masonry,别硬统一。
3.2 第一个可运行的布局:登录页卡片
拿一个真实感强的场景练手:一个居中的登录卡片,宽度是屏幕宽度的 80%,高度自适应内容,内部从上到下依次是标题、两个输入框、一个按钮,间距统一 16。
- (void)setupUI { UIView *card = [[UIView alloc] init]; card.backgroundColor = [UIColor whiteColor]; card.layer.cornerRadius = 12; [self.view addSubview:card]; self.cardView = card; // 卡片本身:水平居中,宽度占屏幕 80%,垂直居中 [card mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.view); make.width.equalTo(self.view).multipliedBy(0.8); }]; UILabel *title = [[UILabel alloc] init]; title.text = @"账号登录"; [card addSubview:title]; self.titleLabel = title; UITextField *account = [[UITextField alloc] init]; account.placeholder = @"手机号"; account.borderStyle = UITextBorderStyleRoundedRect; [card addSubview:account]; UITextField *password = [[UITextField alloc] init]; password.placeholder = @"密码"; password.secureTextEntry = YES; password.borderStyle = UITextBorderStyleRoundedRect; [card addSubview:password]; UIButton *login = [UIButton buttonWithType:UIButtonTypeSystem]; [login setTitle:@"登 录" forState:UIControlStateNormal]; [card addSubview:login]; self.loginButton = login; // 标题 [title mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(card).offset(24); make.left.equalTo(card).offset(20); make.right.equalTo(card).offset(-20); make.height.mas_equalTo(28); }]; // 账号输入框 [account mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(title.mas_bottom).offset(16); make.left.right.equalTo(title); make.height.mas_equalTo(44); }]; // 密码输入框 [password mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(account.mas_bottom).offset(16); make.left.right.equalTo(title); make.height.mas_equalTo(44); }]; // 按钮:顶部对齐密码框底部,底部留 24,卡片高度就由内容链推出来 [login mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(password.mas_bottom).offset(24); make.left.right.equalTo(title); make.height.mas_equalTo(48); make.bottom.equalTo(card).offset(-24); }]; }这段代码值得逐行看的有两处。
一是make.width.equalTo(self.view).multipliedBy(0.8)。这里用的是比例而不是固定值,屏幕宽度变化时卡片自动跟着缩放,不用写任何适配代码。multipliedBy作用在参照视图的对应属性上,最终生成的约束是card.width == view.width * 0.8。
二是最后那条make.bottom.equalTo(card).offset(-24)。这条约束的作用是把卡片的高度"撑"出来——卡片自己没写 height,它的高度由内部的约束链决定:title 顶部 24 + 28 + 16 + 44 + 16 + 44 + 24 + 48 + 24。这条链闭合了,卡片就有了确定高度。这是约束布局里最常用的技巧,后面讲自适应高度还会用到。
3.3 常用写法速查与设计稿换算
实际开发里高频出现的写法就那么十几种,整理成表比翻文档快得多。
| 需求 | 写法 |
|---|---|
| 四边贴父视图 | make.edges.equalTo(superview) |
| 四边带内边距 | make.edges.equalTo(superview).insets(UIEdgeInsetsMake(8, 8, 8, 8)) |
| 水平垂直居中 | make.center.equalTo(superview) |
| 宽度占父视图一半 | make.width.equalTo(superview).multipliedBy(0.5) |
| 宽高比 4:3 | make.height.equalTo(view.mas_width).multipliedBy(0.75) |
| 尺寸固定 | make.size.mas_equalTo(CGSizeMake(100, 44)) |
| 顶对齐 + 左对齐 | make.top.left.equalTo(superview).offset(16) |
| 右对齐带负偏移 | make.right.equalTo(superview).offset(-16) |
| 高于等于某个值 | make.height.mas_greaterThanOrEqualTo(44) |
| 优先级设定 | make.height.mas_equalTo(44).priorityHigh() |
设计稿换算这块,很多团队的做法是写个宏:
#define SCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width) #define MAS_SCALE(w) ((w) * SCREEN_WIDTH / 375.0)用的时候make.height.mas_equalTo(MAS_SCALE(44))。这套方案能用,但我不太推荐大范围使用。原因有两个:一是屏幕宽度变化时所有值线性缩放,会导致在小屏上文字被挤、在大屏上留白过多,视觉比例失真;二是这个宏在横屏、分屏模式下会算出奇怪的值。
更稳的思路是分级处理:间距用固定值加少量断点调整,宽度高度用比例约束,字号用系统动态字体。真正需要等比的场景(比如按设计稿精确还原的营销页),用multipliedBy在约束层面表达比例,比在数值层面做缩放要可靠得多。举个例子,一张宽度占父视图 100%、高度按 16:9 的图片:
[imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.equalTo(self.view); make.height.equalTo(imageView.mas_width).multipliedBy(9.0 / 16.0); }];这条约束在任何屏幕宽度下都成立,不需要计算任何像素值。
4. 复杂场景:等分、滚动视图、自适应高度与动画
4.1 等分布局与间距分配的计算
四个按钮横向等分,是列表页、筛选栏、底部工具栏的标配。手写 frame 的算法是(总宽 - 总间距) / 4,用约束表达则不需要算具体数值,只需要表达"彼此等宽、依次排开、首尾贴边"。
NSMutableArray<UIView *> *items = [NSMutableArray array]; for (NSInteger i = 0; i < 4; i++) { UIView *item = [[UIView alloc] init]; item.backgroundColor = [UIColor lightGrayColor]; [self.containerView addSubview:item]; [items addObject:item]; } [items mas_distributeViewsAlongAxis:MASAxisTypeHorizontal withFixedSpacing:8 leadSpacing:16 tailSpacing:16]; [items mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.containerView).offset(12); make.height.mas_equalTo(60); }];mas_distributeViewsAlongAxis:是 Masonry 提供的一个批量工具方法,一行搞定轴向分布,内部会自动处理首尾间距和中间等距。它的便利性很高,但有个前提:所有视图必须已经加入同一个父视图,而且数量不能为 0,否则会崩。这个检查最好自己加一层,尤其是数组来自网络数据的时候。
如果不想用这个工具方法,手动写循环也完全可行,逻辑是"第一个贴左边,后面的贴前一个的右边,最后一个贴右边":
UIView *lastItem = nil; for (NSInteger i = 0; i < 4; i++) { UIView *item = items[i]; [item mas_makeConstraints:^(MASConstraintMaker *make) { make.top.bottom.equalTo(self.containerView); if (lastItem) { make.left.equalTo(lastItem.mas_right).offset(8); make.width.equalTo(lastItem); } else { make.left.equalTo(self.containerView).offset(16); } }]; lastItem = item; } [lastItem mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.containerView).offset(-16); }];这个循环的关键在于make.width.equalTo(lastItem)。每新增一个视图就把它和上一个绑成等宽,最终所有视图的宽度会被求解器统一成一个值——(容器宽 - 16 - 16 - 8 * 3) / 4。你不用自己算,但你需要理解为什么它是这个值:因为总宽被"第一个的左边距"、"最后的右边距"、以及"中间三个 8 的间距"和"四个等宽的视图"完全瓜分了,约束系统求解的唯一解就是这个。
注意:循环里如果给每个视图都写了
make.right.equalTo(...),会和等宽链条产生冲突。等分布局的核心是"只约束一端,另一端靠链条推导",两边都锚死就会过约束。
4.2 UIScrollView 与自适应高度的正确姿势
滚动视图是约束布局最容易翻车的地方,因为我见过太多人在UIScrollView上直接给子视图写约束,然后发现滚不动。原因在于:滚动视图的内容尺寸不是由它自己决定的,而是由内部的子视图约束推出来的,而子视图的约束必须相对于一个"内容容器",不能相对于滚动视图本身的可视区域。
标准做法是加一层contentView,让它和滚动视图的四边对齐,并固定宽度(竖向滚动场景):
UIScrollView *scrollView = [[UIScrollView alloc] init]; [self.view addSubview:scrollView]; [scrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; UIView *contentView = [[UIView alloc] init]; [scrollView addSubview:contentView]; [contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(scrollView); make.width.equalTo(scrollView); // 竖向滚动:宽度锁死,高度自由 }];make.edges.equalTo(scrollView)这一句把 contentView 的四边和 scrollView 的可视区域绑在一起,宽度那条则保证 contentView 不会被横向撑开。之后所有子视图都往 contentView 上加,最后用最后一个视图的 bottom 约束到 contentView 的mas_bottom,高度链就闭合了,滚动区域自动算出来。
横向滚动的轮播场景反过来:make.height.equalTo(scrollView)锁高度,宽度自由展开。这点别搞混,搞混的表现是"能滚动但是内容被压扁"。
至于UITableViewCell的自适应高度,思路完全一致,只是容器换成了contentView。要点有三个:cell 内部从上到下形成一条完整的垂直约束链,最后一个视图的 bottom 必须约束到cell.contentView.mas_bottom;不要给 cell 设置固定高度;不要在heightForRowAtIndexPath里返回一个具体数值,让它走自动维度。Masonry 在这个场景里最大的价值是"链条清晰",二十行的约束代码能一眼看出上下依赖关系,出了问题容易定位是哪一环断了。
4.3 约束驱动的动画与优先级抢占
约束布局的动画跟 frame 动画思路不同:你不能直接改frame,要改约束的constant,然后在动画块里触发重新布局。
- (void)toggleCardHeight:(BOOL)expanded { [self.cardView mas_updateConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(expanded ? 240 : 120); }]; [UIView animateWithDuration:0.28 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ [self.view layoutIfNeeded]; } completion:nil]; }顺序很重要:先改约束,再进动画块调layoutIfNeeded。如果反过来,你会看到界面"跳"一下就结束,没有过渡——因为约束变化和布局刷新不在同一次提交里。这个坑我踩过不止一次,排查了半小时才意识到是调用顺序的问题。
优先级在动画里也有用处。比如一个展开收起的面板,收起时希望高度优先服从最大值,展开时希望高度服从内容。可以用两条约束、不同优先级来表达:
// 内容撑开,优先级稍低 [panel mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(contentHeight).priority(750); }]; // 最大高度,优先级高 [panel mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_lessThanOrEqualTo(300).priorityRequired(); }];优先级 750 是NSLayoutPriorityDefaultHigh,低于必须满足的 1000。当内容高度超过 300 时,高优先级的"小于等于 300"会赢,面板被限制在 300;内容不足 300 时,750 那条生效,高度跟着内容走。用优先级而不是 if-else 判断,好处是切换状态时只改constant,不用重装约束,动画也更顺。
5. 冲突排查与工程规范
5.1 读懂冲突日志的三个线索
约束冲突的日志看着吓人,其实结构很固定,抓住三个线索就能快速定位。
第一个线索是Unable to simultaneously satisfy constraints后面那段约束列表。每条约束都会打印出类名、内存地址和数学表达式,比如MASLayoutConstraint:0x7f8a UIView:0x7f90.height == 44。MASLayoutConstraint这个类名说明约束是 Masonry 创建的,不是系统或手写的。表达式直接告诉你"哪条约束、什么关系、什么值"。
第二个线索是Will attempt to recover by breaking constraint后面那条。求解器在冲突时只能随机断一条来恢复,断掉的往往是数值较小的那条,不一定是你想断的。看到这行不要急着改它,要找的是另一条和它矛盾的约束。
第三个线索是地址。同一个视图的所有约束地址前缀通常相近,因为它们是同批创建的。如果日志里出现了好几条关于同一个视图的height,基本可以确定是mas_makeConstraints被调了两次。
实操中最有效的排查手段是给约束加key:
[self.cardView mas_makeConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(120).key(@"card.height"); }];加了 key 之后,日志和调试工具里都能看到这个标识,比对着内存地址猜靠谱得多。团队里推广一下这条习惯,排查效率提升非常明显。
5.2 高频问题速查表
下面这些是我在真实项目里反复遇到的,按出现频率排了序。
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 视图完全不显示 | 没加addSubview,或约束相对的是未入层级的视图 | 检查加入父视图的顺序,先 add 再 make |
| 布局改了没反应 | 只调了mas_makeConstraints新增约束,旧约束还在 | 改用mas_updateConstraints或remake |
| 约束越积越多 | 在layoutSubviews或cellForRow里无脑 make | 约束只在初始化时建一次,之后只改数值 |
| 滚动视图滚不动 | 子视图约束相对 scrollView 而非 contentView | 加 contentView,闭合高度链 |
| cell 高度都变成默认值 | 垂直链没闭合,缺 bottom 约束 | 最后一项约束到contentView.mas_bottom |
| 动画瞬间完成 | 先layoutIfNeeded后改约束 | 改成先改约束,再进动画块刷新 |
| 偶发崩溃 | 在 view 未加入父视图时引用了父视图的 mas 属性 | 保证引用方已在层级中 |
| 宽高比约束不生效 | 用equalTo传了浮点比例值 | 值类型一律用mas_equalTo |
| 约束冲突日志刷屏 | 同一个属性有多条等值约束 | 给约束加 key,统一管理 |
这张表可以贴在团队的知识库里,新同学入职时看一遍能少踩一半的坑。
5.3 团队协作下的约束管理约定
一个界面两三个人写,约束代码很快就会变成没人敢动的雷区。几个约定能让它一直保持可维护。
约束代码集中在setupConstraints方法里,不要散落在业务逻辑中间。视图的生命周期里约束只应该建立一次,其他时候都是修改。把建立约束抽成一个独立方法,修改约束的地方就只剩下明确的几处,改界面时定位成本很低。
给关键约束加 key,尤其是那些后面会被update的。key 的命名用"视图名.属性"的格式,比如card.height、button.leading,团队里统一一下格式就行。
避免在分类里用mas_之外的前缀。Masonry 自己占用了mas_前缀,你在UIView上扩展方法时用别的规范前缀(比如demo_),能避免将来升级库版本时的方法名冲突。
约束的数量要有个意识。一个 cell 里超过 30 条约束,滚动性能就会开始受影响。这不是 Masonry 的问题,是 Auto Layout 本身的求解成本。列表页的 cell 布局,能简化的尽量简化:能用edges就别写四条边,能用size就别写宽高两条,能用center就别写centerX加centerY。少一条约束就少一次求解。
还有一个容易被忽略的点:给约束加上注释,说明这条为什么存在。比如"这里必须留 8 而不是 16,因为设计上需要和图标基线对齐"——这种信息在半年后只有写的人记得,而写的人半年后也忘了。
我个人在几个项目里推这套约定的体会是,最有效的不是规范本身,而是把约束代码从业务代码里物理隔离出来。视图控制器里只保留布局相关的几行,剩下全在子视图的初始化里闭环。这样改业务逻辑的时候不会误伤布局,改布局的时候也不用在几百行代码里翻找。踩过几次"改了个判断条件结果整个界面错位"的坑之后,你也会认同这一点。