在了解Masonry框架之前,有必要先了解一下自动布局的概念。在iOS6之前,UI布局的方式是通过frame属性和Autoresizing来完成的,而在iOS6之后,苹果公司推出了AutoLayout的布局方式,它是一种基于约束性的、描述性的布局系统,尤其是苹果的手机屏幕尺寸变多之后,AutoLayout的应用也越来越广泛。

但是,手写AutoLayout布局代码是十分繁琐的工作(不熟悉的话,可以找资料体验一下,保证让你爽到想哭,_);鉴于此,苹果又开发了VFL的布局方式,虽然简化了许多,但是依然需要手写很多代码;如果,你希望不要手写代码,那么可以用xib来布局UI,可以图形化添加约束,只是xib的方式不太适合多人协作开发。综合以上的各种问题,Masonry出现了,这是一款轻量级的布局框架,采用闭包、链式编程的技术,通过封装系统的NSLayoutConstraints,最大程度地简化了UI布局工作。

本文主要分析一下Masonry的源码结构、布局方式和实现原理等等。

框架结构

Masonry框架的源码其实并不复杂,利用自己的描述语言,采用优雅的链式语法,使得自动布局方法简洁明了,并且同时支持iOSMacOS两个系统。Masonry框架的核心就是MASConstraintMaker类,它是一个工厂类,根据约束的类型会创建不同的约束对象;单个约束会创建MASViewConstraint对象,而多个约束则会创建MAXCompositeConstraint对象,然后再把约束统一添加到视图上面。

布局方式

Masonry的布局方式比较灵活,有mas_makeConstraints(创建布局)、mas_updateConstraints(更新布局)、mas_remakeConstraints(重新创建布局)三种:

1.mas_makeConstraints:

给视图(视图本身或父视图)添加新的约束

// 给view1添加约束,frame和superView一样
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview);
}];

2.mas_updateConstraints:

更新视图的约束,从视图中查找相同的约束,如果找到,就更新,会设置makerupdateExistingYES

// 更新view1的上边距离superView为60,宽度为100
[view1 mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(@60);
make.width.equalTo(@100);
}];

3.mas_remakeConstraints:

给视图添加约束,如果视图之前已经添加了约束,则会删除之前的约束,会设置makerremoveExistingYES

// 重新设置view1的约束为:顶部距离父视图为300,左边距离父视图为100,宽为100,高为50
[view1 mas_remakeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview).offset(300);
make.left.equalTo(superview).offset(100);
make.width.equalTo(@100);
make.height.equalTo(@50);
}];

Masonry的布局相对关系也有三种:.equalTo(==)、.lessThanOrEqualTo(<=)、.greaterThanOrEqualTo(>=)。

Masonry的布局关系的参数也有三种:

1. @100 --> 表示指定具体值

2. view --> 表示参考视图的相同约束属性,如view1的left参考view2的left等

3. view.mas_left --> 表示参考视图的特定约束属性,如view1的left参考view2的right等

实现原理

Masonry是利用闭包和链式编程的技术实现简化操作的,所以需要对闭包和链式编程有一定的基础。下面会根据案例来具体分析一下Masonry的实现细节,代码实现的功能是设置view1frameCGRectMake(100, 100, 100, 100);其中,mas_equalTo(...)是宏,会被替换成equalTo(MASBoxValue((...))),功能是把基本类型包装成对象类型:

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.top.equalTo(@100);
make.size.mas_equalTo(CGSizeMake(50, 50));
}];

1.创建maker

首先调用mas_makeConstraints:,这是一个UIView的分类方法,参数是一个设置约束的block,会把调用视图作为参数创业一个maker

// View+MASAdditions.h
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
self.translatesAutoresizingMaskIntoConstraints = NO;
// 创建maker,并保存调用该方法的视图view
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker);
return [constraintMaker install];
}

2.生成约束

接下来,开始利用maker产生约束,即调用block(constraintMaker)

2.1 设置坐标x

make.left

调用过程:

// MASConstraintMaker.h
- (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 isKindOfClass:MASViewConstraint.class]) {
// 传入的参数是nil,所以此处代码不会执行
...
}
if (!constraint) {
// 设置newConstraint的代理为maker
newConstraint.delegate = self;
// 把约束加入到数组中
[self.constraints addObject:newConstraint];
}
// 返回MASViewConstraint类型的约束对象
return newConstraint;
}

