Masonry源码解读

416 阅读12分钟

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类型的属性的声明,反而是看到了一些平时见的比较少的方法的声明:
    0D223676-9F5F-4D4E-AAE0-05FA01F25A5F.png
    也就是说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);
        }
    }];
}

休息一下

解读源码

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

E4C13061-A282-4C44-9626-B797EE011C74.png
下面就从一个最简单最基本的使用开始来探究源码:

    [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方法。
    57BE757D-EE05-47E3-9F2D-4A58A3F2712A.png
    继续往下查看:
    668FACAD-700E-4583-82AB-867A76F46A5D.png
    再继续:
    20ED5488-141A-442A-A8D5-3A7798A4486F.png
    我们来看一下MASViewConstraint对象的创建:
    2728C00E-9170-4905-BCB0-82CD9C91ACAF.png

那么现在我们来总结一下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方法。
    9413264B-AB4F-4F1B-921A-153DE3B96E93.png
    这里通过代码创建了一个MASViewAttribute对象,对象的view即superview,对象的attribute即NSLayoutAttributeLeft,我们看看是如何创建的:
    892F2158-3377-4352-8EFF-B5937A412744.png
    总结一下:

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

  • 3.make.left.equalTo(superview.mas_left) 进入equalTo查看具体实现:
    F8189FD9-D511-48B3-95B5-CBA04C2DE78C.png
    也就是说我执行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的实现:
    4D84193E-0A50-425E-8CF8-297CCB1B53B8.png
    self.secondViewAttribute = attribute;会触发secondViewAttribute这个属性的set方法,我们看一下其set方法的实现:
    1C0A9CAA-0874-49C3-9E93-F126A4428219.png
    这里的意思就是make.left.equalTo()这个括号里面传入的东西可能有三种情况,第一种是数字,第二种是一个UIView对象,如果是UIView对象,那就将其layoutAttribute设置为何firstAttribute一致,也就是我们也可以这样写:make.left.equalTo(superview),这样Masonry也能成功识别。第三种是传入的MASViewConstraint对象,如果传入这种对象则可以直接赋值给secondViewAttribute属性。 下面我们再来看一下第一种情况传入数字的处理方式,我们进入setLayoutConstantWithValue:这个方法:
    B7C31203-51F7-4E48-80ED-4F3B58AC9D31.png
    代码里面设置offset,centerOffset等都不是真正的实现,真正的实现在offset属性的set方法里,我们要其MASViewConstraint类中找offset属性的set方法:
    C66A8756-CD7D-418C-838F-4130A72E8A0E.png
    总结起来就是如果传入的是数值类型,那么就给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()这个方法:

AF2FEF9E-81D3-4CAC-A6AB-79BBED81485C.png
从图中我们可以看到,这个方法就是把一些数值类型的值转为id类型。

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

所以.mas_offset(30)也就等于.valueOffset(@30),那么我们来查看一下MASConstraint.m中的实现:

9EDB3756-844B-48C8-BF55-1DF70D10A560.png
最终就是设置了MASViewConstraint的layoutConstant属性:
85F60147-C2E5-4A0B-9AB7-E96B8BD51130.png
到这里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的分类中定义并实现的,下面我们看一下其具体实现:
    48B3AC46-BE1A-4FEE-98E2-252900F394F3.png
    72B1DEFF-9089-41C5-A215-C070C0781C33.png
    再来看[constraint install]:,这个方法的内容比较多,我们分两部分来看:
    0735EF28-E952-4C4F-8F20-7DA742D42D64.png
    C50DD5C3-64C0-4E40-86F2-B18311F7979C.png
    到这里约束就添加完成了。 总结一下添加约束的大体流程:

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

休息一下

组合约束(MASCompositeConstraint)

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

make.size.equalTo(superview).sizeOffset(CGSizeMake(10, 10));
  • 1.make.size
    44B0B9CD-B912-44A6-8C8B-F07275FDFAE9.png
    继续往下看:- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs方法:
    081C430A-BD90-4FC8-9D62-4FDC07D0EA6B.png
    总结一下make.size做了哪些事:

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

  • 2..equalTo(superview)
    D5C22579-A50F-44BC-A79B-5815FB5FC96D.png
    继续:
    5FDF1EB1-ED9A-4AD4-8EA8-B935A7AF5D5F.png
    总结一下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的:

CEDE51E1-CB87-42C7-9E40-A3AFA01A4755.png
进入[constraintMaker install]
99A0DCCC-96D2-455F-B0E2-EE71B5745FD0.png
看看[constraint install]中是怎么实现的:
2CE11985-5A8F-4701-939C-AB16EA8B56EA.png

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

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

F6106353-8D44-4F85-BE0C-1E8E8CE7D9A0.png
再看[constraintMaker install]:
5D7151E9-43FE-4E11-8AB2-CE965A40B190.png
7AAA699D-C641-48AD-83D8-DA855ADE7437.png

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