开篇叨叨

Masonry 是iOS目前用得比较多的一个布局框架,被广泛应用与较为简单的界面布局中,它是对iOS AutoLayout的一个封装,如果有使用过AutoLayout 原生API进行布局过我相信很难摆脱它给我们iOS职业生涯带来的阴影。经过封装过的Masonry提供了一种链式方式来描述NSLayoutConstraints,可以说经过封装后的Masonry有一种从地狱到天堂的感受。

我们来对比下原生AutoLayout以及使用Masonry实现同一个布局的代码效果。下面是实现在一个view中包含另外一个view,并且内部view和外部view的各个边界padding均为10

AutoLayout 版本

UIView *superview = self.view;

UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[superview addConstraints:@[

//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeLeft
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeLeft
multiplier:1.0
constant:padding.left],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeBottom
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeBottom
multiplier:1.0
constant:-padding.bottom],

[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],

]];
Masonry 版本

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];

但是碍于性能的问题在项目中还是舍弃了,但是作为对AutoLayout的学习还是值得对其进行深入分析下的。

Masonry 源码解析

Masonry 提供了mas_makeConstraints,mas_updateConstraints,mas_remakeConstraints 三个入口。

  • mas_makeConstraints:

mas_makeConstraints 是最常用的通过它我们可以很方便得为一个UIView添加各种约束,mas_makeConstraints一般在viewDidLoad方法中调用来为view添加布局约束。

  • mas_updateConstraints

有时候我们只需要修改某个属性的值那么就需要用到mas_updateConstraints,mas_updateConstraints 一般在 updateConstraints方法中调用,这是苹果推荐的更新布局约束的地方。它会在调用setNeedsUpdateConstraints的时候被间接调用。

- (void)updateConstraints {

[self.button mas_updateConstraints:^(MASConstraintMaker *make) {
make.center.equalTo(self);
make.width.equalTo(@(self.buttonSize.width)).priorityLow();
make.height.equalTo(@(self.buttonSize.height)).priorityLow();
make.width.lessThanOrEqualTo(self);
make.height.lessThanOrEqualTo(self);
}];

//according to apple super should be called at end of method
[super updateConstraints];
}
  • mas_remakeConstraints

mas_remakeConstraints 和mas_updateConstraints类似,也是用于改变已经拥有的布局约束,但是不同的是它会在应用约束前线移除已经应用的约束。

mas_makeConstraints && mas_updateConstraints && mas_remakeConstraints
1. 约束条件的构建
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

和YogaKit类似它会在内部初始化一个MASConstraintMaker然后通过block传递出去,便于在外部对MASConstraintMaker进行设值,最后通过MASConstraintMaker的install方法应用约束。
代码的开头有个

self.translatesAutoresizingMaskIntoConstraints = NO;

在translatesAutoresizingMaskIntoConstraints设置为YES的情况下会把autoresizingMask 转换为 Constraints,也就是把 frame ,bouds,center 方式布局的视图自动转化为约束形式,这在使用AutoLayout很容易引入不需要的约束,甚至带来约束冲突。在使用frame形式布局的时候不需要设置该值,使用默认的YES.但是对于AutoLayout记得要把这个值设置为no

在MASConstraintMaker创建方法中会创建一个constraints数组用于存放约束

- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}

我们接下来看下我们怎么使用通过block抛出来的MASConstraintMaker,假设我们设置的约束如下:

make.top.equalTo(superview.mas_top).with.offset(padding.top);

MASConstraintMaker中包含了一系列的只读MASConstraint属性:

@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
@property (nonatomic, strong, readonly) MASConstraint *leading;
@property (nonatomic, strong, readonly) MASConstraint *trailing;
@property (nonatomic, strong, readonly) MASConstraint *width;
@property (nonatomic, strong, readonly) MASConstraint *height;
@property (nonatomic, strong, readonly) MASConstraint *centerX;
@property (nonatomic, strong, readonly) MASConstraint *centerY;
@property (nonatomic, strong, readonly) MASConstraint *baseline;
@property (nonatomic, strong, readonly) MASConstraint *firstBaseline;
@property (nonatomic, strong, readonly) MASConstraint *lastBaseline;
@property (nonatomic, strong, readonly) MASConstraint *leftMargin;
@property (nonatomic, strong, readonly) MASConstraint *rightMargin;
@property (nonatomic, strong, readonly) MASConstraint *topMargin;
@property (nonatomic, strong, readonly) MASConstraint *bottomMargin;
@property (nonatomic, strong, readonly) MASConstraint *leadingMargin;
@property (nonatomic, strong, readonly) MASConstraint *trailingMargin;
@property (nonatomic, strong, readonly) MASConstraint *centerXWithinMargins;
@property (nonatomic, strong, readonly) MASConstraint *centerYWithinMargins;
@property (nonatomic, strong, readonly) MASConstraint *edges;
@property (nonatomic, strong, readonly) MASConstraint *size;
@property (nonatomic, strong, readonly) MASConstraint *center;

