写这篇源码解析的引子呢,是因为最近做了一次“大改版”的需求,有些功能重新开发,也有一些改动小的功能在老代码上修改,因为设计的UI改动比较多,review代码的时候就发现,很多copy来的旧代码,被删删改改换了一个新面貌。其中有不少是页面UI相关的代码,copy过来很容易就缺少自己的思考,发现了很多Masonry
的使用不太合理,比如view
的首次布局就使用mas_remakeConstraints
等等。
其实大家做开发都好多年了,这种错误很容易发现也很容易改正,用的都很6却不是每个人都知道Masonry
的工作原理,索性最近居家办公,抽空写一写我自己对Masonry
的一点儿读码笔记。也在掘金上看了一些别人写的文章,也学习到了很多,自己写一写再加深一点印象。
为什么代码可以这么写
先看看平时我们是如何使用Masonry
的:
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.top.mas_equalTo(0);
make.width.mas_equalTo(110);
}];
复制代码
上面是一段Masonry
的最常用的代码,那么第一个问题是:
为什么
self.titleLabel
可以直接调用mas_makeConstraints
?
这是个很显而易见的问题,Masonry
是写了个UIView
的分类吧,方便任何UIView
以及其子类可以直接使用Masonry
中的布局方法。---------View+MASAdditions
下一个问题:
为什么可以
make.leading.top.mas_equalTo(0);
这样点语法的形式一直调用
点语法,我们可能最先想到的就是调用了getter
方法,返回一个对应的对象,比如这样一句简单的代码self.label.text = @"xxx"
,不难理解,self.label
返回懒加载的UILabel
对象,而UILabel
又有一个text
的属性,可以通过点语法访问这个属性,并赋值。
说到这里,是不是就有些明白了,点语法不仅可以调用getter
方法,其实定义的任何方法都可以,只是用点语法调用的时候会收获一个warning ----- Property access result unused - getters should not be used for side effects. 但是调用效果和[]调用是一样的。
再回到make.leading.top.mas_equalTo(0);
这句代码中来,make是一个MASConstraintMaker
对象,MASConstraintMaker
拥有leading
这个属性,所以make.leading
其实就是调用了getter
方法,而leading
是MASConstraint
类型,那么就去看看,MASConstraint
是不是得有个top
属性啊,肯定有啊 ,没有这代码得报错啊!
就这么一步步分析,其实这个点语法,说高级点叫链式调用(method chaining),就得遵循这么个规则。除了上面说的可以通过点语法多次连接调用,还有一个更重要的知识点是这种方式如何传参。
跟到mas_equalTo(0);
这个方法的源码中,首先可以看到,mas_equalTo
返回的是一个block
类型,具体实现:
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
复制代码
MASConstraint * (^)(id)
是一个带参数有返回值的block,就是说调用完.mas_equalTo
其实是返回了一个block类型,block如何执行呢,我们当然知道,直接block()
就可以,而这里的block是需要参数的,那我们正好也传了一个数值的参数进去,最后,这个block执行完继续收获一个MASConstraint
。
说到这里,是不是有点恍然大悟了,每个点语法调用一次就返回一个MASConstraint
,想想日常使用的场景,是不是后面还可以继续.mas_offset(10),可以继续链式调用。也就是说,每次链式调用都返回自身,然后就可以继续调用自身的其他方法了。
为什么不直接使用NSLayoutConstraint
NSLayoutConstraint
是UIKit库中自带的UI布局大法,却被我们抛弃,除了一些封装的三方或者二方组件中,为了减少依赖,基本很少出现在项目代码中,可能说的有些绝对,在我们的项目中是这样。欣赏一段使用代码:
[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],
]];
复制代码
这仅仅是设置一个view的约束,就如此繁琐,但好在代码可读性不差,嗯~不爱但也不要伤害。
而Masory
的出现,帮助开发者大大提高开发效率同时降低代码量,但是,如果你看过源码,你一定知道,被你用的很6的Masory
就是基于NSLayoutConstraint
的封装。跟到源码中,最后会看到下面的代码:
MASLayoutConstraint *layoutConstraint
= [MASLayoutConstraint constraintWithItem:firstLayoutItem
attribute:firstLayoutAttribute
relatedBy:self.layoutRelation
toItem:secondLayoutItem
attribute:secondLayoutAttribute
multiplier:self.layoutMultiplier
constant:self.layoutConstant];
[self.installedView addConstraint:layoutConstraint];
复制代码
和上面NSLayoutConstraint
的使用如出一辙,而MASLayoutConstraint
也是继承自NSLayoutConstraint
。了解了这一点,我们再继续去浅析一下Masory
的工作原理。
浅析原理
前面一直铺垫,终于写到原理部分了。就是要看看源码,这里好像也没什么技巧,就debug断点看看调用链,然后学习一下作者的编码技巧吧。再回到最开始这句代码:
[self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.top.mas_equalTo(0);
make.width.mas_equalTo(110);
}];
复制代码
debug执行一步,调入UIView+MASAdditions.m
中:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
复制代码
- mas_makeConstraints:
这个方法的参数是一个block,block在方法内部被执行,所以这里的重点是,block中我们要如何把想加在view上的约束,告诉Masory
。
这里就要研究另外一个重要的类,根据名字就可以知道,这是一个“创建约束”的类MASConstraintMaker
。那是不是可以猜测,把约束都交给这个“创建约束”的类,他内部就会自动帮我做很多事,比如组织一系列NSLayoutConstraint
需要的参数。因为上面说过了Masory
是基于NSLayoutConstraint
的封装,那Masory
为我们开发者提供的最重要的一件事就是,把我们写的一些简单的top,bottom
等,变成NSLayoutConstraint
的实例,最终加载到view
上。
继续查看MASConstraintMaker
的初始化方法:
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
self.view = view;
self.constraints = NSMutableArray.new;
return self;
}
复制代码
记录了需要加约束的view
,还创建了一个用来存放约束的数组constraints
,为啥用数组存呢,也不难理解,因为想要view
放在一个指定的位置,肯定是有一组约束来共同实现的。
继续往下执行到block(constraintMaker);
,执行这个block中的代码,就是我们真正写约束的代码了:
^(MASConstraintMaker *make) {
make.leading.top.mas_equalTo(0);
make.width.mas_equalTo(110);
}
复制代码
上面分析链式调用的时候就知道了,这样一串儿的点语法,其实是调用了一个又一个的方法,比如leading:
- (MASConstraint *)leading {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
}
复制代码
这里就得继续介绍一个抽象类MASConstraint
,他有两个子类,分别是MASViewConstraint
和MASCompositeConstraint
,为啥要有两个子类呢,继续往下看源码就知道了。
最后- addConstraintWithLayoutAttribute
调入下面的方法中,这里就用到了上面说的两个子类:
- (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;
}
复制代码
又出现一个类MASViewAttribute
,先不用太关注,这个类就是标记了一下约束是leading
,然后又进一步组装成了MASViewConstraint
,MASViewConstraint
才是具体描述一个约束的对象。当第一次调入这个方法的时候,肯定要走if (!constraint)
这个分支,然后把组装的newConstraint
加入存放约束的数组constraints
中,并返回newConstraint
。这里就可以看出抽象类和派生类存在的意义了,因为是集成关系,可以直接返回MASViewConstraint
和MASCompositeConstraint
。
到这里.leading的调用就结束了,总结一下就是组装了一个用来描述约束leading的对象,然后把这个对象存入约束数组中。
然后继续.top的调用,还是重复上面的调用栈,又进入- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute
这个方法里,两次调用有什么不同呢?显而易见,make.leading的时候,是MASConstraint类型,而make.leading
调用完返回了MASViewConstraint
类型,再进入这个方法呢,就不是直接通过make
了,而是通过记录了leading
的那个MASViewConstraint
对象,所以当top的调用进入方法,就会进入if ([constraint isKindOfClass:MASViewConstraint.class])
这个分支中,最终把两次调用得到的两个约束存入MASCompositeConstraint对象中并返回。
分析到这里就不难理解了,MASViewConstraint
是对一个单一的约束进行描述的对象,而MASCompositeConstraint
则是对多个混合的约束进行描述的对象。这里就更加感受到两个派生类的妙用了吧。以及对方法的复用,不管外部使用者是一个一个约束的设置,还是混合着好几个约束一起设置,最后都会进入这个方法里一起处理,最终存入到约束数组中。
还剩下一个.mas_equalTo
的调用,mas_equalTo
方法返回一个block,上面也介绍过了,block块内的代码如下,在调用测通过.mas_equalTo()
执行了这个block
- (MASConstraint * (^)(id))mas_equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
复制代码
根据源码可以看到这个block的参数是一个id类型,联想到我们日常的使用,不难知道,mas_equalTo
后面可以传入一个数字,也可以是其他view的某个约束,比如mas_equalTo(self.button.mas_bottom)
等,所以这里的类型不能是一个固定的类型。在回想一下NSLayoutConstraint
的使用,想一想走到这一步,对于创建一个NSLayoutConstraint
还缺哪些参数嘛?
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],
复制代码
你心里应该已经有了答案,mas_equalTo
后面传入的数字,就是上面代码中的padding.top
,如果mas_equalTo(self.button.mas_bottom)
呢,self.button.mas_bottom
是一个MASViewAttribute
类型,最后会被解析为上面代码中的superview
,NSLayoutAttributeTop
这两个参数。
我们继续看源码:
- (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;
}
};
}
复制代码
NSLayoutRelation
是一个枚举值,这里我们调用了mas_equalTo
传入的枚举值是NSLayoutRelationEqual
,attribute
传入的是一个数字,并被处理成了NSNumber
类型。attribute
是数组类型的先不看,到这里,这句make.leading.top.mas_equalTo(0);
代码终于是被执行完了。但是,到这里好像只是有了约束的各种描述,存在一个MASConstraint
类型的对象中,还有最重要的一步没有做,那就是把约束加载到对应的view上,还需要[self addConstraints:@[constrant]];
又要回到最初的代码,我们这一系列的分析,都只是执行了一个block而已啊,就是这段代码中的block(constraintMaker);,这个方法还没有执行完,还有最后的一句代码[constraintMaker install]
:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
复制代码
install做了什么事呢:
- (void)install {
.....省略一些逻辑判断......
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;
.....省略一些逻辑判断......
[self.installedView addConstraint:layoutConstraint];
}
复制代码
省略了一些逻辑判断,install就做了一件最重要的事,那就是把前面组装的约束加载到view上,实现了对view的自动布局。
以上对源码的分析,设计了masory中最重要的几个类,以及链式调用,block的运用,还有很多可以深入研究的东西这里就不唠叨了。这篇浅析确实写的太唠叨了,自我吐槽一下。希望如此的唠叨,可以让每一位读到这篇文章的人都可以有自己的收获。
思考?
1.使用Masory
为什么不会导致循环引用(block)?
这个问题比较简单,思考一下循环引用发生的条件就知道了,masonry
中设置布局的⽅法中的block
对象并没有被View
所引⽤,⽽是直接在⽅法内部同步执⾏完成,不满足发生循环引用的条件。
2.mas_makeConstraints、mas_updateConstraints、mas_remakeConstraints
的区别?
这三个方法是我们写布局代码时最常用的,也是文章开头我说的引子,理解了三者的区别才能在不同的场景下选择最适合的来用。深入源码,其实区别就一步了然了。
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.updateExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
constraintMaker.removeExisting = YES;
block(constraintMaker);
return [constraintMaker install];
}
复制代码
区别就只是设置了updateExisting
和removeExisting
,在install
时根据这两个值,做了一系列对应的操作。先看removeExisting
,也就是mas_remakeConstraints与mas_makeConstraints
的区别:
- (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
,会先拿到view
上已经设置的所有约束,然后遍历全部做一次uninstall
,就是把view上所有的约束都删除掉,然后再处理新约束的install
。所以mas_remakeConstraints
是重新为view
设置新的约束,完全不受之前约束的影响。
而对于updateExisting
,此处只是把所有存储的约束都标记一下是需要更新的,并没有删除之前的约束。真正更新约束的代码在MASViewConstraint
的install
方法中,就是上面我们分析源码的时候 ,省略的那一部分逻辑代码。
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
,会去遍历view
上已经设置的所有约束,然后遍历和当前这个约束做对比,如果找到了和当前这个约束一样的则返回,否则返回nil,如果找到了一样的约束,就直接做一次更新。这里的对比,是把firstItem、secondItem、firstAttribute、secondAttribute
等等这些属性都做了对比,都相等才算找到,那么问题就来了,我们update
了啥?
答案是:constant
,是一个float
值,就是NSLayoutConstraint
中,表示偏移量的一个值,比如make.width.mas_equalTo(110);
中,可以把110update
为90这样。而对于make.size.mas_equalTo(CGSizeMake(6, 6));
这种,也是可以更新的,因为其本质是更新了width
和height
,分解成了两个约束。
我想结论应该是,mas_updateConstraints
只可以更新约束中描述偏移量或者size
的常量值。