Masonry源码解读

Masonry这个框架是使用代码进行自动布局使用的,它的使用非常广泛,这段时间一直在学习这个框架,因此想把学到的东西记下来,方便以后查阅,也便于与人分享。

自动布局约束的等式:

item1.attribute1 = multiplier × item2.attribute2 + constant

Masonry中使用了大量的点链式语法,考虑到应该有些小伙伴不知道点链式语法的来龙去脉,因此这里先整理一下点链式语法

点链式语法

我们先来看一下Masonry框架的一种使用:

    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(superview.mas_left).mas_offset(30);
    }];

上面的代码是Masonry的简单的使用,这里面就用到了点链式语法make.left.equalTo(superview.mas_left).mas_offset(30);,我们看一下这句点链式语法,这里面包括三个要素:

  • 点语法:我们在访问属性的时候会使用点语法。
  • 小括号调用:在Objective-C中使用[ ]来调用方法,只有在调用Block的时候会使用(),因此这里我们可以使用Block来实现点链式语法中的()。
  • 连续调用:Block是有返回值的,那么我们可以在每次调用完Block后返回调用者对象本身,那么我们就可以实现连续的调用了。
    总结起来就是:

我们可以声明一些Block类型的属性,让block类型的属性的返回值为其本身。

下面用一个计算器的例子来说明一下:

//Calculator.h
@interface Calculator : NSObject

//这里是创建一个属性,属性的类型是block类型,属性名是add
@property (nonatomic, copy)Calculator * (^add)(NSInteger num);
@property (nonatomic, copy)Calculator * (^minus)(NSInteger num);
@property (nonatomic, copy)Calculator * (^multiply)(NSInteger num);
@property (nonatomic, copy)Calculator * (^divide)(NSInteger num);

@property (nonatomic, assign)NSInteger result;

@end
//Calculator.m
@implementation Calculator

- (instancetype)init
{
    self = [super init];
    if (self) {
        self.result = 0;
    }
    
    return self;
}

//这里实现的是add这个属性的get方法,只不过属性的类型是block类型的。
- (Calculator * (^)(NSInteger num))add
{
    return ^id(NSInteger num){
        self.result += num;
        return self;
    };
}

- (Calculator * (^)(NSInteger num))minus
{
    return ^id(NSInteger num){
        self.result -= num;
        return self;
    };
}

- (Calculator * (^)(NSInteger num))multiply
{
    return ^id(NSInteger num){
        self.result *= num;
        return self;
    };
}

- (Calculator * (^)(NSInteger num))divide
{
    return ^id(NSInteger num){
        self.result /= num;
        return self;
    };
}

@end

调用:

    Calculator *calculator = [[Calculator alloc] init];
    calculator.add(5).minus(8).multiply(8).divide(23);
  • 1.calculator.add是调用了add属性的get方法,这个方法会返回一个block,block如下:
    return ^id(NSInteger num){
        self.result += num;
        return self;
    };
  • 2.calculator.add(5)会执行这个block,这个block的返回值是Calculator对象本身,所以calculator.add(5)执行完了得到的是一个Calculator对象。
  • 3.Calculator对象继续访问minus属性,执行minus属性的get方法。
    #####更简洁的实现
    上面是通过声明一系列的block类型的属性,再实现block属性的get方法来实现链式调用,但是Masonry的实现方式和这种方式还是有区别,我们在Masonry中并没有发现Block类型的属性的声明,反而是看到了一些平时见的比较少的方法的声明:Masonry源码解读也就是说Masonry中是把Block类型的属性改成了返回值为Block类型的方法,这样也能成功实现链式语法,这是为什么呢?
    回想一下,当我们通过点语法去访问属性的时候实质上就是访问了get方法,那么当不存在一个名为name的属性时,我们使用self.name去访问时是不是也会跑去执行名为name的方法呢?答案是肯定的,也就是只要我们申明了一个xxx方法,那就可以放心的写self.xxx
    所以最终Calculator.h文件就改成了这样:
@interface Calculator : NSObject
/*
@property (nonatomic, copy)Calculator * (^add)(NSInteger num);
@property (nonatomic, copy)Calculator * (^minus)(NSInteger num);
@property (nonatomic, copy)Calculator * (^multiply)(NSInteger num);
@property (nonatomic, copy)Calculator * (^divide)(NSInteger num);
@property (nonatomic, assign)NSInteger result;
*/
- (Calculator * (^)(NSInteger num))add;
- (Calculator * (^)(NSInteger num))minus;
- (Calculator * (^)(NSInteger num))multiply;
- (Calculator * (^)(NSInteger num))divide;

