Masonry源码解析

3,299 阅读7分钟

Masonry 的实现其实特别简洁,只有 2800 多行代码,10个类文件,以下是核心类的导图,以及核心流程解析:

NSLayoutConstraint、Masonry 对比

NSLayoutConstraint 添加 AutoLayout 约束

首先,给一个 view 添加 AutoLayout 的原生方法:

  • 使用 NSLayoutAttribute 创建一个 NSLayoutConstraint 对象;
  • 然后 [view addConstraint:]
UIView *superView = self.view;
UIView *view1 = [UIView new];
[superView addSubView:view1];

// 禁用自动约束
view1.translatesAutoresizingMaskIntoConstraints = NO;

// 创建一个 view1 相对于 superView 的 left 约束
NSLayoutConstraint *leftConstraint
= [NSLayoutConstraint constraintWithItem:view1
                               attribute:NSLayoutAttributeLeft
                               relatedBy:NSLayoutRelationEqual
                                  toItem:superView
                               attribute:NSLayoutAttributeLeft
                              multiplier:1.0
                                constant:10];
// 省略 30 行
NSLayoutConstraint *rightConstraint;
NSLayoutConstraint *topConstraint;
NSLayoutConstraint *bottomConstraint;
    
[view1 addConstraint:leftConstraint];
[view1 addConstraint:rightConstraint];
// 省略...

使用 Masonry 添加 AutoLayout 约束

上文添加的约束,使用 Masonry 如下,简化了大概 35 行代码:

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
     make.edges.equalTo(superView).offset(10);
}];

Masonry 添加约束流程解析

mas_makeConstraints 内部逻辑展开如下:

    [view1 mas_makeConstraints:^(...) {
        view1.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *maker = [[MASConstraintMaker alloc] initWithView:view1];
 
        // 调用 block():
        maker.left.equalTo(0);
        maker.width.and.top.equalTo().offset(2);
 
        [maker install];
    }]

可以从上述例子中看到,Masonry 添加约束调用的 [view1 mas_makeConstraints:] 里,主要做了以下事情:

  1. 禁用 translatesAutoresizingMaskIntoConstraints 自动约束;
  2. 初始化一个 ConstraintMaker (约束工厂类),并给 maker 添加 上下左右/宽高 的约束(注意这一步只是将约束记录到 maker 的 constraint array 中);
  3. 在 maker install 时,将 constraint array 中的约束逐个 add 到 view 上;

1:生成约束(并 record)

1.1 make.left

    // 语法分析:
    MASConstraintMaker *make = [MASConstraintMaker new];
    // make.left 只是 get 方法( MASConstraintMaker 的 MASConstraint* left 属性)
    MASConstraint *mas = make.left;
  • 可以看到 make.left 是调用 .left 属性;
  • 但由于重写了 left 属性的 get 方法,所以,make.left 会调用 [make addConstraint:] 添加一条 NSLayoutAttributeLeft 类型的约束;
  • left 属性的 get 方法,最终 return 了一个 MASConstraint 对象,所以后续才能链式调用;
// MASConstraintMaker.m

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

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    
    // 用 view 和 NSLayoutAttribute 构建 MASViewAttribute
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    // 用 MASViewAttribute 构建 MASViewConstraint
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    
    // ... 省略
    
    if (!constraint) {
        // 作用??
        newConstraint.delegate = self;
        // 将 MASViewConstraint 添加到 make.constraints(Array) 中,等 install 时再将约束 add 到 view 上
        [self.constraints addObject:newConstraint];
    }
    // return 的 MASViewConstraint 可以继续 链式调用
    return newConstraint;
}

1.2 make.left.and

MASConstraint *mas = make.left; 中可以看到 make.left 返回的是一个 MASConstraint * 类型。

make.left.and.with and 和 with 作为 get 方法被调用,里边只是简单的 return 了 MASConstraint self,所以可以继续链式调用

1.3 make.left.and.width

MASConstraint *mas = make.left; 中可以看到 make.left 返回的是一个 MASConstraint * 类型。因此,链式调用到 .width 时,调用的不再是 maker 的方法,而是 MASConstraint 的方法:

// MASConstraint.m

- (MASConstraint *)width {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth];
}

MASConstraint 是一个 Abstract 抽象类:只提供接口,内部是空实现,需要子类处理。因此 width 里调用的 addConstraintWithLayoutAttribute: 会调用到子类 MASViewConstraint 中:

// MASViewConstraint.m

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

MASViewConstraint 中并没有生成或添加约束,只是调用 delegate 去处理。这个 delegate 刚好就是前边的 maker 对象,所以又回到了上一步 make.left 一样的逻辑(注意上一步中省略掉没贴的代码):

