iOS 源代码分析 -- Masonry

文章来自iOS 源代码分析 — Masonry

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

Masonry 的使用还是很简洁的:

[button 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: 开始

其中最常用的方法就是

// View+MASAdditions.h

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

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

// View+MASAdditions.h

- (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 中.

Provides constraint maker block and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs.

这个分类为我们提供一种非常便捷的方法来配置 MASConstraintMaker, 并为视图添加 mas_left mas_right 等属性.

方法的实现如下:

// View+MASAdditions.m

- (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 方法为视图添加约束.

MASConstraintMaker

MASConstraintMaker 为我们提供了工厂方法来创建 MASConstraint. 所有的约束都会被收集直到它们最后调用 install 方法添加到视图上.

Provides factory methods for creating MASConstraints. Constraints are collected until they are ready to be installed

在初始化 MASConstraintMaker 的实例时, 它会持有一个对应 view 的弱引用, 并初始化一个 constraints 的空可变数组用来之后配置属性时持有所有的约束.

// MASConstraintMaker.m

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

这里的 MAS_VIEW 是一个宏, 是 UIView 的 alias.

// MASUtilities.h

#define MAS_VIEW UIView

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

访问 makeleft right top bottom 等属性时, 会调用 constraint:addConstraintWithLayoutAttribute: 方法.

// MASViewConstraint.m

- (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.equalTo(@80)

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

// MASConstraint.h

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

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

MASConstraint 是一个抽象类, 其中有很多的方法都必须在子类中覆写的. Masonry 中有两个 MASConstraint 的子类, 分别是 MASViewConstraintMASCompositeConstraint. 后者实际上是一些约束的集合. 这么设计的原因我们会在 post 的最后解释.

先来看一下这三个方法是怎么实现的:

// MASConstraint.m

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

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

// MASConstraint.m

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

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

// MASConstraint.m

#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 做准备.

// MASViewConstraint.m

- (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 方法, 它会根据你传入的值的种类赋值.

// MASConstraintMaker.m

- (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 的时, 会直接设置 constraintoffset, 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 数组中的所有约束.

// MASConstraintMaker.m

- (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 的子类的几个属性.

// MASViewConstraint.m

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

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

// MASViewConstraint.m

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:

// MASViewConstraint.m

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

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

// View+MASAdditions.m

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

// MASViewConstraint.m

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

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

[self.installedView addConstraint:layoutConstraint];

其他问题

到现在为止整个 Masonry 为视图添加约束的过程就已经完成了, 然而我们还有一些待解决的其它问题.

make.left.equal(view).with.offset(30)

我们在前面的讨论中已经讨论了这个链式语法的前半部分, 但是在使用中也会”延长”这个链式语句, 比如添加 with offset.

其实在 Masonry 中使用 with 并不是必须的, 它的作用仅仅是使代码更加的易读.

Optional semantic property which has no effect but improves the readability of constraint

// MASConstraint.m
- (MASConstraint *)with {
    return self;
}

- (MASConstraint *)and {
    return self;
}

with 有着相同作用的还有 and, 这两个方法都会直接返回 MASConstraint, 方法本身不做任何的修改.

offset 方法其实是修改 layoutConstraint 中的常量, 因为 self.layoutConstant 在初始化时会被设置为 0, 我们可以通过修改 offset 属性来改变它.

// MASViewConstraint.m

- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}

MASCompositeConstraint

MASCompositeConstraint 是一些 MASConstraint 的集合, 它能够提供一种更加便捷的方法同时为一个视图来添加多个约束.

A group of MASConstraint objects

通过 make 直接调用 edges size center 时, 就会产生一个 MASCompositeConstraint 的实例, 而这个实例会初始化所有对应的单独的约束.

// MASConstraintMaker.m

- (MASConstraint *)edges {
    return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom];
}

- (MASConstraint *)size {
    return [self addConstraintWithAttributes:MASAttributeWidth | MASAttributeHeight];
}

- (MASConstraint *)center {
    return [self addConstraintWithAttributes:MASAttributeCenterX | MASAttributeCenterY];
}

这些属性都会调用 addConstraintWithAttributes: 方法, 生成多个属于 MASCompositeConstraint 的实例.

// MASConstraintMaker.m

NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
    
for (MASViewAttribute *a in attributes) {
   [children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
}
    
MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
constraint.delegate = self;
[self.constraints addObject:constraint];
return constraint;

mas_equalTo

Masonry 中还有一个类似与 magic 的宏, 这个宏将 C 和 Objective-C 语言中的一些基本数据结构比如说 double CGPoint CGSize 这些值用 NSValue 进行包装.

这是一种非常简洁的使用方式, 如果你对这个非常感兴趣, 可以看一下 MASUtilities.h 中的源代码, 在这里就不对这个做出解释了.

Masonry 如何为视图添加约束(面试回答)

Masonry 与其它的第三方开源框架一样选择了使用分类的方式为 UIKit 添加一个方法 mas_makeConstraint, 这个方法接受了一个 block, 这个 block 有一个 MASConstraintMaker 类型的参数, 这个 maker 会持有一个约束的数组, 这里保存着所有将被加入到视图中的约束.

我们通过链式的语法配置 maker, 设置它的 left right 等属性, 比如说 make.left.equalTo(view), 其实这个 left equalTo 还有像 with offset 之类的方法都会返回一个 MASConstraint 的实例, 所以在这里才可以用类似 Ruby 中链式的语法.

在配置结束后, 首先会调用 maker 的 install 方法, 而这个 maker 的 install 方法会遍历其持有的约束数组, 对其中的每一个约束发送 install 消息. 在这里就会使用到在上一步中配置的属性, 初始化 NSLayoutConstraint 的子类 MASLayoutConstraint 并添加到合适的视图上.

视图的选择会通过调用一个方法 mas_closestCommonSuperview: 来返回两个视图的最近公共父视图.

总结

虽然 Masonry 这个框架中的代码并不是非常的多, 只有 1,2 万行的代码, 但是感觉这个项目阅读起来十分的困难, 没有 SDWebImage 清晰, 因为代码中类的属性非常的多, 而且有很多相似的属性会干扰我们对这个项目的阅读, 整个框架运用了大量的 block 语法进行回调.

虽然代码十分整洁不过我觉得却降低了可读性, 但是还是那句话, 把简洁留给别人复杂留给自己, 只要为开发者提供简洁的接口就可以了.

Follow: @Draveness