走进 Masonry

导语 Masonry 源码阅读

在阅读这篇文章之前,你需要对两块东西有明确的了解

1、AutoLayout, 至少能够知道并使用过

/* Create constraints explicitly.  Constraints are of the form "view1.attr1 = view2.attr2 * multiplier + constant" 
 If your equation does not have a second view and attribute, use nil and NSLayoutAttributeNotAnAttribute.
 */
+(instancetype)constraintWithItem:(id)view1 attribute:(NSLayoutAttribute)attr1 relatedBy:(NSLayoutRelation)relation toItem:(nullable id)view2 attribute:(NSLayoutAttribute)attr2 multiplier:(CGFloat)multiplier constant:(CGFloat)c;

这个 API, 因为 Masonry 本质最后调用的就是这个

2、设计模式 Composite,如果你还不清楚该设计模式,你需要 Google 看看对应的文章

—————————————————— 回归正题 ——————————————————

首先简单看一下 Masonry 主要的设计以及 Class 结构方法,这是一个经典的 Composite 设计模式

另外还有一个辅助的 Class MASConstraintMaker

其中 leftright 等方法分别被定义在了 MASConstraintMASConstraintMaker 中,具体的内部实现稍微不同

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

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(由于还有 macOS 平台,所以 这里的View 被定义为 NSView 或者 UIView) 被定义

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

代码的第一行关闭 AutoresizingMask, 相当于让 AutoLayout 开始生效

第二行创建了一个 MASConstraintMaker(MASConstraintMakerMASConstraint 一样 定义了 left right 等方法) 对象,也就进行链式操作make.left.right.equalTo 的开始,创建了所谓的 maker.

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

下面开始解析这一段链式代码(``make.left.right.equalTo`)

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

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

constraints 将会保存 3 个对象,第一个对象记录了 make.top.equalTo(self.view) 的所有信息,第二个对象记录了 make.left.right.equalTo([@0](https://github.com/0 "@0" )) 的所有信息,第三个对象记录了make.bottom.equalTo(self.view),而对象的数据结构就是上图中的 MASViewConstraintMASCompositeConstraint

接下来,我们开始解析 MASViewConstraintMASCompositeConstraint这两个对象,也就是一个MASCompositeConstraint 或者 一个 MASViewConstraint 是如何存储一个类似于make.left.right.equalTo([@0](https://github.com/0 "@0" ));这样一条链式信息的 :

先看第一个层级:

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 结构如下图

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

剩余的代码很简单,只是根据 top 这个属性,新建你一个 MASViewConstraint 的对象,然后为 MASViewConstraint 创建 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 的操作稍后介绍),也就是让constraints对象保存了所有 make.top 的信息

接着,我们看第二句(和 make.top 的区别在于这句话有 2 个链式结构构成)

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

方法通过某个协议以及 delegate 重新调用回到了 MASConstraintMaker(make)

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute

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

这里不太好理解,可以这么想:一个 MASViewConstraint 就是一个文件(文件系统的文件),一个 MASCompositeConstraint 就是一个文件夹,每一个 .left.right 这样的 layout 就是一个文件。于是,make.left, 创建了一个文件 fileLeft,make.left.right 在创建 fileLeft 的基础上了一个 文件 fileRight(代表着 right),之后又创建了一个文件夹叫做 folderLeftRight, 这个文件夹里面放了 fileLeft 与 fileRight。当链式变成更多之后,比如 make.left.right.bottom, 就会创建一个 folderLeftRightBottom 这样的文件夹,里面放了一个文件夹 folderLeftRight 和一个文件 fileBottom, folderLeftRight 里面有 2 个文件夹 fileLeft 和 fileRight。接下来,如果更多的链式也保持一样的道理,再添加一个文件夹而已,模型大致是这样

箭头表示这个 MASConstraint 在哪一个 MASCompositeConstraintchildConstraints,而最终被保存的数据结构,就是 root 所代表的那个 MASCompositeConstraint (如果只有一层,则是 MASViewConstraint,因为他们同时继承自 MASConstraint

最后是 equalTo 的语法

先看一下 equalTo 的定义

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

划重点了:equalTo 是一个 返回 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 模式中MASCompositeConstraintequalToWithRelation 其实就是让所有的子类依次去执行 equalToWithRelation,最终也就是让 MASViewConstraint 去执行 equalToWithRelation的。就像我们算文件和文件夹的 fileSize 道理一样,文件夹占用固定的 4k(举个:chestnut:),文件夹真正的大小就是算他下面的每一个文件大小,遇到文件夹,继续递归下去计算。

另外,看一下 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);
    }
}

这也是为什么我们能够让equalTo 后面可以接 NSNumberUIView 甚至是 NSValue

make.left.equalTo(@0) // make.left.equalTo(self.view)

好了,到此为止,所有的链式代码已经解读完毕,至于 类似的 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 大同小异,这里不再做更多陈述。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏非典型技术宅

Masonry的层层进阶1 基础写法:2 进阶写法3 自动装箱的写法4 Masonry的练习4.1 设置居中5. 更新及重建约束6. Masonry的两个宏

862
来自专栏MelonTeam专栏

XCode LLDB调试小技巧基础篇提高篇汇编篇

导语 记录平时用到的XCode LLDB调试小技巧 工欲善其事必先利其器,介绍一些LLDB调试的命令和小技巧~ 基础篇 1.print命令 p 输...

4518
来自专栏一“技”之长

Masonry源码解析 原

    Masonry的核心依然是使用原生的NSLayoutConstraint类来进行添加约束,通过统一的封装和链式函数式编程的方式让开发者添加约束布局更加方...

814
来自专栏流媒体

Android RTMP推流之MediaCodec硬编码一(H.264进行flv封装)

在前面Android平台下使用FFmpeg进行RTMP推流(摄像头推流)的文章中,介绍了如何使用FFmpeg进行H264编码和Rtmp推流。接下来讲分几篇文章来...

1113
来自专栏Golang语言社区

Golang国际化(i18n)和本地化(l10n)指南

Go is a statically compiled language that gained a lot of popularity lately due ...

1723
来自专栏陈满iOS

iOS数据埋点统计方案(附Demo): 运行时Method Swizzling机制与AOP编程(面向切面编程)

工程说明,首页Test1ViewController,其中有4个按钮,点击第一个按钮打印,第二个到第四个按钮分别跳转到Test2ViewController,T...

421
来自专栏技术之路

精典算法之详解 河内之塔

河内之塔(Towers of Hanoi)是法国人M.Claus(Lucas)于1883年从泰国带至法国的,河内为越战时北越的首都,即现在的胡志明市;1883年...

1808
来自专栏女程序员的日常

STM8S——8位基本定时器(TIM4)

简介:该定时器由一个带可编程预分频器的8位自动重载的向上计数器所组成,它可以用来作为时基发生器,具有溢出中断功能。 ? 主要功能: (1)8位向上计数的自动...

1971
来自专栏Pythonista

Golang之(for)用法

地鼠每次选好了一块地,打洞,坚持半个月发现地下有块石头,然后他就想绕路了。。。殊不知绕路只会让它离成果越来越远

754
来自专栏landv

c语言_头文件_stdlib

1233

扫码关注云+社区