// MASConstraintMaker.m

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    
    // 同 make.left ,省略...
    // MASViewAttribute *viewAttribute = ;
    // MASViewConstraint *newConstraint = ;

    // make.left 时,此方法传入的 constraint 是 nil,因此分析 make.left 时,这块代码省略了
    // make.left.width 到 .width 时,这里传入的 constraint 就是 make.left 生成的 newConstraint 对象
    if ([constraint isKindOfClass:MASViewConstraint.class]) {
        // 注: 此处的 constraint 即 left constraint;newConstraint 即 width constraint
        NSArray *children = @[constraint, newConstraint];
        // MASCompositeConstraint 是 MASConstraint 的子类,constraint group
        MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
        compositeConstraint.delegate = self;
        
        // 替换约束:
        // 替换已经记录到 maker.constraintArray 中的 left constraint 约束为 constraint group
        [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
        return compositeConstraint;
    }
    // ... 省略 
    // if 条件成立,已经 return,后续不会再重复 add constraint 到 array
}

// array 替换元素
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
    NSUInteger index = [self.constraints indexOfObject:constraint];
    [self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}

1.4 make.left.and.width.equalTo()

用于 make.left 作为 get 方法返回的是 MASViewConstraint 对象,可以继续链式调用:make.left.equalTo(superView),即调用 MASViewConstraint.equalTo();

equalTo 入参是 id 类型的 attribute,attribute 类型可以是 MASViewAttribute、UIView、NSValue,equalTo 方法内部对类型做了 if 判断;

equalTo 是个宏定义,展开后的调用链是: qualTo(x) -> mas_equalTo(x) -> MASViewConstraint.equalToWithRelation(x, NSLayoutRelationEqual)

注意:

  • 这里调用过程中将 NSLayoutRelation 参数也传入到了 MASViewConstraint.equalToWithRelation();
  • equalToWithRelation 中保存了 equalTo 传入的 id 类型 attributeNSLayoutRelation 到 MASViewConstraint 对象中,并返回 MASViewConstraint 对象,以继续链式调用;
// MASViewConstraint.m

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

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        // ... 简化
        
        // 保存了 equalTo 传入的 attribute 和 NSLayoutRelation 类型
        self.layoutRelation = relation;
        self.secondViewAttribute = attribute;
        // 继续返回 MASConstraint,链式调用
        return self;
    };
}

1.5 make.left.equalTo().offset()

继续链式调用 offset:make.left.equalTo(superView).offset(2);

注:

  • offsetequalTo 不同,不是像 equalTo有入参的,所以可以加括号 equalTo(xxx);
  • offset方法是一个无入参方法,但是方法的返回值是一个有入参有返回值的 block 类型,因此可以 .offset(3) 调用;
  • 并且,由于 block 的返回值仍是 MASConstraint 对象,所以可以继续链式调用;

简化逻辑如下:

    // make.left 是 get 方法
    MASConstraint* mas = make.left;
    MASConstraint*(^block)(CGFloat f) = mas.offset;
    // 由于 .offset 的 返回值是 block,所以,可以直接调用:
    mas.offset(2);
    // mas.offset(2) 等同于:
    block(2);
    

    // block 的返回值
    MASConstraint *mas2 = mas.offset(2);

1.6 make.xxx

  • 其它 make.center make.insets.and.size 都是一样的用法;
  • make.left.priority(MASLayoutPriorityDefaultLow) 等于 make.left.priorityLow

2:添加约束

2.1 [make install]

对 MASConstraintMaker *make 添加约束后,make install 最后执行约束:

// MASConstraintMaker.m

- (NSArray *)install {
    
    // mas_remakeConstraints 的逻辑:先对已加的约束全部移除,再重新添加
    if (self.removeExisting) {
        NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
        for (MASConstraint *constraint in installedConstraints) {
            [constraint uninstall];
        }
    }
    
    NSArray *constraints = self.constraints.copy;
    // 遍历 make 里添加的每个 MASConstraint 并 install
    for (MASConstraint *constraint in constraints) {
        // mas_updateConstraints 的逻辑: 标记是否需要更新,稍后在  MASViewConstraint install 里更新已加的约束
        constraint.updateExisting = self.updateExisting;
        
        // 最终 install 约束的还是在 MASViewConstraint 里
        [constraint install];
    }
    [self.constraints removeAllObjects];
    return constraints;
}

2.2 [constraint install]

// MASViewConstraint.m

- (void)install {
    // ... 省略:避免重复添加的逻辑
    
    // 取出 MASViewConstraint 记录的 两个 MASViewAttribute 对象
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // ... 省略
    
    // 1、构建一个 NSLayoutAttribute
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];

    // ...
    
    //    创建完约束对象后,寻找约束该添加到那个View上:
    if (self.secondViewAttribute.view) { //    如果是两个视图相对约束,就获取两种的公共父视图。
        MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
        self.installedView = closestCommonSuperview;
    } else if (self.firstViewAttribute.isSizeAttribute) { //    如果添加的是Width或者Height,那么就添加到当前视图上
        self.installedView = self.firstViewAttribute.view;
    } else { //    如果既没有指定相对视图,也不是Size类型的约束,那么就将该约束对象添加到当前视图的父视图上
        self.installedView = self.firstViewAttribute.view.superview;
    }

    // 是否已经添加了约束
    MASLayoutConstraint *existingConstraint = nil;
    
    // mas_updateConstraints 的逻辑
    if (self.updateExisting) {
        existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint];
    }
    if (existingConstraint) { // 存在,直接重新赋值
        existingConstraint.constant = layoutConstraint.constant;
        // ...
    } else {
        // 不存在,添加
        // 2、[view addConstraint:NSLayoutAttribute]; !!!
        [self.installedView addConstraint:layoutConstraint];
        // ...
    }
}

