Masonry源码分析

版本:v1.1.0

github链接

引子

首先我们根据较为完整的使用,与NSLayoutConstraints作对比,然后再逐步分析Masonry实现自动布局的过程。

       UIView *superview = self.view;
    UIView *view1 = [[UIView alloc] init];
    view1.backgroundColor = UIColor.redColor;
    [superview addSubview:view1];
    
    //Masonry布局
    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);
    }];
    
    //NSLayoutConstraint布局
    [superview addConstraints:@[
        //view1 constraints
        [NSLayoutConstraint constraintWithItem:view1
                                     attribute:NSLayoutAttributeTop
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:superview
                                     attribute:NSLayoutAttributeTop
                                    multiplier:1.0
                                      constant:padding.top],

        [NSLayoutConstraint constraintWithItem:view1
                                     attribute:NSLayoutAttributeLeft
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:superview
                                     attribute:NSLayoutAttributeLeft
                                    multiplier:1.0
                                      constant:padding.left],

        [NSLayoutConstraint constraintWithItem:view1
                                     attribute:NSLayoutAttributeBottom
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:superview
                                     attribute:NSLayoutAttributeBottom
                                    multiplier:1.0
                                      constant:-padding.bottom],

        [NSLayoutConstraint constraintWithItem:view1
                                     attribute:NSLayoutAttributeRight
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:superview
                                     attribute:NSLayoutAttributeRight
                                    multiplier:1
                                      constant:-padding.right],
     ]];

从布局代码量可以看出,使用Masonry布局是简洁很多的,可读性也较高。

当然,Masonry是通过链式调用的方式简化了代码,实际上最后也是转化为NSLayoutConstraint布局(下文会具体分析)。

分析过程

我们从上面的mas_makeConstraints布局角度出发,分析Masonry如何做到如此简洁地完成一个view的约束。主要分析步骤为

一、给谁做约束

二、如何组成约束

三、如何完成约束

一、给谁做约束

通过查看mas_makeConstraints方法的定义可知,约束对象类型为MAS_VIEW,在MASUtilities.h文件中可以找到其宏定义。

#if TARGET_OS_IPHONE || TARGET_OS_TV
    #define MAS_VIEW UIView
#elif TARGET_OS_MAC
    #define MAS_VIEW NSView
#endif

这里我们仅针对iOS架构来分析,可以得出:是给UIView做约束

二、如何组成约束

我们先来分析这一句代码,研究各个链式调用是如何最终组成一句约束的,我们可以先从make入手

make.left.equalTo(superview.mas_left).with.offset(padding.left);

(1)make是何物?

mas_makeConstraints调用得知,make为MASConstraintMaker类型对象,可以理解为协助创建约束的类。那其对应的topleftright等等属性是什么概念呢?

(2)MASConstraintMaker的topleft等属性

① 查看定义得知,这些属性定位在MASConstraintMaker类中,返回了MASConstraint类型

    @property (nonatomic, strong, readonly) MASConstraint *left;
    - (MASConstraint *)left {
      return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
  }
  
  - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
    }

②以left为例,实际的操作为,构造一个MASViewConstraint(继承自MASConstraint)对象并返回,其firstViewAttributeitem为当前做约束的view,layoutAttribute为对应的NSLayoutAttributeLeft

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        //replace with composite constraint
        NSArray *children = @[constraint, newConstraint];
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

如上我们得到MASConstraint类型对象,可以算是确认了需要创建约束的具体属性,然后将该约束添加到constraints数组属性中。

接下来就是equalTo(superview.mas_left).with.offset(padding.left);操作,我们先来研究MASConstraintequalTo(...) / mas_equalTo(...)操作。

(3)MASViewConstraint的equalTo(...) / mas_equalTo(...)方法

#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define equalTo(...)                     mas_equalTo(__VA_ARGS__)

- (MASConstraint * (^)(id))mas_equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}

//MASViewConstraint的equalToWithRelation方法
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            //……
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

该方法返回了一个block,传入attribute返回MASConstraint类型对象。

此方法确认了layoutRelation为NSLayoutRelationEqual, 而传入的参数(superview.mas_left)即为secondViewAttribute

(4)MASViewConstraintwith/and方法

- (MASConstraint *)with {
    return self;
}

- (MASConstraint *)and {
    return self;
}

这两个方法返回了self,其实的起到了连接语义的作用,一般可以省略。接下来我们看看offset方法

(5)MASViewConstraintoffset(...)/mas_offset(...)方法

#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))
#define offset(...)                      mas_offset(__VA_ARGS__)

- (MASConstraint * (^)(NSValue *value))valueOffset {
    return ^id(NSValue *offset) {
        NSAssert([offset isKindOfClass:NSValue.class], @"expected an NSValue offset, got: %@", offset);
        [self setLayoutConstantWithValue:offset];
        return self;
    };
}

- (void)setLayoutConstantWithValue:(NSValue *)value {
    if ([value isKindOfClass:NSNumber.class]) {
        self.offset = [(NSNumber *)value doubleValue];
    } else if (strcmp(value.objCType, @encode(CGPoint)) == 0) {
        CGPoint point;
        [value getValue:&point];
        self.centerOffset = point;
    } else if (strcmp(value.objCType, @encode(CGSize)) == 0) {
        CGSize size;
        [value getValue:&size];
        self.sizeOffset = size;
    } else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets insets;
        [value getValue:&insets];
        self.insets = insets;
    } else {
        NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value);
    }
}

- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}

