Masonry源码分析

AutoLayout是个好东西,但是官方的API实在不好用,Masonry应时而生为AutoLayout提供了简洁的接口,我们的项目中的布局全部都是用Masonry,可以说离了它有些寸步难行。

Masonry使用起来是十分简单的:

1
2
3
4
5
[self.aView mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(self.view);
make.top.equalTo(self.view.mas_top).offset(100);
make.width.height.mas_equalTo(200);
}];

从mas_makeConstraints开始

Masonry中使用最多的就是mas_makeConstraints:这个方法,这是用于第一次添加约束时使用的方法,关于设置约束,一共有三种方法:

1
2
3
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

从方法名可以很容易看出这三个方法分别是什么作用,第二个方法是更新约束时使用的,第三个方法是重新添加约束时使用的,也就是以前的约束不需要时完全重新设置约束,需要注意的是如果要重新设置约束一定要用第三个方法,连续调用第一个方法容易引起约束的冲突,虽然程序不一定会crash。

这三个方法会返回一个数组,这个数组中是新添加的约束,不过我从来没有用到过这个返回值,如果不是看源码,其实都不知道这些方法是有返回值的。

下面看一下mas_makeConstraints:的实现:

1
2
3
4
5
6
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

首先将translatesAutoresizingMaskIntoConstraints这个属性设置为NO

By default, the autoresizing mask on a view gives rise to constraints that fully determine

the view’s position. This allows the auto layout system to track the frames of views whose

layout is controlled manually (through -setFrame:, for example).

When you elect to position the view using auto layout by adding your own constraints,

you must set this property to NO. IB will do this for you.

这句话的意思大致就是最终系统都是用constraints的方式来组织视图,但是如果这个设置为YES系统会将你设置的Frame之类的属性转换为constraints,但是如果你要自己添加约束,也就是如果你要使用AutoLayout的话,就必须将这个属性设置为NO。如果你用InterfaceBuilder的AutoLayout,会自动将这个属性设置为NO

第二步是用当前View来实例化一个MASConstraintMaker类型的maker,这里的self是调用mas_makeConstraints:的view。

第三步执行传入的block中的代码,将刚刚实例化的maker传入block,用于配置这个maker

Note:曾经产生过一个疑惑,就是我们在使用Masonry进行布局的时候,在block中都是直接引用self的,为什么不会产生循环引用?看完源码就明白了其中的原因,首先这个block肯定是强引用了self的,假设我们是在一个VC中进行的布局(大多数情况下是这样),这个self就是VC,然后这个VC强引用了调用Masonry接口的View,但是这个view没有引用这个block,事实上这个block没有被任何对象引用,所以这个block在执行完以后就会被释放了,block引用了self,但是self没有直接或间接引用block,所以不会存在循环引用的问题。

最后向这个maker发送install的消息,将用户设置的约束添加到view上。

MASConstraintMaker

首先看看它的初始化方法:

1
2
3
4
5
6
7
8
9
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}

这里的MAS_VIEW是一个宏:

1
2
3
4
5
#if TARGET_OS_IPHONE || TARGET_OS_TV
#define MAS_VIEW UIView
#elif TARGET_OS_MAC
#define MAS_VIEW NSView
#endif

这个里使用宏的意图比较明显,Masonry希望不仅仅支持iOS,同时也支持tvOS和macOS

maker保持了当前view的引用,当然这里的引用是弱引用,虽然强引用也不会引起循环引用,但是这里弱引用其实和合理,因为如果view都不存在了,这个maker也没有存在的必要了,view不应该因为maker的引用而引用计数加1。

同时maker实例化了一个可变数组constraints,这个数组中保存的就是要添加到当前view的约束。

我们看一下当调用make.left时会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
...
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}

我们看的最终调用的是-constraint: addConstraintWithLayoutAttribute:这个方法,我删去了其中暂时无关的代码,不过删去的代码在后面还会提到。

因为传入的constraintnil,所以直接进入这个if判断,在这个判断中将新生成的constraint的代理设为maker,并将其加入self.constraints这个数组中。

最后将新生成的constraint返回。

MASConstraint