@end

Masonry的使用方法

1.使用MASConstraintMaker创建约束
UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

或者更简单的方法:

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(padding);
}];
并不是只有equalTo即等于这一种关系,还可以有:

lessThanOrEqualTo:等同于NSLayoutRelationLessThanOrEqual
greaterThanOrEqualTo:等同于NSLayoutRelationGreaterThanOrEqual

2.MASViewAttribute

Masonry中有MASViewAttribute这个类,这个类就等同于NSLayoutAttribute这个类:

MASViewAttribute NSLayoutAttribute
view.mas_left NSLayoutAttributeLeft
view.mas_right NSLayoutAttributeRight
view.mas_top NSLayoutAttributeTop
view.mas_bottom NSLayoutAttributeBottom
view.mas_leading NSLayoutAttributeLeading
view.mas_trailing NSLayoutAttributeTrailing
view.mas_width NSLayoutAttributeWidth
view.mas_height NSLayoutAttributeHeight
view.mas_centerX NSLayoutAttributeCenterX
view.mas_centerY NSLayoutAttributeCenterY
view.mas_baseline NSLayoutAttributeBaseline
3.与常数有关的问题

自动布局不允许对齐的属性如left,right,centerY等设置为常数,如果我们传了一个常数给这些属性,Masonry会自动把这些约束变为相对于父视图的约束,即:

//creates view.left = view.superview.left + 10
make.left.equalTo(@10)
4.mas前缀相关

在使用Masonry的时候,有时候会比较迷糊什么时候使用带有mas前缀的,什么时候使用不带前缀的,我们看下面这句代码:

make.top.mas_equalTo(42);

这句代码也可以这样写:

make.top.equalTo(@42);

但是这样写就会报错:

make.top.equalTo(42);

原因就在于这个括号里面的参数类型必须是id类型,如果括号里面的参数不传id类型就传常量类型也行,那么就必须要在equalTo前面加上mas,加上mas后,mas_equalTo会把传进来的数值类型变成id类型。

5.MASCompositeConstraint类相关

Masonry给了我们几个便利的方法来让我们一次性创建多个约束,Masonry中与这个约束相关的类是MASCompositeConstraint类,简单使用如下:
edges

// make top, left, bottom, right equal view2
make.edges.equalTo(view2);

// make top = superview.top + 5, left = superview.left + 10,
//      bottom = superview.bottom - 15, right = superview.right - 20
make.edges.equalTo(superview).insets(UIEdgeInsetsMake(5, 10, 15, 20))

size

// make width and height greater than or equal to titleLabel
make.size.greaterThanOrEqualTo(titleLabel)

// make width = superview.width + 100, height = superview.height - 50
make.size.equalTo(superview).sizeOffset(CGSizeMake(100, -50))

center

// make centerX and centerY = button1
make.center.equalTo(button1)

// make centerX = superview.centerX - 5, centerY = superview.centerY + 10
make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10))
6.修改已经存在的约束

当我们只是修改约束的constant的时候,可以使用mas_updateConstraints:

// this is Apple's recommended place for adding/updating constraints
// this method can get called multiple times in response to setNeedsUpdateConstraints
// which can be called by UIKit internally or in your code if you need to trigger an update to your constraints
- (void)updateConstraints {
    [self.growingButton 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);
    }];

    //according to apple super should be called at end of method
    [super updateConstraints];
}

当我们要修改的不止是约束的constant的时候,使用mas_updateConstraints就力不从心了,这时就需要使用mas_remakeConstraints:

- (void)changeButtonPosition {
    [self.button mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.size.equalTo(self.buttonSize);

        if (topLeft) {
        	make.top.and.left.offset(10);
        } else {
        	make.bottom.and.right.offset(-10);
        }
    }];
}

Masonry源码解读

解读源码

我们在解读源码的时候先从最简单最基础的使用开始,然后由浅入深,逐渐深入。下面我们先分析一下整个框架的文件结构:

Masonry源码解读
下面就从一个最简单最基本的使用开始来探究源码:

    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(superview.mas_left).mas_offset(30);
    }];

我们先不管外面的方法调用,只需要知道make是MASConstraintMaker类型的就行了,从make.left.equalTo(superview.mas_left).mas_offset(30);开始:

  • 1.make.left
    left是它的一个属性,这里调用的是属性的getter方法。Masonry源码解读继续往下查看:Masonry源码解读再继续:
    Masonry源码解读我们来看一下MASViewConstraint对象的创建:
    Masonry源码解读

