iOS Masonry源码分析

带着问题去思考,更加深入的理解Masonry。我们来看看Masonry常见的问题。

问题目录:
1.没有添加view,就使用了masonry布局。为什么会崩溃?
2.调用left.equalTo方法后不能继续设置width?
3.为什么mas_updateConstraints只能对已存在的约束更新?如果是不存在的约束控制台会为什么会报错?
4.1同一个view使用多个mas_makeConstraints会不会有问题?为什么?
4.2 接着问题4.1,什么时候需要分开写呢?
5.make约束完后什么时候能取到正确的frame?
6.怎么获取已经使用约束的值?
7.使用mas_makeConstraints为什么不需要__weak引用?

问题1

1.1没有添加view,就使用了masonry布局。

UIView * view = [[UIView alloc] init];
    view.backgroundColor = [UIColor redColor];
//    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(10);
        make.right.equalTo(self.view).offset(-10);
        make.top.equalTo(self.view).offset(10);
        make.bottom.equalTo(self.view).offset(-10);
    }];

1.2原因

closestCommonSuperview

closestCommonSuperview不能为空,作者用NSAssert方式让使用者更好调试。

1.3 mas_closestCommonSuperview

- (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view {
    MAS_VIEW *closestCommonSuperview = nil;

    MAS_VIEW *secondViewSuperview = view;
    while (!closestCommonSuperview && secondViewSuperview) {
        MAS_VIEW *firstViewSuperview = self;
        while (!closestCommonSuperview && firstViewSuperview) {
            if (secondViewSuperview == firstViewSuperview) {
                closestCommonSuperview = secondViewSuperview;
            }
            firstViewSuperview = firstViewSuperview.superview;
        }
        secondViewSuperview = secondViewSuperview.superview;
    }
    return closestCommonSuperview;
}

这个方法是查找约束view与添加的view父视图。
如[self.view addSubview:view];则closestCommonSuperview返回的是self.view;而不添加,则是查找view与nil的父视图,直接返回为空。

问题2

2.调用left.equalTo方法后不能继续设置width

//错误写法
make.left.equalTo(self.view).offset(10).width.mas_equalTo(ScreenWidth-20);
//正确写法
make.left.equalTo(self.view).offset(10);
make.width.mas_equalTo(ScreenWidth-20);

错误写法

需要了解hasLayoutRelation为什么是NO,正常添加width布局时为什么是YES?
从逻辑上看Masonry支持链式,错误写法在语法上没有错误。
看下面这段源码:

- (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;
}

这段代码在#pragma mark - standard Attributes的方法都会调用,如make.left、make.center。
每一条make.语法都会产生一个新的MASViewConstraint对象。

- (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;
        }
    };
}

- (void)setLayoutRelation:(NSLayoutRelation)layoutRelation {
    _layoutRelation = layoutRelation;
    self.hasLayoutRelation = YES;
}
  1. equalToWithRelation返回的是MASViewConstraint对象
  2. 而每一条MASViewConstraint只要设置了equalTo(包括mas_equalTo等,只要调用了self.equalToWithRelation的block)方法就会把self.hasLayoutRelation置为YES.
  3. 而接着调用width不是MASConstraintMaker中的width方法,而是MASViewConstraint的width方法。
    所以在调用equalToWithRelation方法后,要设置left、width等属性,需要重新make.语法。

问题3

3.为什么mas_updateConstraints只能对已存在的约束更新?如果是不存在的约束控制台会为什么会报错?

- (void)install {
    ...

    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];
    }
}

//判断MASLayoutConstraint属性是否相等
- (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint {
    // check if any constraints are the same apart from the only mutable property constant

    // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints
    // and they are likely to be added first.
    for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) {
        if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue;
        if (existingConstraint.firstItem != layoutConstraint.firstItem) continue;
        if (existingConstraint.secondItem != layoutConstraint.secondItem) continue;
        if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue;
        if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue;
        if (existingConstraint.relation != layoutConstraint.relation) continue;
        if (existingConstraint.multiplier != layoutConstraint.multiplier) continue;
        if (existingConstraint.priority != layoutConstraint.priority) continue;

        return (id)existingConstraint;
    }
    return nil;
}
  1. mas_updateConstraints中有个变量updateExisting标识是否更新,逆序查找self.installedView.constraints,所有属性都相等时返回existingConstraint.
  2. existingConstraint.constant = layoutConstraint.constant;这段就是更新已存在约束
  3. 假如updateExisting为YES,而existingConstraint不存在,则走了[self.installedView addConstraint:layoutConstraint]。导致view的约束会报错。这段应该是个bug,可以用下面代码修复:
