美文网首页
iOS Masonry源码分析

iOS Masonry源码分析

作者: 某非著名程序员 | 来源:发表于2019-11-03 09:15 被阅读0次

    带着问题去思考,更加深入的理解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,建议先把链式理解。

    相关文章

      网友评论

          本文标题:iOS Masonry源码分析

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