那么现在我们来总结一下make.left做了哪些事:

make.left是调用了MASConstraintMaker类的left属性的get方法,这里创建了一个MASViewAttribute对象,这个对象由一个UIView对象和一个NSLayoutAttribute来创建,这里UIView对象是view,NALayoutAttribute为NSLayoutAttributeLeft,所以这里MASViewAttribute对象也就是封装了约束等号左边的两个元素。。然后使用创建的MASViewAttribute对象来创建了一个MASViewConstraint对象,这个对象代表这一行代码所表示的整个约束。最终make.left返回了一个MASVIewConstraint对象。

需要注意的是,MASConstraintMaker对象有一个数组类型的consrtaints属性,新创建的MASViewConstraint对象被加入到了这个属性中,在最后添加约束的时候会遍历这个数组。

  • 2.superview.mas_left
    mas_left是分类的一个属性,所以superview.mas_left会调用分类的-(MASViewAttribute *)mas_left方法。Masonry源码解读
    这里通过代码创建了一个MASViewAttribute对象,对象的view即superview,对象的attribute即NSLayoutAttributeLeft,我们看看是如何创建的:Masonry源码解读
    总结一下:

superview.mas_left返回了一个MASViewAttribute对象,这个对象封装了约束等号右边的两个元素。

  • 3.make.left.equalTo(superview.mas_left)
    进入equalTo查看具体实现:Masonry源码解读也就是说我执行make.left.equalTo会得到一个Block,那么我执行make.left.equalTo(superview.mas_left)就是执行这个Block,即make.left.equalTo(superview.mas_left)会执行self.equalToWithRelation(attribute, NSLayoutRelationEqual)这一行核心代码,并返回这一行核心代码的返回值。
    由于make.left是MASViewConstraint对象,所以我们要去MASViewConstraint类中查看equalWithRelation的实现:Masonry源码解读self.secondViewAttribute = attribute;会触发secondViewAttribute这个属性的set方法,我们看一下其set方法的实现:
    Masonry源码解读这里的意思就是make.left.equalTo()这个括号里面传入的东西可能有三种情况,第一种是数字,第二种是一个UIView对象,如果是UIView对象,那就将其layoutAttribute设置为何firstAttribute一致,也就是我们也可以这样写:make.left.equalTo(superview),这样Masonry也能成功识别。第三种是传入的MASViewConstraint对象,如果传入这种对象则可以直接赋值给secondViewAttribute属性。
    下面我们再来看一下第一种情况传入数字的处理方式,我们进入setLayoutConstantWithValue:这个方法:
    Masonry源码解读代码里面设置offset,centerOffset等都不是真正的实现,真正的实现在offset属性的set方法里,我们要其MASViewConstraint类中找offset属性的set方法:
    Masonry源码解读总结起来就是如果传入的是数值类型,那么就给MASViewConstraint的layoutConstant属性赋值。

这样我们就清楚了make.left.equalTo()这个括号中传入各种不同类型的值会怎么操作。

总结一下make.left.equalTo(superview.mas_left)做的事情:

make.left创建了一个firstViewAttribute,firstViewAttribute的view属性即为
mas_makeConstraint方法的调用者,其layoutAttribute属性为NSLayoutAttributeLeftfirstViewAttribute封装了约束等式左边的两个item。接着通过传入firstViewAttribute创建了一个MASViewConstraint对象。superview.mas_left则是创建了一个secondViewAttribute对象,该对象的view即为superview,layoutAttributeNSLayoutAttributeLeftmake.left.equalTo(supervie.mas_left)则是将secondViewAttribute赋值给MASViewConstraint对象的secondViewAttribute属性,并给MASViewConstraint对象的layoutRelation属性赋值。

  • 4..mas_offset(30)
    mas_offset的颜色是土黄色,说明这是一个宏定义,我们点进去,发现这个宏定义是定义在MASConstraint.h文件中:
#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))

这不是一个简单的宏定义,里面还进行了嵌套,我们看一下MASBoxValue()方法做了什么,在MASUtilities.h这个文件中找到了MASBoxValue()这个方法:Masonry源码解读从图中我们可以看到,这个方法就是把一些数值类型的值转为id类型。