其中,上述代码根据maker保存的view和传入的约束属性layoutAttribute创建了一个MASViewAttribute对象,然后根据viewAttribute对象创建了一个MASViewConstraint约束对象,代码如下:

// MASViewAttribute.h
- (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 = view;
_item = item;
// 保存约束属性:NSLayoutAttributeLeft
_layoutAttribute = layoutAttribute; return self;
} // MASViewConstraint.h
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
self = [super init];
if (!self) return nil; // 保存第一个属性(封装了视图view和约束属性NSLayoutAttributeLeft)
_firstViewAttribute = firstViewAttribute;
self.layoutPriority = MASLayoutPriorityRequired;
self.layoutMultiplier = 1; return self;
}

2.2 设置坐标y

make.left.top

由于make.left返回的是MASViewConstraint对象,所以调用的top应该是MASViewConstraint类中的方法(该方法继承自父类MASConstraint),调用过程如下:

// MASConstraint.h
- (MASConstraint *)top {
// self是MASViewConstraint对象
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
} // MASViewConstraint.h
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation"); // self.delegate是maker对象
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
} // MASConstraintMaker.h
- (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]) {
// 由于参数constraint不为nil,所以进入此处执行
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
// 创建约束集合对象,并把先前的约束对象和本次新创建的约束对象保存到数组中
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
// 设置约束集合对象的代理为maker
compositeConstraint.delegate = self;
// 用约束集合对象替换maker中已经保存的约束对象,因为我们同一个maker设置了2个以上的约束
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
// 返回MASCompositeConstraint约束集合对象
return compositeConstraint;
}
if (!constraint) {
...
}
return newConstraint;
}

如果一个maker添加多个约束后,就会创建MASCompositeConstraint对象,创建约束集合的过程如下:

- (id)initWithChildren:(NSArray *)children {
self = [super init];
if (!self) return nil; // 保存约束数组
_childConstraints = [children mutableCopy];
for (MASConstraint *constraint in _childConstraints) {
// 设置数组中所有的约束对象的代理为MASCompositeConstraint对象
constraint.delegate = self;
} return self;
}

在创建了MASCompositeConstraint对象后,就会更新maker中的约束数组,在最后添加约束的时候,就会是全部的约束对象,代码如下:

- (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];
}

2.3 设置x、y的值

make.left.top.equalTo(@100)

make.left.top返回的对象是MASCompositeConstraint类型,调用过程如下:

// MASConstraint.h
- (MASConstraint * (^)(id))equalTo {
return ^id(id attribute) {
return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
} // MASCompositeConstraint.h
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attr, NSLayoutRelation relation) {
for (MASConstraint *constraint in self.childConstraints.copy) {
// 遍历数组,把每个MASViewConstraint对象都调用该方法
constraint.equalToWithRelation(attr, relation);
}
return self;
};
} // MASViewConstraint.h
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
return ^id(id attribute, NSLayoutRelation relation) {
if ([attribute isKindOfClass:NSArray.class]) {
// 由于attribute是@100的包装类型,不是数组,此处代码不会执行
...
} else {
NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
// 设置约束类别为NSLayoutRelationEqual
self.layoutRelation = relation;
// 设置第二个属性
self.secondViewAttribute = attribute;
return self;
}
};
} - (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
_layoutRelation = layoutRelation;
// 表明已经有了约束关系
self.hasLayoutRelation = YES;
}

下面分析一下设置第二个属性secondViewAttribute的过程,因为Masonry重写了setter方法,过程如下:

// MASViewConstraint.h
- (void)setSecondViewAttribute:(id)secondViewAttribute {
if ([secondViewAttribute isKindOfClass:NSValue.class]) {
// secondViewAttribute是@100类型
[self setLayoutConstantWithValue:secondViewAttribute];
} else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
// secondViewAttribute是视图UIView类型
_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
} else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
// secondViewAttribute是view.mas_left类型
_secondViewAttribute = secondViewAttribute;
} else {
NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
}
} // MASConstraint.h @100类型
- (void)setLayoutConstantWithValue:(NSValue *)value {
// 根据value的不同类型,设置不同的属性值
if ([value isKindOfClass:NSNumber.class]) {
self.offset = [(NSNumber *)value doubleValue];
} else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
CGPoint point;
[value getValue:&point];
self.centerOffset = point;
} else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
CGSize size;
[value getValue:&size];
self.sizeOffset = size;
} else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
MASEdgeInsets insets;
[value getValue:&insets];
self.insets = insets;
} else {
NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
}
}

