We Simplify, But We Perfect.


走进 Masonry

2017-03-07


先简单看一下 Masonry 主要的设计以及 Class 结构方法

image description

Masonry 采用了经典的 Composite 设计模式,如果你还不清楚该设计模式,你需要 Google 看看对应的文章

看完了上面类设计图,我们开始跟踪程序

Masonry 开始于这样的代码结构

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIView *aview = [[UIView alloc] init];
    [self.view addSubview:aview];
    
    [aview mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(self.view);
        make.left.right.equalTo(@0);
    }];
}

其中, mas_makeConstraints 方法在 View+MASAdditions 被定义

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

代码的第二行创建了一个 MASConstraintMaker(MASConstraintMakerMASConstraint 一样 定义了 left right 等方法) 对象,也就是我们能够进行链式操作 make.left.right.equalTo 开始对象

第三行执行代码 block(constraintMaker), 也就是执行我们写的 make.left.right.equalTo 的代码

下面开始解析这一段链式代码

MASConstraintMaker 内部有一个 NSMutableArray<MASConstraint *> *constraints 对象, constraints 保存了每一条 make 出来的信息,比如如果你写这样的代码

make.top.equalTo(self.view);
make.left.right.equalTo(@0);

constraints 将会保存 2 个对象,第一个对象记录了 make.top.equalTo(self.view) 的所有信息,第二个对象记录了 make.left.right.equalTo(@0) 的所有信息,而对象的数据结构就是上图中的 MASViewConstraintMASCompositeConstraint

接下来,我们看一看它是如何保存这个信息的:

先看第一个层级:

make.top

调用顺序如下:

- (MASConstraint *)top {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}
- (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]) {
//        //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;
}

其中上面注释的代码是本次调用不会运行的,后面会讲到注释掉的代码的作用

介绍一下 MASViewAttribute

MASViewAttribute 的 Class 结构如下图

image description

上面的 item 字段,Masonry 上给的是 id 字段,因为 还有 viewControllertopLayoutGuide 属性,这里 为了方便理解,可以把它直接看作 UIView