我们这里以top为例:

- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}

我们这里constraint传入的为nil 所以代码的前半部分没有走,直接将self这是为newConstraint的delegate,并将其添加到constraints数组中,并返回newConstraint。

MASViewAttribute这个类是用于存储view以及它相关的布局属性NSLayoutAttribute,而MASViewConstraint则是一条布局约束规则,它包含着MASViewAttribute。
那么这个delegate作用是啥,留到后面介绍。

到目前为止,我们创建了一个MASConstraintMaker,MASConstraintMaker 包含一个constraints数组,用于存储这个view相关的约束,然后将MASConstraintMaker 通过block传递出去,我们在block中为MASConstraintMaker添加各个约束,一个约束是一个MASViewConstraint对象,它是由一个包含着NSLayoutAttribute以及关联的view的MASViewAttribute对象。并将新建的newConstraint返回。

MASConstraint有两个子类:MASViewConstraint 和 MASCompositeConstraint。 MASViewConstraint 为单条约束,而MASCompositeConstraint是类似于edges,size, center 的约束,它是单条约束的集合,比如size 是 width和height的集合,

我们接下来看下equalTo方法:

- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}

MASConstraint 是一个基类这里的部分实现都是直接抛出异常,需要子类来覆写它们。

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }

#define MASMethodNotImplemented() \
@throw [NSException exceptionWithName:NSInternalInconsistencyException \
reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
userInfo:nil]

我们看下它的子类MASViewConstraint和MASCompositeConstraint中的实现

MASViewConstraint中的实现

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
NSMutableArray *children = NSMutableArray.new;
for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.layoutRelation = relation;
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}

我们调用的时候传递进来了两个参数attribute 和 NSLayoutRelationEqual一个是关系,一个是属性。也就是说每个MASViewConstraint都有两个属性firstViewAttribute以及secondViewAttribute以及它们之间的关系layoutRelation。

typedef NS_ENUM(NSInteger, NSLayoutRelation) {
NSLayoutRelationLessThanOrEqual = -1,/*小于或者等于*/
NSLayoutRelationEqual = 0, /*等于*/
NSLayoutRelationGreaterThanOrEqual = 1,/*大于等于*/
};

这里得重点看下secondViewAttribute的设置过程

- (void)setSecondViewAttribute:(id)secondViewAttribute {
//1.
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
[self setLayoutConstantWithValue:secondViewAttribute];
//2.
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
//3.
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
}

这里有三个分支对应如下三种情况:

1. make.left.equalTo(@150); 传入 NSValue 的时, 会直接设置 constraint
2. make.left.equalTo(view); 这时, 就会初始化一个 layoutAttribute 属性与 firstViewArribute 相同的 MASViewAttribute
3. make.left.equalTo(view.mas_right); 传入一个MASViewAttribute

我们再回过头以下面约束为例子看下整个流程:

make.top.equalTo(superview.mas_top).with.offset(padding.top);

在调用make.top的时候会往通过block抛出来的MASConstraintMaker中添加一个MASViewConstraint,并将(mas_top)这个属性作为MASViewConstraint的firstViewAttribute,在调用equalTo的时候{superview.mas_top}将作为secondViewAttribute添加到刚刚创建的MASViewConstraint,并将MASViewConstraint中的layoutRelation标记为NSLayoutRelationEqual。这时候返回的是self。也就是MASViewConstraint类型。

调用.with的时候只是一种美观写法,实际上还是转调了self。没啥用处可以不写。

- (MASConstraint *)with {
return self;
}

我们再接着往下面看.offset

- (MASConstraint * (^)(CGFloat))offset {
return ^id(CGFloat offset){
self.offset = offset;
return self;
};
}

- (void)setOffset:(CGFloat)offset {
self.layoutConstant = offset;
}

这里设置了layoutConstant的值。到目前位置应该可以梳理下各个对象之间到关系了,整个关系如下图所示:

每个view都有一个MASConstraintMaker,MASConstraintMaker里面有个数组constraints用于存放我们为view添加的各个约束条件,每个约束条件都对应一条MASConstraint,MASConstraint有两个子类MASViewConstraint和MASCompositeConstraint,.top .left 这种约束属于MASViewConstraint。.size .center一般是多个约束的简化属于MASViewConstraint。
我们会将mas_top和subview.mas_top分别作为firstViewAttribute和secondViewAttribute,并用layoutRelation标明之间的关系。对于offset等布局常量都存放在layoutConstant,除了这些属性外还有layoutMultiplier,layoutPriority这些重要属性。

2. 约束条件的应用

到目前位置我们构建了当前view到约束条件,接下来我们就看如何将这些约束条件应用到view上:

