Masonry源码解读

作者: 雪山飞狐_91ae | 来源:发表于2018-12-28 18:52 被阅读376次

    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
    这篇文章在简书的地址:https://www.jianshu.com/p/8990c5a98d29

    相关文章

      网友评论

        本文标题:Masonry源码解读

        本文链接:https://www.haomeiwen.com/subject/skpdlqtx.html