剩余的代码很简单,只是根据 top 这个属性,新建你一个 MASViewConstraint 的对象,然后为 MASViewConstraint1 创建 firstViewAttribute (这里还没有生成 secondViewAttribute`)

相当于下面的代码:

//self  是外部的 [aView mas_makeConstraints] 的 aView
MASViewConstraint *newConstraint.firstViewAttribute.item = self.view //外部调用 make.top 的 aview
newConstraint.firstViewAttribute.layoutConstraint = NSLayoutAttributeTop

最后 把 这个 newConstraint 加入到 MASConstraintMaker(make)constraints 对象中

这样 完成了 make.top 的操作 (.equalTo 的操作稍后介绍)

接着,我们看第二句

make.left.right

其中,前面半句 make.left 和上面的步骤是一样的,不同的地方在于后面 .right

首先,前半句 make.left 返回了 MASAttribute(MASViewConstraint)对象,已经不是 MASConstraintMaker(make) 对象了。所以 我们需要看看 MASViewConstraintleft 方法做了那些事情。

调用顺序如下:

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");

    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];    //这里 delegate 指向了刚刚的 `MASConstraintMaker(make)` 对象
}
- (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;
}

这一次最后这个函数的执行了上阶段注释的代码,这段代码和之前的代码大致相同,也是先创建了一个 MASViewConstraint 的对象, 但是,不同之处在于这次组成了一个 MASCompositeConstraintMASCompositeConstraintchildConstraints 放了上次的 make.left 的信息和这次 make.right 的信息,并且 用 MASCompositeConstraint 替换了原来的 MASViewConstraint。 (这里希望你理解 Composite 设计模式的精髓, 因为 类似这种make.left.right.top三连以上的链式句式 MASCompositeConstraint 里面放的是两个MASCompositeConstraint)

最后是 equalTo 的语法

先看一下 equalTo 的定义

- (MASConstraint * (^)(id attr))equalTo

equal 是一个 返回 MASConstraint * 里面包括一个参数 attr 名为 equalTo 的 block

之所以是 attr 被定义为 id 是因为 我们可以写出这样的代码

make.left.equalTo(self.view) 或者 make.left.equalTo(@0) 

整个 equalTo 的调用顺序如下(selfMASConstraint对象)

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

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
//        if ([attribute isKindOfClass:NSArray.class]) {
//            NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
//            NSMutableArray *children = NSMutableArray.new;
//            for (id attr in attribute) {
//                MASViewConstraint *viewConstraint = [self copy];
//                viewConstraint.layoutRelation = relation;
//                viewConstraint.secondViewAttribute = attr;
//                [children addObject:viewConstraint];
//            }
//            MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
//            compositeConstraint.delegate = self.delegate;
//            [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
//            return compositeConstraint;
//        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
//        }
    };
}

这里 我只列举出了 MASViewConstraintequalToWithRelation, 因为 Composite 模式中定义的 equalToWithRelation 其实就是让所有的子类依次去执行 equalToWithRelation,另外 我并没有怎么使用过 equalTo(attr) attr 是一个 NSArray 类型,所以 注释的代码没有去钻研。

MASViewConstraintsecondViewAttribute 被定义如下

- (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(@0) // make.left.equalTo(self.view)

equalTo 后面可以接 NSNumberUIView 甚至是 NSValue

好了,到此为止,所有的链式代码已经解读完毕,至于 类似的 offset 道理都一样,相对简单,这里不再做过多的陈述

最后是 [constraintMaker install] 的代码

调用函数如下(selfMASConstraintMaker):

- (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;
}

上面的代码 removeExistingmas_remakeConstraints 做的事情, Masonry 用一个 MutableSet 记录了所有通过 Masonry 创建的 constraint, 这个 set 被定义在了 MASConstraints.m 里面

#define MAS_VIEW UIView

@interface MAS_VIEW (MASConstraints)

@property (nonatomic, readonly) NSMutableSet *mas_installedConstraints;

@end

@implementation MAS_VIEW (MASConstraints)

static char kInstalledConstraintsKey;

- (NSMutableSet *)mas_installedConstraints {
    NSMutableSet *constraints = objc_getAssociatedObject(self, &kInstalledConstraintsKey);
    if (!constraints) {
        constraints = [NSMutableSet set];
        objc_setAssociatedObject(self, &kInstalledConstraintsKey, constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return constraints;
}

@end

每次通过 Masonry 添加最后添加成功的 constraint 都会被加入到这个 set 里面

函数的最后把所有添加到 MASConstraintMakerconstraints 的数组清空(因为所有的 constraint 都已经被加入到了 View 里)

最后 我们看看 install 的代码

NSArray *constraints = self.constraints.copy;   //这里为什么用 copy 我也不是很清楚作者是怎么想的,可能是出于线程安全的考虑
for (MASConstraint *constraint in constraints) {
    constraint.updateExisting = self.updateExisting;
    [constraint install];
}

[constraint install] 也是利用了 Composite 设计模式的特性(希望你能真正理解 Composite

MASViewConstraint 的 install 实现:

- (void)install {
    if (self.hasBeenInstalled) {
        return;
    }
    
    if ([self supportsActiveProperty] && self.layoutConstraint) {
        self.layoutConstraint.active = YES;
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }
    
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;

    // alignment attributes must have a secondViewAttribute
    // therefore we assume that is refering to superview
    // eg make.left.equalTo(@10)
    if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) {
        secondLayoutItem = self.firstViewAttribute.view.superview;      
        secondLayoutAttribute = firstLayoutAttribute;
    }
    ////---------截止这里的代码都很好理解 就是在配好 NSLayoutConstraint 的所有 item 和 attribute, 而这些东西在之前的 make 链式语法都组装好了---------////
    
    ////---------其中 masonry 自己的注释也写明白了 为什么 make.left.equalTo(@10) 这样的代码能够被组装成 NSLayoutConstraint---------////     
    //MASLayoutConstraint 可以简单的认为就是 NSLayoutConstraint
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    
    layoutConstraint.priority = self.layoutPriority;
    layoutConstraint.mas_key = self.mas_key;
    
    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;
    } else {
        [self.installedView addConstraint:layoutConstraint];
        self.layoutConstraint = layoutConstraint;
        [firstLayoutItem.mas_installedConstraints addObject:self];
    }
}

这段代码是 install 的最后一个步骤,整体来说比较简单,就是把 MASViewConstraint 变成 NSLayoutConstraint 加入到具体的 UIView 当中,如果你还不清楚怎么用系统的 API 写 AutoLayout 的话,可以点这里

其中 mas_closestCommonSuperview 是找 firstViewAttribute.viewsecondViewAttribute.view 的共同公共的父 View, 这里的算法很简单,只是单纯的遍历

- (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;
}

截止为止,Masonrymas_makeConstraints 的整个过程全部分析完毕,对于剩下的 mas_remakeConstraintsmas_updateConstraints 大同小异,这里不再做更多陈述



上一篇:让你的 App 开启 Multitasking 之旅(二) 下一篇:走进 SDWebImage