Masonry的用法这部分我说过mas前缀的使用,这里我们就看到了其实现方法。如果我们括号里想要直接传入数值类型而不是id类型的参数,那么前面使用的API就必须带mas前缀,否则报错,如果传入的是id类型,则前面使用的API是否带mas前缀均可。

所以.mas_offset(30)也就等于.valueOffset(@30),那么我们来查看一下MASConstraint.m中的实现:Masonry源码解读最终就是设置了MASViewConstraint的layoutConstant属性:
Masonry源码解读
到这里make.left.equalTo(superview.mas_left).mas_offset(30);这行代码就全解读完了。总结一下就是:

make,left创建了一个MASViewConstraint对象,为这个对象的firstViewAttribute属性赋值,superview.mas_left即创建了一个MASViewAttribute对象,equalTo()即把这个MASViewAttribute对象赋值给MASViewConstraint对象的secondViewAttribute属性,.mas_offset(30)则是给MASViewConstraint对象的layoutConstant属性赋值为30.

  • 5.mas_makeConstraints:
    mas_makeConstraints:这个方法是在UIView的分类中定义并实现的,下面我们看一下其具体实现:
    Masonry源码解读Masonry源码解读
    再来看[constraint install]:,这个方法的内容比较多,我们分两部分来看:
    Masonry源码解读Masonry源码解读
    到这里约束就添加完成了。 总结一下添加约束的大体流程:

首先根据MASViewConstraint对象的firstViewAttributesecondViewAttribute这两个属性,访问这两个属性得到firstLayoutItemfirstLayoutAttributesecondLayoutitemsecondLayoutAttribute。然后处理有时是对齐属性如left没有提供view的情况,这时就要设置view为其父视图。然后寻找应该将约束添加到哪个视图上,最后添加约束到对应的视图上。

Masonry源码解读

组合约束(MASCompositeConstraint)

Masonry中可以直接约束size,center,edge这样的组合约束。其本质也就是把它拆成多个单个约束,比如对size的约束,就是拆成width和height这两个约束。下面我们看一下其具体的实现方法:

make.size.equalTo(superview).sizeOffset(CGSizeMake(10, 10));
  • 1.make.size
    Masonry源码解读继续往下看:- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs方法:
    Masonry源码解读
    总结一下make.size做了哪些事:

把size这一个拆分成了width和height这两个,根据这两个约束创建了两个MASViewConstraint对象,装到一个数组里面,使用这个数组来创建了一个MASCompositeConstraint对象,最后返回这个MASCompositeConstraint对象。

  • 2..equalTo(superview)Masonry源码解读继续:
    Masonry源码解读
    总结一下make.size.equalTo(superview):

把size这一个约束拆分成了width和height这两个约束,并根据此创建了两个MASViewConstraint对象,根据这两个对象组成的数组去创建一个MASCompositeConstraint对象。然后遍历MASCompositeConstraint对象的childConstraints数组,取出数组里面的额每个MASViewConstraint对象,然后像处理单个MASViewConstraint一样去处理。

  • 3..sizeOffset(CGSizeMake(10, 10))
    可以想象就是把它拆分,然后赋值给两个MASViewConstraint的layoutConstant属性,就不详细说了。

mas_updateConstraints:和mas_remakeConstraints:

自动布局约束等式:
item1.attribute1 = multiplier × item2.attribute2 + constant

有时候我们有更改约束的需求,比如我们要做一个动画,移动某个视图,那就需要改变视图约束,当我们只是改变约束的constant时,可以使用mas_updateConstraints:这个方法。而当我们需要改变的不止constant时,就需要调用mas_remakeConstraints:这个方法了。

先来看一下mas_updateConstraints:这个方法是怎么实现只改变约束的constant的:Masonry源码解读
进入[constraintMaker install]
Masonry源码解读
看看[constraint install]中是怎么实现的:
Masonry源码解读

总结起来,mas_updateConstraints:就是去self.installedView.constarints这个属性数组中去遍历,看有没有这样一个NSLayoutConstraint对象,它除了constant外,所有内容都和当前的NSLayoutConstraint对象一致,如果有这个对象,那么就把该对象的constant改为当前NSLayoutConstraint对象的constant。这样来完成约束条件的更新。

再来看一下mas_remakeConstraints:
当我们要改变的约束条件不止是constant这么简单时,使用mas_remakeConstraints:就不顶用了,就要使用mas_remakeConstraints:这个方法。Masonry源码解读再看[constraintMaker install]:
Masonry源码解读
Masonry源码解读

这篇文章在简书的地址:https://www.jianshu.com/p/8990c5a98d29