在上一节中make.left就是返回了一个MASConstraint对象,下面看一下make.left.equalTo(self.view)这句话是怎样调用的:

1
2
// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;

在MASConstraint.h中有这样一个接口,我看了半天才搞明白这是一个什么函数,这是一个返回值为block的函数,返回的这个block的返回值是MASConstraint,接受一个id类型的参数,我们看它的调用方式:.equalTo(self.view),这其实比较奇怪,因为我们知道OC中方法是不能用点语法调用的,只有属性才可以,所以其实这里可以把equalTo理解为一个block类型的属性,让这个方法实际上就是这个block的getter方法。

这个方法中的实现是这样的:

1
2
3
4
5
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}

这里直接进入了-equalToWithRelation这个方法,这是一个抽象方法,由MASConstraint的两个子类来实现MASViewConstraintMASCompositeConstraint

Note:这里的抽象方法用一种比较有趣的方法来实现,Masonry定义了一个宏叫做MASMethodNotImplemented(),这个宏会抛出一个异常,如果错误的调用了这个抽象方法在运行时就是导致crash,OC不支持抽象方法,但是这里用了一种独特的方式实现抽象方法,还是挺值得学习的。

在这个方法中传入了一个relation的参数比如上面代码中传入的NSLayoutRelationEqual,这个参数在后面的布局中是会用到的。

调用不同的方法传入的参数就不一样,比如-greaterThanOrEqualTo传入的就是NSLayoutRelationGreaterThanOrEqual,而lessThanOrEqualTo传入的就是NSLayoutRelationLessThanOrEqual

MASViewConstraint

先看一下MASConstraint这个相对简单的子类,我们关注这个子类是如何实现上述的equalToWithRelation这个方法的:

1
2
3
4
5
6
7
8
9
10
11
12
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
...
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
self.layoutRelation = relation;
self.secondViewAttribute = attribute;
return self;
}
};
}

我暂时省略了第一个判断中的内容,在else分支中,首先断言这个constraint没有被重定义。

然后设置layoutRelation,在setter方法中将上面的self.hasLayoutRelation标记为YES,这里的relation在前面说过

最后设置secondViewAttribute,看到second自然会想到会不会有first,确实是有的,first就是当前view的attribute,其实这个理解起来不难,一个约束就是描述两个view之间的关系(尺寸约束除外),所以这个MASViewConstraint最重要的三个属性就是:firstViewAttributesecondViewAttributelayoutRelation

这个secondViewAttributesetter方法里内容很多:

1
2
3
4
5
6
7
8
9
10
11
- (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);
}
}

这里的secondViewAttribute有三种类型,分别是NSValueMAS_VIEWMASViewAttribute,我可以举三个例子对应这里的三种情况:

1
2
3
make.width.mas_equalTo(100);
make.left.equalTo(self.view);
make.left.equalTo(self.view.mas_left);

其中第二行和第三行是等价的,从setter的代码里可以看出为什么第二个例子和第三个例子是等价的,因为当传入的secondViewAttribute的类型是MAS_VIEW类型时,首先会实例化一个MASViewAttribute的对象,该对象使用传入的ViewfirstViewlayoutAttribute进行配置,所以当传入self.view时会和当前viewattribute保持一致使用left

第三行传入的self.view.mas_left直接就是一个MASViewAttribute对象,直接赋值即可。

MASViewAttribute

MASViewAttribute保存三样东西:MAS_VIEW类型的viewid类型的itemNSLayoutAttribute类型的layoutAttribute

其初始化方法有两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
return self;
}
- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
self = [super init];
if (!self) return nil;
_view = view;
_item = item;
_layoutAttribute = layoutAttribute;
return self;
}

第二个方法中的item在一般情况下和第一个view是同一个对象,当使用Masonry的VC相关的接口时是指id<UILayoutSupport>

最后就是使用两个view的MASViewAttribute来构建constraint并添加到相关view上。

Install

当配置好maker后,就是install的步骤,直接看install中的部分源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)install {
...
MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
...
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
...
MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
self.installedView = closestCommonSuperview;
...
[self.installedView addConstraint:layoutConstraint];
self.layoutConstraint = layoutConstraint;
[firstLayoutItem.mas_installedConstraints addObject:self];
}

