Masonry源码分析(下)
前言
在上一篇-Masonry源码分析(上)文章中介绍了Masonry的文件结构、大致讲了一下类中的方法,希望大家能通过上篇文章熟悉一下几个类的作用。
之前也说过了,我源码分析的思路就是讲清楚下面这个方法的底层调用步骤,看看它是如何对NSLayoutConstraints进行封装的,并且在设计的时候又有什么值得学习的地方。
[view mas_makeConstraints:^(MASConstraintMaker *make) {
// 这里例句了几个比较有代表性的方法(写法有很多)
make.top.mas_equalTo(100);
make.centerX.equalTo(self.mas_centerX);
make.width.and.height.mas_equalTo(100);
}];
废话不多,直接开始!
一、mas_makeConstraints: 方法解析
首先,这个方法是UIView的分类方法,直接被需要添加的view视图调用,在这个方法中主要做了4件事,代码如下:
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
// 关闭系统自带的 autoresizing
self.translatesAutoresizingMaskIntoConstraints = NO;
/**
* 创建约束工厂类
* 做了两件事:
* 1 声明了一个弱引用的view,绑定目标view
* 2 初始化了一个约束数组,存放约束
*/
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
// 调用block,把用户设定的约束加到上面 constraintMaker 创建的约束数组中
block(constraintMaker);
/**
* 调用约束工厂类的 install 方法
* 做了两件事:
* 1 移除已经存在的约束
* 2 组装新的约束(之前约束数组中的元素),底层调用系统的 addConstraints: 方法
*/
return [constraintMaker install];
}
1.1 关闭系统自带的autoresizing,因为你要通过代码去建立约束,不希望系统去干涉。,如果不写,在运行时会报错(不会崩溃),但约束无法正确显示。
1.2 创建MASConstraintMaker工厂类,上一篇文章也有所介绍,这个类最主要的作用就是存储用户建立的约束(在数组中),并在最后组装。
- (id)initWithView:(MAS_VIEW *)view {
self = [super init];
if (!self) return nil;
// 绑定需要添加约束的view
self.view = view;
// 初始化一个数组,存放约束
self.constraints = NSMutableArray.new;
return self;
}
这里self.view和self.constraints属性的声明如下:
// #define MAS_VIEW UIView
@property (nonatomic, weak) MAS_VIEW *view;
@property (nonatomic, strong) NSMutableArray *constraints;
这里大家先留下一个疑问,为什么view的修饰要用weak? 稍后会解答!
1.3 调用block,并将刚刚创建的MASConstraintMaker对象传进去,block执行的过程就是不断向约束数组中添加元素的过程。重点之一!
1.4 MASConstraintMaker对象调用install方法组装约束,这个方法底层调用UIView的addConstraints:方法,也可以说底层对NSLayoutConstraints进行了封装。是Masonry的核心重点。
总结:
OK!到此,mas_makeConstraints:方法内部的4行代码
1.1 没什么好解释的;
1.2 绑定view和初始化数组,且留了一个疑问:为什么view属性用weak修饰;
1.3 把约束添加到数组中;1.4 组装约束;-> 下面分别展开讲
二、block(constraintMaker) 背后
mas_makeConstraints: 方法执行到这一步的时候,会依次执行block中我们自己写的约束代码:make对象作为block传进来的参数,之所以一直命名为make,因为他是MASConstraintMaker类创建的对象(有些人可能一直在使用Masonry,但是却一直不知道为什么这样写)。
下面,我们详细展开说一下第一行 make.top.mas_equalTo(100);
其他的可以举一反三。这里涉及到 链式编程 的知识,实际上是通过点语法在调用getter方法,不了解的朋友可以去看我之前写过的一篇文章:曲线理解iOS链式编程。
首先,make对象通过点语法调用top属性的getter方法。如果看过我上面那篇文章的话,你会发现在OC中可以直接通过点语法调用方法(会报警告),但是这里为什么还要把top包装成属性呢?---我猜测是因为如果不声明属性,在xcode环境写代码的时候是不会出代码提示的,这个问题是致命的!所以要把top声明成属性,但是实际上只要知道他是在调用getter方法就可以了。
其次,我们看看top属性的getter方法中做了什么: .top方法在.top方法中进行了一次封装(可以看到.left或者.right方法都是如此)他们在最后都调用MASConstraint类中一个代理MASConstraintDelegate的方法。在这个方法中把.top约束封装成对象,添加进了maker类初始化的数组中。与此同时方法返回了一个MASConstraint对象去实现链式调用。
再次,我们展开MASConstraintDelegate中方法的实现:
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
// 通过self.view和layoutAttribute(.top),创建 视图属性类(MASViewAttribute)对象
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
/**
* 通过上面创建的 viewAttribute,创建 视图约束类(MASViewConstraint) 对象
* MASViewConstraint 是 MASConstraint抽象类 的子类(单一约束)
*/
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
// 判断有无前置的约束(例如:make.width.height,这里width就是height的前置约束)
if ([constraint isKindOfClass:MASViewConstraint.class]) {
// 如果有,则把两个约束封装金一个数组
NSArray *children = @[constraint, newConstraint];
/**
* 通过上面创建的 children数组,创建 视图约束类(MASCompositeConstraint) 对象
* MASViewConstraint 是 MASConstraint抽象类 的子类(组合约束)
*/
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
// 创建完成组合约束的对象后,把之前约束数组中加入的前置约束替换掉(把之前width的单个约束替换)
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
// 判断有无前置的约束,如果没有,直接加入约束数组中
if (!constraint) {
newConstraint.delegate = self;
// 收集约束
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.constraints indexOfObject:constraint];
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
这个方法内部比较长,我在里面的每一行基本都加入了注释,这个方法大概的意思就是把刚刚.top约束封装成对象,然后加到约束数组中。
因为在这个代理方法内部,通过self.view和.top创建了视图属性类,然后又通过视图属性创建视图约束,最后把这个约束加入到强引用的数组中,最后形成了循环引用。所以要把view用weak来修饰。
ps:上面只是把make.top讲完了,后面还有个.mas_equalTo(100),它的底层调用MASConstraint的抽象方法(在其子类中实现)。
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
/**
* 这里block的两个参数
* 1:ID类型(equalTo(@100) 或 equalTo(self) 等等)
* 2:NSLayoutRelation -> NSLayoutRelationEqual
*/
return ^id(id attribute, NSLayoutRelation relation) {
// 这里传入数组的写法不是很常见(例如:make.height.equalTo(@[view1, view2]);)
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");
// NSLayoutRelationEqual
self.layoutRelation = relation;
// 如果是equalTo(@100),实际上就是把这个100赋值给父类中的offset属性
self.secondViewAttribute = attribute;
return self;
}
};
}
这里底层调用equalToWithRelation:方法的返回值是一个带参数带返回值的block,具体用法依然可以参照我之前写的曲线理解iOS链式编程。方法内部的具体实现可以参考我写的注释。再展开讲的话篇幅太长了,所以大家自己去研究一下。
我们这里来思考一个问题,在我们make.top.mas_equalTo()的时候,Masonry实际是把mas_equalTo()写成了一个宏定义,为什么?
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
原因很简单,和之前把top的getter方法声明成属性一样,为了在xcode的调用中出现代码提示。这个想法很有意思,值得我们学习。
总结:
到此为止,mas_makeConstraints:方法内部4行代码:
1.2 存留的疑问已经解释清楚了;
1.3 block中添加约束,并且把约束添加进maker的数组中;
当然上面出现了很多新的类(MASConstraint、MASViewConstraint、MASCompositeConstraint、MASViewAttribute),如果你不清楚,你可以返回上一篇Masonry源码分析(上)查看。我也画了一个图帮助大家理解:
三、[constraintMaker install]; 背后
在上一步block执行完毕我们已经把用户添加的所有约束添加到了maker的约束数组中,但是这些约束最后怎样作用在view上呢?而且我们都知道Masonry是对系统的NSLayoutConstraints进行的封装,它到底什么时候执行的呢?---答案就在install方法中。
【更新5.18】这里工厂类调用install方法,实际拆解成MASConstraint分别调用子类的方法,这种设计模式更像是 策略模式,之前一直没理解正确,以为是抽象工厂模式,因为两者确实很难区分,但是工厂模式更侧重于对象的管理,而策略模式更侧重于方法的封装。
就Masonry的install方法而言,可以把MASConstraint理解为策略并声明了一个install抽象方法。具体的方法解耦在子类中完成。当然这里的子类还做了更多其他操作。但是就install方法本身,这个设计模式更像是策略模式,而非工厂模式。这是我的个人理解。
- (NSArray *)install {
// 移除已经存在的约束
if (self.removeExisting) {
// installedConstraintsForView 底层调用的UIview的分类属性mas_installedConstraints(MASViewConstraint中)
NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
for (MASConstraint *constraint in installedConstraints) {
[constraint uninstall];
}
}
/**
* [view addConstraints:@[.top .centerX ... ]];
* 这里把每个约束对象分别调用内部的 install 方法
*/
NSArray *constraints = self.constraints.copy;
for (MASConstraint *constraint in constraints) {
constraint.updateExisting = self.updateExisting;
// 这里MAConstraint调用install方法,实现在子类MASViewConstraint中
[constraint install];
}
// 这个view约束设置完成,把约束数组中的元素清空
[self.constraints removeAllObjects];
return constraints;
}
maker类的install方法中做了3件事:
1 如果有已存在的约束,先讲它做移除操作(uninstall)。
2 遍历约束数组,分别调用子类的install方法(核心方法)。
3 约束设置完成后,将约束数组清空。
四、子类的 [constraint install]; 背后
因为约束数组中存放的约束都是父类MASConstraint的类型,install也是个抽象方法,MASConstraint的两个子类MASViewConstraint、MASCompositeConstraint都分别实现了install方法。
MASCompositeConstraint是组合约束类,它的创建是通过一个封装了2个MASViewConstraint类元素的数组。所以在install的时候实际上就是调用单个约束类的install方法。
- (void)install {
for (MASConstraint *constraint in self.childConstraints) {
constraint.updateExisting = self.updateExisting;
[constraint 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;
...
// NSLayoutConstraint 封装
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; // key
// self.installedView 赋值
...
/* addConstraint: 添加约束 */
[self.installedView addConstraint:layoutConstraint];
...
在这个方法中,我们很清楚的看到它调用了系统的NSLayoutConstraint去创建约束,并且调用了view的addConstraint:去添加约束。万变不离其宗!
总结:
梳理了一下流程: makeConstraints:方法调用流程个人的收获:
通过两天的学习和整理,我个人对Masonry的底层原理有了一定的理解,在这个过程中也发散了很多知识点,比如链式编程、抽象类、工厂方法等等。同时也总结了一套阅读优秀源码的方法。这个学习过程本身收货的更多。
我觉得在源码阅读的时候,自己的惰性和浮躁比代码难度起到更大的阻碍作用。但是提升自己没有捷径,加油!
关于Masonry的这3篇文章在这里就告一段落了,纯原创。所以如果发现其中的错误我会第一时间改正。
我在阅读过程中给源码加的注释也给大家参考一下: LearnMasonry