探究 Masonry 源码

Masonry 是一个轻量级自动布局框架,开发者可以使用更简洁的链式语法为控件进行布局。Masonry 的使用可以参考官网,这里主要探究一下 Masonry 的实现。

Masonry 是对 Auto Layout 的封装,最终还是通过 Auto Layout 来对控件添加约束。这里简单地介绍一下约束,view 与 view 之间的布局可以用一系列线性方程来表示,一个单独的方程就表示为一个约束。下图就是一个简单的线性方程:

EE6ADF1B-E69F-41E0-AD0D-46E351BA61C3.png

这条约束说明了红色 view 与 蓝色 view 左右之间的位置关系,布局中所有的关系都可以抽象成这样的方程,因此布局的过程就是创建一系列约束,也就是创建一系列线性方程的过程。使用 NSLayoutConstraint 来创建约束时,每条约束都要按照下面的方法来创建,这样当布局复杂的时候就需要大量的代码。Masonry 对 NSLayoutConstraint 进行了封装,使用起来更加简洁易懂。下面就来探究一下 Masonry 如何对 NSLayoutConstraint 进行封装。

// 使用 NSLayoutConstraint 创建约束
redView.translatesAutoresizingMaskIntoConstraints = NO
[NSLayoutConstraint constraintWithItem:redView
                                 attribute:NSLayoutAttributeLeading
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:blueView
                                 attribute:NSLayoutAttributeTrailing
                                multiplier:1.0
                                  constant:8.0]

// 使用 Masonry 
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.leading.equalTo(blueView.mas_trailing).with.offset(8.0); 
}];

Masonry 结构

首先我们来先看一个 Masonry 中所包含的类,对其有个大致的了解


101162FC-4D0D-4EE1-A3A8-B7BB9F005488.png

其中 UIViewController+MASAdditions、UIView+MASAdditions、NSArray+MASAdditions 三个 category 包含布局所使用的方法,它们通过 MASConstraintMaker 工厂类来创建 MASContraints,并且为视图添加约束。MASViewConstraint 和 MASCompositeConstraint 是 MASConstraint 的子类,MASConstraint 是个抽象类,由其子类 MASViewConstraint 和 MASCompositeConstraint 来创建实例,另外 MASConstraint 里面提供对链式语法的支持,使用者可以使用链式语法来创建约束。
在看具体的源码之前,我们先来了解一下 Masonry 是如何表示上述布局表达式的,之前说了一个表达式就表示一个 constraint,MASConstraint 就是 Masonry 中用来生成 constraint 对象的类,因其是个抽象类,具体由其子类来实现,先来看一下 MASViewConstraint 这个类,其主要包含了两个属性和一个初始化方法,其中 firstViewAttribute 和 secondViewAttribute 对应下图等式中的部分,如此我们只需要设置其 relationship,multiplier 和 constant 这个约束就完成了,这些都可以通过 MASConstraint 给定的方法来完成,具体内容下面会介绍。

@interface MASViewConstraint : MASConstraint <NSCopying>
@property (nonatomic, strong, readonly) MASViewAttribute *firstViewAttribute;
@property (nonatomic, strong, readonly) MASViewAttribute *secondViewAttribute;
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute;
+ (NSArray *)installedConstraintsForView:(MAS_VIEW *)view;
@end
E93D48F8-B0C4-4722-8748-A88CDA8C5EDD.png
从 mas_makeConstraints 开始源码探究

UIView+MASAdditions 依赖于 MASViewAttribute 和 MASConstraintMaker,该 category 包含类型为 MASViewAttribute 的成员属性,并且提供了我们布局最常用的几个方法。

// 创建并为当前视图添加约束
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
// 更新当前视图已有的约束
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
// 移除已有约束,重新布局
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

首先来看一下创建约束的过程,以下代码是 mas_makeConstraints:方法的具体实现,在使用时我们是通过 block 回调来添加约束。

  • 首先将 translatesAutoresizingMaskIntoConstraints 属性设置为 NO,Auto Layout 与 Autoresizing 不能同时使用。
  • 创建 MASConstraintMaker 的实例,MASConstraintMaker 提供工厂方法来创建 MASConstraint
  • 通过 block 回调创建约束,并添加到 MASConstraintMaker 的私有数组 constraints 中
  • 执行 [constraintMaker install] 方法,最终通过 MASConstraint 的 install 的方法,对相应视图添加约束
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}

实例化 MASConstraintMaker 的过程

MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];

// 创建 MASConstraintMaker 实例,并初始化 constraints 和 view 私有属性,将 self.view 设置为当前 view
- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;
    
    self.view = view;
    self.constraints = NSMutableArray.new;
    
    return self;
}

创建好 MASConstraintMaker 实例之后,来具体看一下 block 中 make.leading.equalTo(blueView.mas_trailing).with.offset(8.0); 的执行,执行结果最终生成一个类型为 MASViewConstraint 的对象。

  • make 即为 MASConstraintMaker 的实例
  • leading 为 MASConstraintMaker 的属性, make.leading 会执行 MASConstraintMaker 中 leading 的 getter 方法,返回一个 MASContraint 对象,实际会返回一个 MASViewConstraint 对象,并设置其 firstViewAttribute 属性。
