源码分析--Masonry

Masonry

[Masonry] 是 Objective-C 中用于自动布局的第三方框架, 我们一般使用它来代替冗长, 繁琐的 AutoLayout 代码. 

框架的使用:

[view mas_makeConstraints:^(MASConstraintMaker *make) {
	make.centerX.equalTo(self.view);
	make.top.equalTo(self.view).with.offset(40);
	make.width.equalTo(@185);
	make.height.equalTo(@38);
}];

mas_makeConstraints:

最常用的方法

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;

同样, 也有用于'更新和重新构建'约束的分类方法:

- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

Constraint Maker Block

以 `mas_makeConstraints:` 方法为入口来分析一下 Masonry 以及类似的框架(SnapKit)是如何工作的. `mas_makeConstraints:` 方法位于 `UIView` 的分类 `MASAdditions` 中.

方法的实现

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

因为 Masonry 是封装的苹果的 AutoLayout 框架, 所以我们要在为视图添加约束前将 `translatesAutoresizingMaskIntoConstraints` 属性设置为 `NO`. 如果这个属性没有被正确设置, 那么视图的约束不会被成功添加.

在设置 `translatesAutoresizingMaskIntoConstraints` 属性之后, 

* 我们会初始化一个 `MASConstraintMaker` 的实例.

* 然后将 maker 传入 block 配置其属性.

* 最后调用 maker 的 `install` 方法为视图添加约束.

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    self.view = view;
    self.constraints = NSMutableArray.new;
    return self;
}

Setup MASConstraintMaker

在调用 `block(constraintMaker)` 时, 实际上是对 `constraintMaker` 的配置.

make.centerX.equalTo(self.view);
make.top.equalTo(self.view).with.offset(40);
make.width.equalTo(@185);
make.height.equalTo(@38);

访问 `make` 的 `left` `right` `top` `bottom`  等属性时, 会调用 `constraint:addConstraintWithLayoutAttribute:` 方法.

 - (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}

- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}

- (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]) { ... }
    if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }
    return newConstraint;
}

在调用链上最终会达到 `constraint:addConstraintWithLayoutAttribute:` 这一方法, 在这里省略了一些暂时不需要了解的问题. 因为在这个类中传入该方法的第一个参数一直为 `nil`, 所以这里省略的代码不会执行.

这部分代码会先以布局属性 `left` 和视图本身初始化一个 `MASViewAttribute` 的实例, 之后使用 `MASViewAttribute` 的实例初始化一个 `constraint` 并设置它的代理, 加入数组, 然后返回.

这些工作就是你在输入 `make.left` 进行的全部工作, 它会返回一个 `MASConstraint`, 用于之后的继续配置.

在 `make.left` 返回 `MASConstraint` 之后, 我们会继续在这个链式的语法中调用下一个方法来指定约束的关系.

- (MASConstraint * (^)(id attr))equalTo;
- (MASConstraint * (^)(id attr))greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))lessThanOrEqualTo;

这三个方法是在 `MASViewConstraint` 的父类, `MASConstraint` 中定义的.

`MASConstraint` 是一个抽象类, 其中有很多的方法都**必须在子类中覆写**的. Masonry 中有两个 `MASConstraint` 的子类, 分别是 `MASViewConstraint` 和 `MASCompositeConstraint`. 后者实际上是一些**约束的集合**. 这么设计的原因我们会在 post 的最后解释.先来看一下这三个方法是怎么实现的:

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

该方法会导致 `self.equalToWithRelation` 的执行, 而这个方法是定义在子类中的, 因为父类作为抽象类没有提供这个方法的具体实现.

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); }

`MASMethodNotImplemented` 也是一个宏定义, 用于在**子类未继承这个方法**或者**直接使用这个类**时抛出异常.

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
                                 userInfo:nil]