- (NSArray *)install {
if (self.removeExisting) {
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
NSArray *constraints = self.constraints.copy;
for (MASConstraint *constraint in constraints) {
constraint.updateExisting = self.updateExisting;
[constraint install];
}
[self.constraints removeAllObjects];
return constraints;
}

首先先判断removeExisting字段,这个字段在mas_remakeConstraints的时候设置为YES.也就是说在mas_remakeConstraints中应用新的约束之前会将当前view上的所有的约束给清除后再应用新的。
在install方法中会对constraints的每个约束调用install方法进行应用。

- (void)install {
//在install之前,会做一次判断,看是否已经被install过
if (self.hasBeenInstalled) {
return;
}

//如果layoutConstraint支持active方法则将self.layoutConstraint.active标记为YES.
if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
//并将当前约束添加到mas_installedConstraints,mas_installedConstraints这里记录着已经安装的约束。
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}

// 否则,使用传统方式,把约束添加到对应位置
//取出firstLayoutAttribute和secondLayoutAttribute对应的数据
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

// alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
//对于对齐属性必须要有secondViewAttribute,也即是参照的对象。如果self.secondViewAttribute是空的
//这里会将参照的对象设置为当前对象的父对象。
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
secondLayoutItem = self.firstViewAttribute.view.superview;
secondLayoutAttribute = firstLayoutAttribute;
}

MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];

layoutConstraint.priority = self.layoutPriority;
layoutConstraint.mas_key = self.mas_key;

if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}


MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}
}

我们一步步来看下:

  1. 先检查下是否已经应用过了,如果已经应用过了就直接返回
if (self.hasBeenInstalled) {
return;
}
  1. 如果layoutConstraint支持active方法则将self.layoutConstraint.active标记为YES.

iOS 8版本之后,Auto Layout推出新的接口。NSLayoutConstraint多了一个active属性,用于激活、失效一个约束。不需要再考虑约束安装位置。原本用于添加、移除约束的接口addConstraint/addConstraints、removeConstraint/removeConstraints,在后续的版本升级将会过期,这里是对iOS 8版本之后做的适配。


if ([self supportsActiveProperty] && self.layoutConstraint) {
self.layoutConstraint.active = YES;
//并将当前约束添加到mas_installedConstraints,mas_installedConstraints这里记录着已经安装的约束。
[self.firstViewAttribute.view.mas_installedConstraints addObject:self];
return;
}

  1. 取出参数并对需要的参数做修正:在应用之前会将firstLayoutAttribute和secondLayoutAttribute的对应的数据取出。如果当前元素为对齐属性那么必须要有secondViewAttribute,也即是参照的对象。如果这种情况下self.secondViewAttribute是空的这里会将参照的对象设置为当前对象的父对象。

  2. 将MASViewConstraint转换成原生的AutoLayout约束条件:
    也就是把我们在block中设置的一条条MASViewConstraint生成一个个约束

    MASLayoutConstraint *layoutConstraint
    = [MASLayoutConstraint constraintWithItem:firstLayoutItem
    attribute:firstLayoutAttribute
    relatedBy:self.layoutRelation
    toItem:secondLayoutItem
    attribute:secondLayoutAttribute
    multiplier:self.layoutMultiplier
    constant:self.layoutConstant];

    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
  3. 确定installedView

// 例如make.left.equalTo(view) 情况
if (self.secondViewAttribute.view) {
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
// 例如make.left.equalTo(@40) 情况
} else if (self.firstViewAttribute.isSizeAttribute) {
self.installedView = self.firstViewAttribute.view;
} else {
self.installedView = self.firstViewAttribute.view.superview;
}

这部分首先会看下self.secondViewAttribute.view是否有值,如果有的话,会通过mas_closestCommonSuperview来查找self.firstViewAttribute.view 和self.secondViewAttribute.view最近的公共父view作为self.installedView,
如果当前约束是尺寸约束,那么将self.firstViewAttribute.view作为self.installedView,否则将self.firstViewAttribute.view.superview作为self.installedView

  1. 应用或者更新installedView上的对应约束
MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}
if (existingConstraint) {
// just update the constant
existingConstraint.constant = layoutConstraint.constant;
self.layoutConstraint = existingConstraint;
} else {
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}

updateExisting是在mas_updateConstraints方法中设置的,也就是说如果是更新某个约束条件的话先通过layoutConstraintSimilarTo找到对应的约束,如果找到了就将找到的约束赋值给self.layoutConstraint,如果没有相似的就调用addConstraint来应用约束。到此整个流程结束。

最后用一张图来结束这篇博客:

Contents
  1. 1. 开篇叨叨
  2. 2. Masonry 源码解析
    1. 2.1. mas_makeConstraints && mas_updateConstraints && mas_remakeConstraints
    2. 2.2. 1. 约束条件的构建
    3. 2.3. 2. 约束条件的应用