代码技巧

block 灵活使用

block 写法

    @property (nonatomic, strong) UIView *(^myBlock)(NSLayoutAttribute attr);
    
    self.myBlock = ^UIView *(NSLayoutAttribute attr) {
        return greenView;
    };

        // blcok 在右侧时,^ 在最前
    dispatch_block_t t = ^void(void) {
    };
    
    UIView *(^block)(NSLayoutAttribute attr) = self.myBlock;
    
    self.myBlock = block;   

更多 block 语法,见:How Do I Declare A Block in Objective-C?

block 作为入参简化外部调用

在方法需要传入一个 ConfigModel 时,使用 block 作为入参:让外部无需关注 ConfigModel 初始化方式,只需要关注配置项

// 使用:
[view mas_makeConstraints:^(MASConstraintMaker *make) {
     make.edges.equalTo(lastView).offset(2);
}];

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {

    // new 一个 maker,然后调用 block 传出去
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    // 外部对 maker 配置后,maker install
    block(constraintMaker);
    
    return [constraintMaker install];
}

应用案例: SDK 初始化

[SHWAccountSDK setupConfig:^(SHWAccountConfig *config) {
    config.appKey = @"1";
    config.secret = @"2";
}];

block 入参作为 result 回调

[SHWAccountSDK getSMSCode:phoneNum success:^(BOOL isSuccess) {
    // getSMSCode 内部 new 了一个 Request 对象;
} failure:^(NSString *errCode, NSString *errMsg) {

}];

MyRequest *request;
[request startWithSuccess:^(BOOL isSuccess) {
    NSLog(@"getSMSCodeAsyncWithPhoneNum success");
} failure:^(NSString *errCode, NSString *errMsg) {
    NSLog(@"getSMSCodeAsyncWithPhoneNum fail");
}];

block 作为返回值,链式调用

通常函数的入参是 block 用的比较多,但函数的返回值是 block 时,可以写出链式调用的优雅写法:

- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}

// 使用:
[blueView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_bottom).offset(padding);
}];

原理:

    MASConstraint *mas = [MASConstraint new];
    MASConstraint*(^block)(CGFloat f) = mas.offset;
    
    // 以下相等
    block(2);
    mas.offset(2);
    // 由于 mas.offset 的返回值是一个 (入参是 CGFloat 的)block,在后边直接追加 (2),相当于调用 block
    // 并且,由于 block 的返回值仍是 mas 类型,所以后边可以继续链式调用     

对比:

// 返回值不是 block,只是 self class 时
- (MASConstraint *)setOffset:(CGFloat)offset {
    self.offset = offset;
    return self;
}
// 使用:
MASConstraint *mas = [MASConstraint new];
// 只能这样调用
[[mas setOffset:3] setOffset:1];

MASBoxValue: mas_equalTo、equalTo

mas_equalTo() 可以传入多种类型入参:·

    [view1 makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(view2);
        make.left.mas_equalTo(3);
        make.left.mas_equalTo(@(2));

        make.left.mas_equalTo(view2.mas_left);
        make.size.mas_equalTo(CGSizeMake(10, 10));

        make.edges.equalTo(UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f));
        make.height.equalTo(@[redView, blueView])
    }];

其定义如下:


#define MASBoxValue(value)      _MASBoxValue(@encode(__typeof__((value))), (value))
#define mas_equalTo(...)        equalTo(MASBoxValue((__VA_ARGS__)))

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return ...;
    };
}

// 尽管传入的是 id 类型,但是解析也还是要支持变参和,并将 float,double,int 这样的值类型数据转换成和 equalTo 一样的对象 NSNumber 数据:
static inline id _MASBoxValue(const char *type, ...) {
    va_list v;
    va_start(v, type);
    id obj = nil;
    if (strcmp(type, @encode(id)) == 0) {
        id actual = va_arg(v, id);
        obj = actual;
    } else if (strcmp(type, @encode(CGPoint)) == 0) {
        CGPoint actual = (CGPoint)va_arg(v, CGPoint);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(CGSize)) == 0) {
        CGSize actual = (CGSize)va_arg(v, CGSize);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) {
        MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets);
        obj = [NSValue value:&actual withObjCType:type];
    } else if (strcmp(type, @encode(int)) == 0) {
        int actual = (int)va_arg(v, int);
        obj = [NSNumber numberWithInt:actual];
    } // ... 省略
    
    va_end(v);
    return obj;
}

这也顺便解释了 mas_equalToequalTo 的区别:没有区别,mas_equalTo 调用的还是 equalTo,只是调用前对入参进行了 boxValue 转换类型

相关阅读