Masonry 的实现其实特别简洁,只有 2800 多行代码,10个类文件,以下是核心类的导图,以及核心流程解析:
NSLayoutConstraint、Masonry 对比
NSLayoutConstraint 添加 AutoLayout 约束
首先,给一个 view 添加 AutoLayout 的原生方法:
- 使用 NSLayoutAttribute 创建一个 NSLayoutConstraint 对象;
- 然后 [view addConstraint:]
UIView *superView = self.view;
UIView *view1 = [UIView new];
[superView addSubView:view1];
// 禁用自动约束
view1.translatesAutoresizingMaskIntoConstraints = NO;
// 创建一个 view1 相对于 superView 的 left 约束
NSLayoutConstraint *leftConstraint
= [NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superView
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:10];
// 省略 30 行
NSLayoutConstraint *rightConstraint;
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *bottomConstraint;
[view1 addConstraint:leftConstraint];
[view1 addConstraint:rightConstraint];
// 省略...
使用 Masonry 添加 AutoLayout 约束
上文添加的约束,使用 Masonry 如下,简化了大概 35 行代码:
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superView).offset(10);
}];
Masonry 添加约束流程解析
mas_makeConstraints 内部逻辑展开如下:
[view1 mas_makeConstraints:^(...) {
view1.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *maker = [[MASConstraintMaker alloc] initWithView:view1];
// 调用 block():
maker.left.equalTo(0);
maker.width.and.top.equalTo().offset(2);
[maker install];
}]
可以从上述例子中看到,Masonry 添加约束调用的 [view1 mas_makeConstraints:] 里,主要做了以下事情:
- 禁用 translatesAutoresizingMaskIntoConstraints 自动约束;
- 初始化一个 ConstraintMaker (约束工厂类),并给 maker 添加 上下左右/宽高 的约束(注意这一步只是将约束记录到 maker 的 constraint array 中);
- 在 maker install 时,将 constraint array 中的约束逐个 add 到 view 上;
1:生成约束(并 record)
1.1 make.left
// 语法分析:
MASConstraintMaker *make = [MASConstraintMaker new];
// make.left 只是 get 方法( MASConstraintMaker 的 MASConstraint* left 属性)
MASConstraint *mas = make.left;
- 可以看到 make.left 是调用 .left 属性;
- 但由于重写了 left 属性的 get 方法,所以,make.left 会调用 [make addConstraint:] 添加一条 NSLayoutAttributeLeft 类型的约束;
- left 属性的 get 方法,最终 return 了一个 MASConstraint 对象,所以后续才能链式调用;
// MASConstraintMaker.m
- (MASConstraint *)left {
return [self constraint:nil addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 用 view 和 NSLayoutAttribute 构建 MASViewAttribute
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
// 用 MASViewAttribute 构建 MASViewConstraint
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
// ... 省略
if (!constraint) {
// 作用??
newConstraint.delegate = self;
// 将 MASViewConstraint 添加到 make.constraints(Array) 中,等 install 时再将约束 add 到 view 上
[self.constraints addObject:newConstraint];
}
// return 的 MASViewConstraint 可以继续 链式调用
return newConstraint;
}
1.2 make.left.and
MASConstraint *mas = make.left;
中可以看到 make.left
返回的是一个 MASConstraint *
类型。
make.left.and.with
and 和 with 作为 get 方法被调用,里边只是简单的 return 了 MASConstraint self,所以可以继续链式调用
1.3 make.left.and.width
MASConstraint *mas = make.left;
中可以看到 make.left
返回的是一个 MASConstraint *
类型。因此,链式调用到 .width 时,调用的不再是 maker 的方法,而是 MASConstraint
的方法:
// MASConstraint.m
- (MASConstraint *)width {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}
MASConstraint
是一个 Abstract 抽象类:只提供接口,内部是空实现,需要子类处理。因此 width 里调用的 addConstraintWithLayoutAttribute:
会调用到子类 MASViewConstraint
中:
// MASViewConstraint.m
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
MASViewConstraint
中并没有生成或添加约束,只是调用 delegate 去处理。这个 delegate
刚好就是前边的 maker
对象,所以又回到了上一步 make.left
一样的逻辑(注意上一步中省略掉没贴的代码):
// MASConstraintMaker.m
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 同 make.left ,省略...
// MASViewAttribute *viewAttribute = ;
// MASViewConstraint *newConstraint = ;
// make.left 时,此方法传入的 constraint 是 nil,因此分析 make.left 时,这块代码省略了
// make.left.width 到 .width 时,这里传入的 constraint 就是 make.left 生成的 newConstraint 对象
if ([constraint isKindOfClass:MASViewConstraint.class]) {
// 注: 此处的 constraint 即 left constraint;newConstraint 即 width constraint
NSArray *children = @[constraint, newConstraint];
// MASCompositeConstraint 是 MASConstraint 的子类,constraint group
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
// 替换约束:
// 替换已经记录到 maker.constraintArray 中的 left constraint 约束为 constraint group
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
// ... 省略
// if 条件成立,已经 return,后续不会再重复 add constraint 到 array
}
// array 替换元素
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.constraints indexOfObject:constraint];
[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
1.4 make.left.and.width.equalTo()
用于 make.left 作为 get 方法返回的是 MASViewConstraint
对象,可以继续链式调用:make.left.equalTo(superView)
,即调用 MASViewConstraint.equalTo();
equalTo
入参是 id 类型的 attribute,attribute
类型可以是 MASViewAttribute、UIView、NSValue
,equalTo 方法内部对类型做了 if 判断;
equalTo 是个宏定义,展开后的调用链是:
qualTo(x) -> mas_equalTo(x) -> MASViewConstraint.equalToWithRelation(x, NSLayoutRelationEqual)
注意:
- 这里调用过程中将 NSLayoutRelation 参数也传入到了 MASViewConstraint.equalToWithRelation();
- equalToWithRelation 中保存了 equalTo 传入的
id 类型 attribute
和NSLayoutRelation
到 MASViewConstraint 对象中,并返回 MASViewConstraint 对象,以继续链式调用;
// MASViewConstraint.m
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
// ... 简化
// 保存了 equalTo 传入的 attribute 和 NSLayoutRelation 类型
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
// 继续返回 MASConstraint,链式调用
return self;
};
}
1.5 make.left.equalTo().offset()
继续链式调用 offset:make.left.equalTo(superView).offset(2);
注:
-
offset
和equalTo
不同,不是像equalTo
有入参的,所以可以加括号 equalTo(xxx); -
offset
方法是一个无入参方法,但是方法的返回值是一个有入参有返回值的 block 类型,因此可以 .offset(3) 调用; - 并且,由于 block 的返回值仍是 MASConstraint 对象,所以可以继续链式调用;
简化逻辑如下:
// make.left 是 get 方法
MASConstraint* mas = make.left;
MASConstraint*(^block)(CGFloat f) = mas.offset;
// 由于 .offset 的 返回值是 block,所以,可以直接调用:
mas.offset(2);
// mas.offset(2) 等同于:
block(2);
// block 的返回值
MASConstraint *mas2 = mas.offset(2);
1.6 make.xxx
- 其它
make.center
make.insets.and.size
都是一样的用法; -
make.left.priority(MASLayoutPriorityDefaultLow)
等于make.left.priorityLow
;
2:添加约束
2.1 [make install]
对 MASConstraintMaker *make 添加约束后,make install 最后执行约束:
// MASConstraintMaker.m
- (NSArray *)install {
// mas_remakeConstraints 的逻辑:先对已加的约束全部移除,再重新添加
if (self.removeExisting) {
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
NSArray *constraints = self.constraints.copy;
// 遍历 make 里添加的每个 MASConstraint 并 install
for (MASConstraint *constraint in constraints) {
// mas_updateConstraints 的逻辑: 标记是否需要更新,稍后在 MASViewConstraint install 里更新已加的约束
constraint.updateExisting = self.updateExisting;
// 最终 install 约束的还是在 MASViewConstraint 里
[constraint install];
}
[self.constraints removeAllObjects];
return constraints;
}
2.2 [constraint install]
// MASViewConstraint.m
- (void)install {
// ... 省略:避免重复添加的逻辑
// 取出 MASViewConstraint 记录的 两个 MASViewAttribute 对象
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
// ... 省略
// 1、构建一个 NSLayoutAttribute
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
// ...
// 创建完约束对象后,寻找约束该添加到那个View上:
if (self.secondViewAttribute.view) { // 如果是两个视图相对约束,就获取两种的公共父视图。
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) { // 如果添加的是Width或者Height,那么就添加到当前视图上
self.installedView = self.firstViewAttribute.view;
} else { // 如果既没有指定相对视图,也不是Size类型的约束,那么就将该约束对象添加到当前视图的父视图上
self.installedView = self.firstViewAttribute.view.superview;
}
// 是否已经添加了约束
MASLayoutConstraint *existingConstraint = nil;
// mas_updateConstraints 的逻辑
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) { // 存在,直接重新赋值
existingConstraint.constant = layoutConstraint.constant;
// ...
} else {
// 不存在,添加
// 2、[view addConstraint:NSLayoutAttribute]; !!!
[self.installedView addConstraint:layoutConstraint];
// ...
}
}
代码技巧
block 灵活使用
block 写法
@property (nonatomic, strong) UIView *(^myBlock)(NSLayoutAttribute attr);
self.myBlock = ^UIView *(NSLayoutAttribute attr) {
return greenView;
};
// blcok 在右侧时,^ 在最前
dispatch_block_t t = ^void(void) {
};
UIView *(^block)(NSLayoutAttribute attr) = self.myBlock;
self.myBlock = block;
更多 block 语法,见:How Do I Declare A Block in Objective-C?
block 作为入参简化外部调用
在方法需要传入一个 ConfigModel 时,使用 block 作为入参:让外部无需关注 ConfigModel 初始化方式,只需要关注配置项
// 使用:
[view mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(lastView).offset(2);
}];
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
// new 一个 maker,然后调用 block 传出去
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
// 外部对 maker 配置后,maker install
block(constraintMaker);
return [constraintMaker install];
}
应用案例: SDK 初始化
[SHWAccountSDK setupConfig:^(SHWAccountConfig *config) {
config.appKey = @"1";
config.secret = @"2";
}];
block 入参作为 result 回调
[SHWAccountSDK getSMSCode:phoneNum success:^(BOOL isSuccess) {
// getSMSCode 内部 new 了一个 Request 对象;
} failure:^(NSString *errCode, NSString *errMsg) {
}];
MyRequest *request;
[request startWithSuccess:^(BOOL isSuccess) {
NSLog(@"getSMSCodeAsyncWithPhoneNum success");
} failure:^(NSString *errCode, NSString *errMsg) {
NSLog(@"getSMSCodeAsyncWithPhoneNum fail");
}];
block 作为返回值,链式调用
通常函数的入参是 block 用的比较多,但函数的返回值是 block 时,可以写出链式调用的优雅写法:
- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}
// 使用:
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_bottom).offset(padding);
}];
原理:
MASConstraint *mas = [MASConstraint new];
MASConstraint*(^block)(CGFloat f) = mas.offset;
// 以下相等
block(2);
mas.offset(2);
// 由于 mas.offset 的返回值是一个 (入参是 CGFloat 的)block,在后边直接追加 (2),相当于调用 block
// 并且,由于 block 的返回值仍是 mas 类型,所以后边可以继续链式调用
对比:
// 返回值不是 block,只是 self class 时
- (MASConstraint *)setOffset:(CGFloat)offset {
self.offset = offset;
return self;
}
// 使用:
MASConstraint *mas = [MASConstraint new];
// 只能这样调用
[[mas setOffset:3] setOffset:1];
MASBoxValue: mas_equalTo、equalTo
mas_equalTo()
可以传入多种类型入参:·
[view1 makeConstraints:^(MASConstraintMaker *make) {
make.left.mas_equalTo(view2);
make.left.mas_equalTo(3);
make.left.mas_equalTo(@(2));
make.left.mas_equalTo(view2.mas_left);
make.size.mas_equalTo(CGSizeMake(10, 10));
}];
其定义如下:
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return ...;
};
}
// 尽管传入的是 id 类型,但是解析也还是要支持变参和,并将 float,double,int 这样的值类型数据转换成和 equalTo 一样的对象 NSNumber 数据:
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(int)) == 0) {
int actual = (int)va_arg(v, int);
obj = [NSNumber numberWithInt:actual];
} // ... 省略
va_end(v);
return obj;
}
这也顺便解释了 mas_equalTo
和 equalTo
的区别:没有区别,mas_equalTo
调用的还是 equalTo
,只是调用前对入参进行了 boxValue
转换类型