由于@100NSNumber类型,所以执行self.offset来设置偏移量,代码如下:

// MASViewConstraint.h
- (void)setOffset:(CGFloat)offset {
// 设置layoutConstant属性值,在最后添加属性时作为方法参数传入
self.layoutConstant = offset;
} - (void)setLayoutConstant:(CGFloat)layoutConstant {
_layoutConstant = layoutConstant; #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV)
...
#else
self.layoutConstraint.constant = layoutConstant;
#endif
}

这里有网友疑惑,因为self.layoutConstraint在上面的方法中一直是nil,设置它的constant属性是没有意义的,不知道这么写有何意义?其实,我也有同样的疑问!!!

2.4 设置size

另外,make.size的实现过程和上面的分析类似,有兴趣的可以自行参考,看一看具体的实现过程,在此不做分析。

3.安装约束

下面分析一下约束的安装过程

[constraintMaker install]

调用过程如下:

// MASConstraintMaker.h
- (NSArray *)install {
if (self.removeExisting) {
// 是remake,所以要先删除已经视图中存在的约束
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;
} // MASViewConstraint.h
- (void)uninstall {
if ([self supportsActiveProperty]) {
// 如果 self.layoutConstraint 响应了 isActive 方法并且不为空,会激活这条约束并添加到 mas_installedConstraints 数组中,最后返回
self.layoutConstraint.active = NO;
[self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
return;
} [self.installedView removeConstraint:self.layoutConstraint];
self.layoutConstraint = nil;
self.installedView = nil; [self.firstViewAttribute.view.mas_installedConstraints removeObject:self];
}

下面分析一下install的过程:

- (void)install {
// 如果已经安装过约束,直接返回
if (self.hasBeenInstalled) {
return;
} // 如果 self.layoutConstraint 响应了 isActive 方法并且不为空,会激活这条约束并添加到 mas_installedConstraints 数组中,最后返回
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; // alignment attributes must have a secondViewAttribute
// therefore we assume that is refering to superview
// eg make.left.equalTo(@10)
if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
// 如果第一个属性不是size属性,并且第二个属性为nil,就把第二个视图设置为view的父视图,约束属性设置为view的约束属性
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];
NSAssert(closestCommonSuperview,
@"couldn't find a common superview for %@ and %@",
self.firstViewAttribute.view, self.secondViewAttribute.view);
self.installedView = closestCommonSuperview;
} else if (self.firstViewAttribute.isSizeAttribute) {
// 如果第一个属性是设置size的,就把第一个视图赋值给installedView
self.installedView = self.firstViewAttribute.view;
} else {
// 否则就取第一个视图的父视图赋值给installedView
self.installedView = self.firstViewAttribute.view.superview;
} MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
// 如果是更新属性,就根据layoutConstraint查看视图中是否存在该约束
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];
}
}

求两个视图的最小父视图的代码如下:

- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
MAS_VIEW *closestCommonSuperview = nil; MAS_VIEW *secondViewSuperview = view;
while (!closestCommonSuperview && secondViewSuperview) {
MAS_VIEW *firstViewSuperview = self;
while (!closestCommonSuperview && firstViewSuperview) {
if (secondViewSuperview == firstViewSuperview) {
// 如果first和second的视图一样,就设置closestCommonSuperview,并返回
closestCommonSuperview = secondViewSuperview;
}
firstViewSuperview = firstViewSuperview.superview;
}
secondViewSuperview = secondViewSuperview.superview;
}
return closestCommonSuperview;
}

其实,上述代码是先判断firstsecond的视图是否一样,如果一样,直接返回;如果不一样,就判断fisrt的父视图和second是否一样,如果一样,就返回;不一样,继续判断first的父视图和second的父视图是否一样,如果一样,就返回;不一样,重复迭代。


结束语

Masonry的源码分析完结,如果文中有不足之处,希望指出,互相学习。

参考资料

Masonry

Masonry 源码解析

RAC之masonry源码深度解析

Masonry 源码进阶

学习AutoLayout(VFL)

iOS开发-自动布局篇:史上最牛的自动布局教学!

