美文网首页
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,建议先把链式理解。

相关文章

  • Masonry框架源码分析

    Masonry框架源码分析 相信大多数iOS开发者对Masonry框架并不陌生 , 本文是笔者通读Masonry的...

  • Masonry

    iOS开发之Masonry框架源码解析

  • iOS Masonry源码分析

    带着问题去思考,更加深入的理解Masonry。我们来看看Masonry常见的问题。 问题目录:1.没有添加view...

  • Masonry是如何适配iOS11的

    iOS 源代码分析 --- Masonry 感谢:译者: @One @Draveness Masonry 是 Ob...

  • App架构方方面面

    布局 揭秘 iOS 布局 Masonry源码解析 自动布局&绝对布局autolayoutautolayout 动画...

  • Masonry分析

    iOS 源代码分析----Masonry Masonry是OC自动布局的框架,简化了AutoLayout的写法。 ...

  • Masonry 源码分析

    Masonry 提供了简单方便的api ,供我们完成项目中的自动布局业务。 从使用的 api 开始讲 调用mas_...

  • Masonry源码分析

    Masonry是iOS在控件布局中经常使用的一个轻量级框架,Masonry让NSLayoutConstraint使...

  • Masonry源码分析

    Masonry源码其实非常简单,就是对AutoLayout的简单封装,但是使用中有几处要注意的地方,所以现在就Ma...

  • Masonry源码分析

    iOS 源代码分析 --- Masonry Masonry 是 Objective-C 中用于自动布局的第三方框架...

网友评论

      本文标题:iOS Masonry源码分析

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