// MASConstraintMaker.m
// leading getter 方法,返回 MASContraint 对象
- (MASConstraint *)leading {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
// 该方法创建 MASContraint 对象
- (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;
}

具体看 -(MASConstraint *)constraint: addConstraintWithLayoutAttribute: 方法
1、创建一个 MASViewAttribute 对象,MASViewAttribute 是对 view + NSLayoutAttribute 的封装,用来存储 view 和 其相关的 NSLayoutAttribute,描述了上述方程式等号的一边,执行 make.leading 时,MASViewAttribute 对象初始化时的 view 即为 redView, NSLayoutAttribute 为 NSLayoutAttributeLeading


D5125E33-DDD6-4FDA-8311-2A344973BEC7.png

2、根据第一步中的 MASViewAttribute 对象实例化 MASViewConstraint 对象,对其 firstViewAttribute 进行赋值。下面的代码为初始化 MASViewConstraint 对象的过程

MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];

// MASViewConstraint.m
- (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
    self = [super init];
    if (!self) return nil;
    
    _firstViewAttribute = firstViewAttribute;
    self.layoutPriority = MASLayoutPriorityRequired;
    self.layoutMultiplier = 1;
    
    return self;
}

3、判断 constraint 是否存在,在当前过程中 constraint 是不存在的,因此此次执行如下过程,设置newConstraint 的 delegate,并将该对象添加到数组中。

if (!constraint) {
        newConstraint.delegate = self;
        [self.constraints addObject:newConstraint];
    }

MASViewConstraint 对象的 delegate 方法定义在 其父类 MASConstraint 的 MASConstraint + Private.h 分类中,这个delegate 是实现链式语法的重点。

@protocol MASConstraintDelegate <NSObject>
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
@end

4、最后返回 newConstraint 对象
至此,make.leading 执行完毕,返回了 newConstraint 对象,该对象的 firstViewAttribute 已经设置好了,即为方程式的左边部分,layoutMultiplier 也设置为了1,此时方程式只剩下右边的 item2 、Attribute2、和常数了,Masonry 已经将item 和 attribute 打包为了 MASViewAttribute。

  • 接着执行 equalTo 方法, make.leading.equalTo(blueView.mas_trailing),iOS的语法中没有方法后面跟着(参数)的,这里很明显是一个 block,利用 block 实现了链式语法,这里要注意 block 的返回类型要为 MASConstraint 类型。
//MASViewConstraint.m
- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
// 返回值为block 
- (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;
        }
    };
}

在执行 equalTo 方法之前,先看一下该方法 block 所需要的参数 (id attr),参数类型可以MASViewAttribute, UIView, NSValue, NSArray 中的任意一个,显然 blueView.mas_trailing 应该是 MASViewAttribute 类型的对象。

self.layoutRelation = relation;
self.secondViewAttribute = attribute;
// 
- (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);
    }
}

执行上述代码,在 secondViewAttribute 的 set 方法中在对不同类型的参数进行区分,如果是 NSValue,MASConstraint.m 中有对常量设置的方法 setLayoutConstantWithValue;如果是 NSView 类型,则需要实例化一个 MASViewAttribute 对象,此时 layoutAttribute 属性值设置为 self.firstViewAttribute.layoutAttribute;如果是 MASViewAttribute 类型,则直接赋值。

// 参数类型为 NSView 类型
make.leading.equalTo(redView); 
// 等价于  
make.leading.equalTo(redView.mas_leading);  // mas_leading 在此时即为 constraint.firstViewAttribute.layoutAttribute

此处的 secondViewAttribute 就是 等式右侧


69FDAB87-8071-4EAF-B0D8-6EB7CB8ED68A.png
  • 执行完 make.leading.equalTo(blueView.mas_trailing),布局等式除了 constant 之外的所有参数都已设置完毕,接下来执行 .with,该函数返回其本身,只是为了提高代码的可读性。
- (MASConstraint *)with {
    return self;
}
  • 接下来就是 .offset() , 修改 constant 为8.0
// MASConstraint.m
- (MASConstraint * (^)(CGFloat))offset {
    return ^id(CGFloat offset){
        self.offset = offset;
        return self;
    };
}
// MASViewConstraint.m
- (void)setOffset:(CGFloat)offset {
    self.layoutConstant = offset;
}

到这里一个约束就建立好了,回到 mas_makeConstraints 方法中,接下来该执行 [constraintMaker install] 对约束进行安装,首先会判断是够需要移除当前约束重新添加,并判断是否需要更新现有约束,然后执行 [constraint install] 找到合适的 view 来添加约束。

- (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;
}
MASConstraintDelegate 的使用

前面提到了一个 MASConstraintDelegate 的 delegate 方法,这个方法非常重要,对 MASConstraint 对象设置代理,才能支持链式调用

make.width.and.height.equalTo(@10)

make.width 的执行过程上面已经提过了,返回的是一个 MASViewConstraint 实例对象 newConstraint,并设置 newConstraint.delegate = self ,and 方法和 with 方法一样,都返回的是自身,此时调用 .height 方法就不在是 MASConstraintMaker 的方法,而是 MASViewConstraint 的 height 方法,然后调用代理方法添加约束,就完成链式调用

// MASConstraint.m
- (MASConstraint *)height {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
}
// MASViewConstraint.m
#pragma mark - attribute chaining
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
    return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}

关于.equalTo() 的链式调用,可以用下面的一句话来说,具体可参考链接文章

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

推荐阅读更多精彩内容