Masonry源码分析(下)

作者: YYYYYY25 | 来源:发表于2018-05-16 18:17 被阅读160次
    前言

    在上一篇-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约束封装成对象,然后加到约束数组中。

    在这个方法中,可以解释我们之前留下的问题:为什么view属性要用weak修饰。 view -> mas_makeConstraints: -> block -> view 循环引用

    因为在这个代理方法内部,通过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

    本文参考:SnapKit/Masonry 源码

    相关文章

      网友评论

        本文标题:Masonry源码分析(下)

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