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原因

iOS Masonry源码分析_第1张图片
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);

iOS Masonry源码分析_第2张图片
错误写法

需要了解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,建议先把链式理解。

你可能感兴趣的:(iOS Masonry源码分析)