MASLayoutConstraint *existingConstraint = nil;
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
        if (existingConstraint) {
            // just update the constant
            existingConstraint.constant = layoutConstraint.constant;
            self.layoutConstraint = existingConstraint;
        }
        NSAssert(existingConstraint != nil, @"existingConstraint 不存在,请检查约束");
    }else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }

错误的约束应该让开发者去处理,在项目复杂的时候,看到控制台大量约束错误,却不知道是那个view约束报错,这是令人头疼的事情。

问题4

4.1同一个view使用多个mas_makeConstraints会不会有问题?为什么?

    UIView * view = [[UIView alloc] init];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(10);
        make.width.mas_equalTo(ScreenWidth-20);
    }];
    
    [view mas_makeConstraints:^(MASConstraintMaker *make){
        make.top.equalTo(self.view).offset(10);
        make.bottom.equalTo(self.view).offset(-10);
    }];

答案是不会有问题。

- (void)install {
   ...
    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;
        }
        NSAssert(existingConstraint != nil, @"existingConstraint 不存在,请检查约束");
    }else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
    
}
  1. self.installedView是view与self.view的第一个共同的view。
  2. 两次mas_makeConstraints产生了两个MASConstraintMaker对象,但view与self.view没有变,所以两次查找的self.installedView是相同的。
  3. make.left、make.width、make.top、make.bottom都是添加到self.installedView上。
    综上:两个mas_makeConstraints的与一个mas_makeConstraints效果上是一样的,而有时候就需要分开写。

4.2 接着问题4.1,什么时候需要分开写呢?

    UIView * view = [[UIView alloc] init];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
    
    UIView * view2 = [[UIView alloc] init];
    view2.backgroundColor = [UIColor greenColor];
    [self.view addSubview:view2];
    
    [@[view,view2] mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view).offset(10);
        make.width.mas_equalTo(ScreenWidth-20);
        make.height.mas_equalTo(200);
    }];
    
    [view mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view).offset(10);
    }];
    
    [view2 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(view.mas_bottom).offset(10);
    }];

假如有多个view其中大部分属性是相同的,但又有细微差异。我们可以统一约束相同部分,再针对不同部分单独约束。

问题5

5.make约束完后什么时候能取到正确的frame?

需要理解的:Masonry实际是调用了系统的constraintWithItem,而系统的方法最终都会转成frame.约束完后不会立即刷新界面,需要等待下一个runloop才能刷新。如果在make后直接获取frame,frame是不正确的。

  1. 在viewController中viewDidLayoutSubviews中可正确获取frame
  2. 调用layoutIfNeeded方法后可正确获取frame

问题6

6.怎么获取已经使用约束的值

每一条make语句都会生成MASViewConstraint,layoutConstant就是每条约束的值。下面是获取约束的值:

#import "UIView+Masonry.h"
#import "Masonry.h"

@implementation UIView (Masonry)

//获取约束的值
- (CGFloat)getOffsetWithAttribute:(NSLayoutAttribute)attribute{
    NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self];
    for (MASViewConstraint * constraint in installedConstraints) {
        MASViewAttribute * firstViewAttribute = constraint.firstViewAttribute;
        if (firstViewAttribute.layoutAttribute == attribute) {
            CGFloat offset = [[constraint valueForKey:@"layoutConstant"] floatValue];
            return offset;
        }
    }
    return 0;
}

@end

问题7

7.使用mas_makeConstraints为什么不需要__weak引用?

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

这个问题很常见,但见到很多人都不理解。block是个局部变量,不会产生循环引用。

总结:

  1. 带着问题去查看源码,比直接阅读第三方源码理解的更加透彻。
  2. 如果想更透彻的理解Masonry,建议先把链式理解。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容