Masonry 源码分析

Masonry 提供了简单方便的api ,供我们完成项目中的自动布局业务。

从使用的 api 开始讲

 [button mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.equalTo(superView);
        make.left.equalTo(superView).offset(100);
        make.height.and.width.equalTo(@40);
 }];

调用mas_makeComstraints:,通过在 Constraint Maker Block 中链式调用make的属性,对make进行配置,就完成了对视图的约束设置。

同时还提供了其他两个 api ,能够指定在添加约束前的行为。

  • mas_updateConstraints:在添加约束前,如果是已经存在的约束,则对其进行更新。
  • mas_remakeConstraints:在添加约束前,删除当前视图已经存在的约束。

入口方法

mas_makeConstraints:

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

方法中首先设置了translatesAutoresizingMaskIntoConstraints属性为no,这样我们才能在视图中添加自定义的约束。之后

  • 初始化 maker,和对应视图绑定在一起,并且创建一个可变的数组 constraints 来存放需要添加的约束。
  • 调用 block 完成对 maker 的配置
  • install maker

配置 maker

make.left

调用 left 属性,设置添加的约束类型。会指定对应的 LayoutAttribute ,然后创建约束并添加到make的约束数组中。接下来看具体的实现代码

// MASConstraintMaker.h

- (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方法的时候,传入的 constraint 为 nil ,所以会在使用 layoutAttribute 和视图本身生成 MASViewAttribute 对象,并用其初始化了MASViewConstraint后,会将 maker 设置为它的代理,并将约束对象添加到约束数组中,然后返回。

make.left.equal(@40)

make.left 执行后,会返回一个 MASConstraint 对象,接着链式调用equal(),来制定约束关系。

MASConstraint 是抽象类,提供了链式调用设置约束关系的 block 以及若干抽象接口。

它的子类是 MASViewConstraint 和 MASCompositeConstraint 分别按照各自的需求实现抽象接口。

接着说链式调用 equal 会发生什么,请看代码

// MASConstraint.h

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

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

equal 方法的返回值是一个返回值为 MASConstraint、参数为 attr 的闭包。所以才能实现 equal(@10) 这样的调用方式,并且因为闭包的返回值依旧是 MASConstraint , 所以可以进行接下来的链式调用。

equal(@10) 传入的 attr 参数,即为 -equal 方法体中,返回值闭包的参数 attribute 。接下来会继续调用 self.equalToWithRelation(attribute, NSLayoutRelationEqual) ,传入约束关系和属性参数完成后续的操作。而这个方法的具体实现是在 MASConstraint 的子类中。父类相应的方法实现则是通过使用 MASMethodNotImplemented() 宏,在子类未继承这个方法时或者直接使用父类时产生异常,提示开发者需要在子类中重写这个方法。

// MASViewConstraint.m

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {...} 
        else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}

我们可以看到返回值依旧是一个 block。看到这可能会对调用的顺序有一点晕,下面梳理一下。

我们首先是调用 equal(@10),即调用了equal 方法体中的 block 。所以接下里会执行 self.equalToWithRelation(attribute, NSLayoutRelationEqual)方法,即执行 equalToWithRelation方法体中返回的 block。这个 block 返回的是 当前对象(MASViewConstraint),即为执行self.equalToWithRelation(,)的返回值,也作为equal返回的 block 的返回值

理清执行顺序后,接下来看实现。因为我们传入的 attr@10 ,所以会对 layoutRelationsecondViewAttribute 进行设置。

此时就完成了一条约束的配置。

MASConstraintMaker install

在调用mas_makeConstraints: 的block 后,就完成了 make 的属性设置。接下来会执行 constraintMaker install 安装已添加的约束。

// MASConstraintMaker.h

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

如果之前为视图添加约束时调用的是 mas_remakeConstraint那么就会先移除已有的约束,如果调用的是mas_updateConstraint则会在遍历约束数组时,设置约束的 updateExisting 状态。

然后遍历 constraints 数组, 发送 install 消息。

MASViewConstraint install

接下来就是约束的安装过程,即最后一步为视图添加约束。

// MASViewConstraint.m

if ([self supportsActiveProperty] && self.layoutConstraint) {
        self.layoutConstraint.active = YES;
        [self.firstViewAttribute.view.mas_installedConstraints addObject:self];
        return;
    }

首先是将当前约束添加到 firstViewAttribute.view.mas_installedConstraints 中,保留添加的约束,之后可以对其进行删除更新操作。

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

这一步先获取之后需要的属性。然后对equal(@10) 这种情况进行处理,它指定的是当前视图和父视图的关系。相当于make.left.equalTo(superview.mas_left).offset(10)。所以需要设定 secondLayoutItemSuperViewsecondLayoutAttributefirstLayoutAttribute 相同。

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;

然后用之前获取的属性,初始化 NSLayoutConstraint 的子类 MASLayoutConstraint

之后会确定添加约束的视图:installedView

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

此时会根据 secondViewAttribute 来设置正确的 installedView

  • 如果存在 secondViewAttribute.view ,即equal(view.mas_left),就会寻找 firstLayoutItemsecondLayoutItem 视图的公共 superView,设置为 installedView
  • 如果是 Size 类型属性,installedViewfirstViewAttribute.view
  • 其他情况会直接在把 superview 设置为 installedView
    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];
    }

最后则是对是否需要更新约束进行判断,如果存在相似的约束,那么只改变约束的常量值。对于之前不存在的约束,则直接将约束添加在 installView 中。

总结

这就是 Masonry 为视图添加约束的过程了。

总的概括下来就是, Masonry会创建一个 MASConstraintMaker,传入入口方法的 block 中。我们通过调用maker的api 生成一个MASConstraint对象,此时完成约束关系中第一个视图的属性,并且通过接下来的链式调用,指定约束的关系、设置约束关系中第二个视图及其布局属性。最后调用 make 的 install 消息对添加到maker 中的 约束进行 install ,初始化NSLayoutConstraint 并确定添加约束的视图,完成最后的约束添加工作。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容