开篇叨叨
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:@[
[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 是最常用的通过它我们可以很方便得为一个UIView添加各种约束,mas_makeConstraints一般在viewDidLoad方法中调用来为view添加布局约束。
有时候我们只需要修改某个属性的值那么就需要用到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); }];
[super updateConstraints]; }
|
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]) { 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 { if ([secondViewAttribute isKindOfClass:NSValue.class]) { [self setLayoutConstantWithValue:secondViewAttribute]; } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) { _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute]; } 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 { if (self.hasBeenInstalled) { return; } if ([self supportsActiveProperty] && self.layoutConstraint) { self.layoutConstraint.active = YES; [self.firstViewAttribute.view.mas_installedConstraints addObject:self]; return; } MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
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) { existingConstraint.constant = layoutConstraint.constant; self.layoutConstraint = existingConstraint; } else { [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; } }
|
我们一步步来看下:
- 先检查下是否已经应用过了,如果已经应用过了就直接返回
if (self.hasBeenInstalled) { return; }
|
- 如果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; [self.firstViewAttribute.view.mas_installedConstraints addObject:self]; return; }
|
取出参数并对需要的参数做修正:在应用之前会将firstLayoutAttribute和secondLayoutAttribute的对应的数据取出。如果当前元素为对齐属性那么必须要有secondViewAttribute,也即是参照的对象。如果这种情况下self.secondViewAttribute是空的这里会将参照的对象设置为当前对象的父对象。
将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;
|
确定installedView
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; }
|
这部分首先会看下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
- 应用或者更新installedView上的对应约束
MASLayoutConstraint *existingConstraint = nil; if (self.updateExisting) { existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint]; } if (existingConstraint) { 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来应用约束。到此整个流程结束。
最后用一张图来结束这篇博客: