Masonry源码分析(下)

前言

在上一篇-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 源码

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容