Masonry1.0.2 源码解析的更多相关文章

  1. SpringBoot 2.0.3 源码解析

    前言 用SpringBoot也有很长一段时间了,一直是底层使用者,没有研究过其到底是怎么运行的,借此机会今天试着将源码读一下,在此记录...我这里使用的SpringBoot 版本是  2.0.3.RE ...

  2. YYModel V1.0.4源码解析

    YYKit出现了很长时间了,一直想要详细解析一下它的源码,都是各种缘由推迟了. 最近稍微闲了一点,决定先从最简单的YYModel开始吧. 首先,我也先去搜索了一下YYModel相关的文章,解析主要AP ...

  3. 【JUC源码解析】CyclicBarrier

    简介 CyclicBarrier,一个同步器,允许多个线程相互等待,直到达到一个公共屏障点. 概述 CyclicBarrier支持一个可选的 Runnable 命令,在一组线程中的最后一个线程到达之后 ...

  4. Redis系列(十):数据结构Set源码解析和SADD、SINTER、SDIFF、SUNION、SPOP命令

    1.介绍 Hash是以K->V形式存储,而Set则是K存储,空间节省了很多 Redis中Set是String类型的无序集合:集合成员是唯一的. 这就意味着集合中不能出现重复的数据.可根据应用场景 ...

  5. ArrayList、CopyOnWriteArrayList源码解析(JDK1.8)

    本篇文章主要是学习后的知识记录,存在不足,或许不够深入,还请谅解. 目录 ArrayList源码解析 ArrayList中的变量 ArrayList构造函数 ArrayList中的add方法 Arra ...

  6. EventBus3.0源码解析

    本文主要介绍EventBus3.0的源码 EventBus是一个Android事件发布/订阅框架,通过解耦发布者和订阅者简化 Android 事件传递. EventBus使用简单,并将事件发布和订阅充 ...

  7. solr&lucene3.6.0源码解析(四)

    本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...

  8. solr&lucene3.6.0源码解析(三)

    solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...

  9. Heritrix 3.1.0 源码解析(三十七)

    今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...

随机推荐

  1. java中方法的定义

    所谓的方法(将方法称为函数)指的就是一段可以被重复调用的代码块. 对于方法的返回值类型有两种使用形式: · 有数据返回:返回值类型就使用 Java 中定义的数据类型: · 无数据返回:使用 void ...

  2. vue.js 组件之间传递数据

    前言 组件是 vue.js 最强大的功能之一,而组件实例的作用域是相互独立的,这就意味着不同组件之间的数据无法相互引用.如何传递数据也成了组件的重要知识点之一. 组件 组件与组件之间,还存在着不同的关 ...

  3. java 重载与重写 【转】

    首先我们来讲讲:重载(Overloading) (1) 方法重载是让类以统一的方式处理不同类型数据的一种手段.多个同名函数同时存在,具有不同的参数个数/类型. 重载Overloading是一个类中多态 ...

  4. Docker进阶使用1

    容器间共享文件 Docker 的容器和外部环境是相对隔离的,并且容器是一次性的,运行结束后并不会有任何的持久化的文件或者数据.所以当我们需要做应用数据的持久化,或者保留应用的日志文件时,我们需要用到 ...

  5. salesforce零基础学习(七十五)浅谈SOSL(Salesforce Object Search Language)

    在工作中,我们更多操作的是一个表的对象,所以我们对SOQL的使用很多.但是有时候,我们需要对几个表进行查询操作,类似salesforce的全局搜索功能,这时,使用SOQL没法满足功能了,我们就需要使用 ...

  6. 函数响应式编程及ReactiveObjC学习笔记 (四)

    今天我们继续看其他的类别 UIImagePickerController+RACSignalSupport.h #import <UIKit/UIKit.h> @class RACDele ...

  7. vue使用中的随笔

    在vue中vue-router配置的路径默认有"#"号,虽然无伤大雅,但是很多客户都不想看到,所以在初始配置路由的时候加上下面一句代码就可以了 mode:'history', 路径 ...

  8. Oracle 11g RAC 修改各类IP地址

    Oracle 11g RAC 修改各类IP地址 首先,我们都知道Oracle 11g RAC中的IP主要有:Public IP.VIP.SCAN VIP.Private IP这几种. 一般这类改IP地 ...

  9. wpf的一些总结

    wpf技巧 隐藏控件不占空间,设置visibility为:Collapsed tabcontrol的高度宽度跟随界面的大小变化:属性height\width绑定grid的actualheight\ac ...

  10. [译] 所有你需要知道的关于完全理解 Node.js 事件循环及其度量

    原文地址:All you need to know to really understand the Node.js Event Loop and its Metrics 原文作者:Daniel Kh ...