因为我们为 `equalTo` 提供了参数 `attribute` 和布局关系 `NSLayoutRelationEqual`, 这两个参数会传递到 `equalToWithRelation` 中, 设置 `constraint` 的布局关系和 `secondViewAttribute` 属性, 为即将 maker 的 `install` 做准备.

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) { ... } 
        else {
            ...
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

我们不得不提一下 `setSecondViewAttribute:` 方法, 它并不只是一个简单的 setter 方法, 它会根据你传入的值的种类赋值.

 - (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];

    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        _secondViewAttribute = secondViewAttribute;
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}

  

 

第一种情况对应的就是:

make.left.equalTo(@40);

传入 `NSValue` 的时, 会直接设置 `constraint` 的 `offset`, `centerOffset`, `sizeOffset`, 或者 `insets`

 

第二种情况一般会直接传入一个视图:

make.left.equalTo(view);

 这时, 就会初始化一个 `layoutAttribute` 属性与 `firstViewArribute` 相同的 `MASViewAttribute`, 上面的代码就会使视图与 `view` 左对齐.

 

第三种情况会传入一个视图的 `MASViewAttribute`:

make.left.equalTo(view.mas_right);

使用这种写法时, 一般是因为约束的方向不同. 这行代码会使视图的左侧与 `view` 的右侧对齐.

 

到这里我们就基本完成了对**一个**约束的配置, 接下来可以使用相同的语法完成对一个视图上所有约束进行配置, 然后进入了最后一个环节.

Install MASConstraintMaker

我们会在 `mas_makeConstraints:` 方法的最后调用 `[constraintMaker install]` 方法来安装所有存储在 `self.constraints` 数组中的所有约束.

- (NSArray *)install {
    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;
}

在这个方法会先判断当前的视图的约束是否应该要被 `uninstall`, 如果我们在最开始调用 `mas_remakeConstraints:` 方法时, 视图中原来的约束就会全部被 `uninstall`.

然后就会遍历 `constraints` 数组, 发送 `install` 消息.

MASViewConstraint install

MASViewConstraint 的 `install` 方法就是最后为当前视图添加约束的最后的方法, 首先这个方法会先获取即将用于初始化 `NSLayoutConstraint` 的子类的几个属性.

MAS_VIEW *firstLayoutItem = self.firstViewAttribute.view;
NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
MAS_VIEW *secondLayoutItem = self.secondViewAttribute.view;
NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; 

Masonry 之后会判断当前即将添加的约束是否是 size 类型的约束

if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
   secondLayoutItem = firstLayoutItem.superview;
   secondLayoutAttribute = firstLayoutAttribute;
}

如果不是 size 类型并且没有提供第二个 `viewAttribute`, (e.g. `make.left.equalTo(@10);`) 会自动将约束添加到 `superview` 上. 它等价于:

make.left.equalTo(superView.mas_left).with.offset(10);

 

 

然后就会初始化 `NSLayoutConstraint` 的子类 `MASLayoutConstraint`:

MASLayoutConstraint *layoutConstraint
   = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                   attribute:firstLayoutAttribute
                                   relatedBy:self.layoutRelation
                                      toItem:secondLayoutItem
                                   attribute:secondLayoutAttribute
                                  multiplier:self.layoutMultiplier
                                    constant:self.layoutConstant];
layoutConstraint.priority = self.layoutPriority;    

  

 

接下来它会寻找 `firstLayoutItem` 和 `secondLayoutItem` 两个视图的公共 `superview`, 相当于求两个数的最小公倍数. 

- (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` 安装约束.

MASLayoutConstraint *existingConstraint = nil;
if (self.updateExisting) {
   existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
}

if (existingConstraint) {
   existingConstraint.constant = layoutConstraint.constant;
   self.layoutConstraint = existingConstraint;
} else {
   [self.installedView addConstraint:layoutConstraint];
   self.layoutConstraint = layoutConstraint;
}
[firstLayoutItem.mas_installedConstraints addObject:self];

如果原来的 `view` 中不存在可以升级的约束, 或者没有调用 `mas_updateConstraint:` 方法, 那么就会在上一步寻找到的 `installedView` 上面添加约束. 

[self.installedView addConstraint:layoutConstraint]; 

  

  

  

 

posted @ 2017-05-12 14:27  _NeverMore  阅读(349)  评论(0编辑  收藏  举报