这里只关注其主要的逻辑,首先根据两个viewAttribute用系统API生成一个layoutConstraint对象,然后调用-mas_closestCommonSuperview:方法获取两个view的最近父view,最后在这个父view添加刚才生成的约束。

Note:mas_closestCommonSuperview:的逻辑是先固定一个view,然后向上遍历另一个view的父view,如果找到相同view就退出,没找到再固定第一个view的父view,继续遍历第二个view的父view,直到找到或是遍历完全部。

这其中有很多判断,会分成很多种情况,我这里讲的是最通常的那一种情况。

最后会将该constraint保存起来,同时将自身加入第一个viewinstalledConstraints的数组中

至此整个约束的添加逻辑就完成了。

MASCompositeConstraint

前面在说到make.left这句话的执行情况时省略了一部分代码,这里就将其补回来。

在最开始的使用示例中有一句话make.width.height.mas_equalTo(200);,这句话最终会进入下面这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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;
}
...
return newConstraint;
}

当我们调用make.width时返回一个MASConstraint对象,这个对象也有leftrighttopbottomwidthheight等方法,当对make.width调用.height时,就会生成一个MASCompositeConstraint对象compositeConstraint,这个对象持有一个MASConstraint类型对象的数组,同时用compositeConstraint替换原来的constraint,Masonry使用MASCompositeConstraint来支持在一句话中同时设置多个约束的行为。

我们再来看另一种情况make.top.left.bottom.right.equalTo(self.view);,在这句话中make.top.left返回的已经是一个MASCompositeConstraint对象了,这时调用.bottom时会进入MASCompositeConstraint-constraint: addConstraintWithLayoutAttribute:方法,这个方法的实现如下:

1
2
3
4
5
6
7
- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
id<MASConstraintDelegate> strongDelegate = self.delegate;
MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
newConstraint.delegate = self;
[self.childConstraints addObject:newConstraint];
return newConstraint;
}

它首先拿到自己的代理,这个代理实际上就是maker,我们看前面生成MASCompositeConstraint的代码就可知道,然后调用maker-constraint: addConstraintWithLayoutAttribute:方法,这个方法的作用在此刻就十分单纯,就是生成一个newConstraint

1
2
3
4
5
6
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
...
return newConstraint;
}

省略去的代码都是在此情况下不会执行的部分。

然后将newConstraint代理设为self,同时将其加入到self.childConstraints数组中,在后面安装时,对这个数组中每个约束都发送install消息即可。

总结

第一次阅读源代码选择了Masonry,因为其代码量不是很大,但其实跳坑里去了。Masonry的源码阅读起来真的很吃力,各种拥有类似名字的变量,各种block的嵌套,各种抽象方法给阅读带来了困难。不过这丝毫不影响这个库的优秀,它提供的接口如此简洁,使用起来是如此的丝滑,完美的阐释了那句:把复杂留给自己,把简单留给别人。

链式语法

Masonry通过使用大量的block提供了简洁的链式语法。MASConstraint这个类中的大部分方法的都返回一个block,而block的返回值都是MASConstraint,返回的MASConstraint对象又可以调用返回block的方法,正是通过这样的方式使链式语法能够工作。

抽象方法

通过定义宏:

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

来实现抽象方法真的很有创意。

宏的自动补全

我们看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))
@interface MASConstraint (AutoboxingSupport)
/**
* Aliases to corresponding relation methods (for shorthand macros)
* Also needed to aid autocompletion
*/
- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;
/**
* A dummy method to aid autocompletion
*/
- (MASConstraint * (^)(id offset))mas_offset;
@end

当我们在使用mas_equalTo()这个方法时,实际上使用的是上面的宏,但是Masonry仍然提供了方法,这样做的目的在注释中写的很清楚,为了使宏能够自动补全。

没有循环引用

使用block时最让人心烦的就是循环引用,Masonry使用block为我们提供优雅的使用方式,并没有带来循环引用的弊端,真的是优秀。