可以看出offset方法会根据传入的类型,去设置offset/centerOffset/sizeOffset/insets属性。而设置这些属性,最后都会确认对应约束属性的layoutConstant

(6)MASViewConstraintmultipliedBy(...)方法

- (MASConstraint * (^)(CGFloat))multipliedBy {
    return ^id(CGFloat multiplier) {
        NSAssert(!self.hasBeenInstalled,
                 @"Cannot modify constraint multiplier after it has been installed");
        
        self.layoutMultiplier = multiplier;
        return self;
    };
}

因为MASViewConstraint对象初始化时layoutMultiplier为1.0时,所以layoutMultiplier为1.0时常常不写。这里也就是将layoutMultiplier属性赋值。

(7)总结

make.left.equalTo(superview.mas_left).with.offset(padding.left);

至此,这一整句代码的流程就可以总结为:

设置MASViewConstraint对象的以下属性

firstViewAttribute.item = view1
firstViewAttribute.layoutAttribute = NSLayoutAttributeLeft
layoutRelation = NSLayoutRelationEqual
secondViewAttribute.item = superview
secondViewAttribute.layoutAttribute = NSLayoutAttributeLeft
layoutMultiplier = 1.0
layoutConstant = padding.left

对比以下代码,有没有感觉和NSLayoutConstraint写约束时的参数一模一样

[NSLayoutConstraint constraintWithItem:view1
                             attribute:NSLayoutAttributeLeft
                             relatedBy:NSLayoutRelationEqual
                                toItem:superview
                             attribute:NSLayoutAttributeLeft
                            multiplier:1.0
                              constant:padding.left],

有了这些属性之后,Masonry自然就可以通过mas_makeConstraints/mas_updateConstraints/mas_remakeConstraints方法进行添加/更新/移除后添加相应约束。

接下来我们通过分析mas_makeConstraints方法,了解Masonry如何完成约束

三、如何完成约束

我们先来看mas_makeConstraints的实现

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    //translatesAutoresizingMaskIntoConstraints 默认情况下,视图上的自动调整掩码会产生完全确定的约束视图的位置。这允许自动布局系统跟踪视图的帧。布局是手动控制的(例如,通过-setFrame:)。当您选择通过添加自己的约束来使用自动布局来定位视图时,您必须将此属性设置为NO
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

可以看到该方法首先创建了constraintMaker,然后调用block,也就是设置并添加了block中的每一条约束到constraints中,然后返回[constraintMaker install],我们就来研究MASConstraintMaker的install方法

(1)MASConstraintMaker的install方法

- (NSArray *)install {
    //removeExisting在mas_remakeConstraints情况下为YES
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    NSArray *constraints = self.constraints.copy;
    for (MASConstraint *constraint in constraints) {
        constraint.updateExisting = self.updateExisting;
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

①首先,如果调用的是mas_remakeConstraints会先卸载之前的约束

②将constraints中的每条约束执行install,然后清空constraints。此时我们应该查看MASConstraint的install方法

(2)MASConstraint的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;

    // alignment attributes must have a secondViewAttribute
    // therefore we assume that is refering to superview
    // eg make.left.equalTo(@10)
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;
        secondLayoutAttribute = firstLayoutAttribute;
    }
    
    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;
    
    if (self.secondViewAttribute.view) {
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        NSAssert(closestCommonSuperview,
                 @"couldn't find a common superview for %@ and %@",
                 self.firstViewAttribute.view, self.secondViewAttribute.view);
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) {
        self.installedView = self.firstViewAttribute.view;
    } else {
        self.installedView = self.firstViewAttribute.view.superview;
    }


    MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }
    if (existingConstraint) {
        // just update the constant
        existingConstraint.constant = layoutConstraint.constant;
        self.layoutConstraint = existingConstraint;
    } else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

代码较多,这里就简单总结一下。

① 如果secondViewAttribute =nil,且约束属性不为width和height时,默认设置secondLayoutItem为view.superview、secondLayoutAttribute为firstViewAttribute.layoutAttribute。eg make.left.equalTo(@10)

②用对应的属性构造了MASLayoutConstraint约束对象

③确定要设置安装约束的installedView为 (1)最近的公共祖先view (2)宽高属性约束时,为firstViewAttribute.view (3)firstViewAttribute.view.superview

④判断是否是更新约束,是则更新,不是则installedView添加该约束([self.installedView addConstraint:layoutConstraint];)

因为MASLayoutConstraint继承自NSLayoutConstraint,所以这里installedView(MAS_VIEW类型)的addConstraint方法,其实也就是UIView的addConstraint方法,也就成功地为installedView添加上了该约束。

//Adds a constraint on the layout of the receiving view or its subviews.
- (void)addConstraint:(NSLayoutConstraint *)constraint API_AVAILABLE(ios(6.0)); // This method will be deprecated in a future release and should be avoided.  Instead, set NSLayoutConstraint's active property to YES.

总结

(1)Masonry添加约束流程总结

通过block中的每一行创建一个约束MASViewConstraint,添加到约束数组constraints中,通过install方法逐条添加到对应的installedView上。

(2)equalTo和mas_equalTo、offset和mas_offset功能基本相同,只不过mas_equalTomas_offset添加了对数字字面量的支持(即_MASBoxValue)。

        make.height.equalTo(123.0);     //error: Passing 'double' to parameter of incompatible type '__strong id'
        make.height.mas_equalTo(123.0);

(3)如果约束属性不为宽高,且equalTo/mas_equalTo的对象是父view的相同属性,可省略mas_equalTo。(即不写equalTo/mas_equalTo,默认相对父view布局)

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

推荐阅读更多精彩内容