Masonry 分析

文章目录
  1. 1. Masonry 是什么
  2. 2. 为什么要设计 Masonry
  3. 3. Masonry 代码结构分析
    1. 3.1. 从调用层次分析
    2. 3.2. 分析 Masonry 链式封装
      1. 3.2.1. 主要组件结构
      2. 3.2.2. 使用函数调用栈分析
    3. 3.3. 代码组件分析
      1. 3.3.1. model 层数据
        1. 3.3.1.1. 生成 MASViewAttribute 的方式
        2. 3.3.1.2. MASConstraint 配置过程
      2. 3.3.2. 布局工厂 MASConstraintMaker
    4. 3.4. 其他开发细节分析
      1. 3.4.1. runtime 的运用

分析源码:

分析源码前想想怎么看源码:

  1. 目的:这个第三方库,他是为了解决什么问题的?
  2. 技术起源:效果实现的核心技术是什么?
  3. 设计模式:整体结构是什么样的?
  4. 该怎么入手:由外而内逐层分析层次结构?

Masonry 是什么

Masonry源码

  • Masonry是一个轻量级的布局框架,它以更好的语法包装了AutoLayout
  • Masonry拥有自己的布局DSL,它提供了可链式语法来描述NSLayoutConstraints,从而使布局代码更简洁易读。
    (DSL 其实是 Domain Specific Language 的缩写,中文翻译为领域特定语言)

为什么要设计 Masonry

对比一下 AutoLayout 中使用 NSLayoutConstraint 是多么糟糕

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
UIView *superview = self.view;
UIView *view1 = [[UIView alloc] init];
view1.translatesAutoresizingMaskIntoConstraints = NO;
view1.backgroundColor = [UIColor greenColor];
[superview addSubview:view1];

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[superview addConstraints:@[
//view1 constraints
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeTop
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeTop
multiplier:1.0
constant:padding.top],
... bottom, left
[NSLayoutConstraint constraintWithItem:view1
attribute:NSLayoutAttributeRight
relatedBy:NSLayoutRelationEqual
toItem:superview
attribute:NSLayoutAttributeRight
multiplier:1
constant:-padding.right],
]];

如果使用 Masonry

1
2
3
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).insets(padding);
}];

P.S. Masonry 内部会帮你调用 view.translatesAutoresizingMaskIntoConstraints = NO;

Masonry 设计的目的,就是以链式编程手段封装 NSLayoutConstraint 冗余代码!

  1. 如何处理原来繁杂接口
  2. 如何实现链式
  3. 如何封装 NSLayoutConstraint
  4. 使用了何种编程手段整体架构设计,设计模式,语法技巧

带着这些问题,根据源码结构分析出来


Masonry 代码结构分析

从调用层次分析

先根据函数调用一层一层的找

  1. 最外层接口

根据这段代码分析
UIView category 中实现 View+MASAdditions.h (View+MASShorthandAdditions.h)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define MAS_VIEW UIView

@interface MAS_VIEW (MASAdditions)
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
- (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;

//NSLayoutAttribute properties
@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;
...还有很多布局属性这里省略

@property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr);

@property (nonatomic, strong, readonly) MASViewAttribute *mas_firstBaseline;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_lastBaseline;

//a key to associate with this view
@property (nonatomic, strong) id mas_key;
  • mas_left, mas_right …… 三个见名知意的方法
  • 方法参数 MASConstraintMaker 这个就是 make
  • 属性 MASViewAttribute 这个就是 make 后面的 make.edges
  • id mas_key 这个是啥,目前不知道

我想知道,他的链式是怎么实现的,如何封装 autolayout 相关的数据的,目前看到的只是平常使用的接口
我还要找到下图的这些信息目前看不出来端倪
view formula

  1. 接着往里面看
  • MASViewAttribute: 一个不可变元组,存着 receiver view, related view 和关联的 NSLayoutAttribute [根据文档注释]
1
2
3
@property (nonatomic, weak, readonly) MAS_VIEW *view;
@property (nonatomic, weak, readonly) id item;
@property (nonatomic, assign, readonly) NSLayoutAttribute layoutAttribute;

目前理解为下图等式的相关信息

  • MASConstraintMaker:
    提供一个工厂方法来创建 MASConstraints, 这些 constraints 只有在被 installed 的时候才会被收集根据文档注释

MASConstraintMaker.h

1
2
3
4
5
6
7
8
9
@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
...
@property (nonatomic, strong, readonly) MASConstraint *firstBaseline;
@property (nonatomic, strong, readonly) MASConstraint *lastBaseline;

// 这个是干啥的?
@property (nonatomic, strong, readonly) MASConstraint *(^attributes)(MASAttribute attrs);
  • MASConstraint?跟 NSLayoutConstraint 好像
    • 允许使用可链接的语法创建约束
    • 约束可以表示单个 NSLayoutConstraint(MASViewConstraint)
    • 或一组 NSLayoutConstraints(MASComposisteConstraint)

bingo,到这里下面这两个问题有眉目了, 最开始的几个问题

1
2
2. 如何实现链式
3. 如何封装 `NSLayoutConstraint` 的

分析 Masonry 链式封装

主要组件结构

  1. MASConstraintMaker
  2. MASConstraint
    1. MASViewConstraint
    2. MASComposisteConstraint
  3. MASConstraintDelegate

masonry 主要组件


使用函数调用栈分析

1
2
3
4
5
6
7
8
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(superview.mas_top).with.offset(padding); //with with
make.left.equalTo(greenView.mas_right).offset(padding); //without with
make.bottom.equalTo(blueView.mas_top).offset(-padding);
make.right.equalTo(superview.mas_right).offset(-padding);
make.width.equalTo(greenView.mas_width);
make.height.equalTo(@[greenView, blueView]); //can pass array of views
}];
masonry call stack step1: 主流程

step1

masonry call stack step2:调用block 收集布局信息

step2

masonry call stack step3:install constraints

step3

整体流程&核心调用栈分析结束后,接下来再看细节技巧就容易多了

代码组件分析

  1. MASConstraintMaker
  2. MASConstraint
    1. MASViewConstraint
    2. MASComposisteConstraint
  3. MASConstraintDelegate
  4. MASViewAttribute
  • NSLayoutConstraint 的问题

    • 布局数据分散,使用命令式方法调用,接口参数就是布局属性,导致用户使用闹心
  • Masonry 是如何解决这个问题的呢?

    1. 需要创建对象保存 model 数据
    2. 创建链节点(把大量参数以链节点的方式逐一放在节点中(MASViewConstraint节点))
    3. 调用系统 NSLayoutConstraint 方法
  • 整体步骤:

    1. node1: 创建 MASConstraint 节点,绑定第一个 item1 和 attr1
    2. node2: 创建 MASViewAttribute(item2, attr2),并添加等式的 relation (block 闭包调用)
    3. node3: 添加 valueOffset,priority,divideBy …… 常量数据 (block 闭包低啊用)
    4. block 结束,所有 constraint 配置完,进行 install 内部调用 NSLayoutConstraint 方法,把复杂的方法封装在内部

model 层数据

根据这个等式 等式信息:
view formula

创建一个 constraint 需要:

  1. 两对(view, attr)
  2. relation
  3. mulity
  4. constant
  5. priority
  • MASViewAttribute 元组封装 (item, constraintAttr)
  • MASViewConstraint 在 MASViewAttribute 基础上封装 (mas_attr = 1.0 * mas_attr + constant)
生成 MASViewAttribute 的方式
  1. UIView+MASAdditions
1
2
3
4
5
6
7
8
9
// 计算属性
@property (nonatomic, strong, readonly) MASViewAttribute *mas_left;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_top;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_right;
@property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom;

- (MASViewAttribute *)mas_left {
return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeft];
}
  1. MASConstraintMaker 生成链条节点 MASConstraint 时,使用工厂方法内部配置 MASViewAttribute
MASConstraint 配置过程

make.bottom.equalTo(blueView.mas_top).offset(-padding);

  1. attr1: 由 MASConstraintMaker 中的计算属性方法创建 make.top.left
  2. relation: 由 MASConstraint 中的方法 equalTo 添加
  3. attr2: 由 UIView+MASAdditions 扩展方法得到
  4. offset: 由 MASConstraint 中的方法 equalTo 添加
  5. priority: 由 MASConstraint 中的方法 priority 添加


布局工厂 MASConstraintMaker

他的 product 是 MASConstraint

  1. 他的工厂方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 1. 计算属性,我理解为工厂方法
@property (nonatomic, strong, readonly) MASConstraint *left;
@property (nonatomic, strong, readonly) MASConstraint *top;
@property (nonatomic, strong, readonly) MASConstraint *right;
@property (nonatomic, strong, readonly) MASConstraint *bottom;
...

// 计算属性的 get 方法
- (MASConstraint *)top {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop];
}

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

// 2. 真正的工厂方法,同时 他也是 给 constraint 添加 NSLayoutAttribute 的地方
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {

MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];

// 2.2 make.top.left.equalTo(superview).insets(padding); [left 节点的时候调用,产生第二个 节点以后所有节点的方法]
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;
}

// 2.1 make.top.left.equalTo(superview).insets(padding); [top 节点的时候调用,产生第一个 节点的方法]
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}

得出结论:

  1. 根据上面代码中的 2.1,2.2 知道 make.left.right.top…… 会生产 3 个 MASViewConstraint,并组合到 MASCompositeConstraint 里面

2333 这就知道了 为什么要有 MASCompositeConstraint,他的一个作用就是 实现链式兼容啊!(因为赤裸裸的 3个 MASViewConstraint,怎么变成一个 节点啊)
那么问题又来了,make.left.right.top(view),是怎么实现 left = view.left, right = view.right …… 的呢?

  1. 等式关系,使用函数表示,内部直接把对应 relation 配置给 constraint
1
2
3
- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;
  1. 接下来的链式都是用函数闭包的方式,把参数传给 constraint 的 [闭包格式 MASConstraint * (^)(parameter 参数)]
  2. MASConstraintDelegate 是工厂方法协议, 从 这段代码的 2 看,他是让 constraint 生成 constraint 的方法过渡点。
    1. 他是为了满足 make.top.left 中 top.left 通过 topConstraint 生成一个 leftConstraint的方法
  3. 所以对于链式编程技巧:
    1. 链条的开始,创建成链的起始节点对象
    2. 一条链上应该只有一个对象,后面的所有节点都是对于这个对象的属性配置添加
    3. 如果同一条链上有多个对象,那么使用组合模式,把多个节点对象封装成一个节点对象

从上面得到

1
2
为什么要使用闭包传?使用方法消息传不好吗?
好像就是为了不要 `[]` 这个符号吧……,使用 `.` 链式调用看着好看

其他开发细节分析

下面这些是啥?
都是一些 c 语言的宏,在其他文章里分析过了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//MASUtilities.h
#define MASAttachKeys(...) \
{ \
NSDictionary *keyPairs = NSDictionaryOfVariableBindings(__VA_ARGS__); \
for (id key in keyPairs.allKeys) { \
id obj = keyPairs[key]; \
NSAssert([obj respondsToSelector:@selector(setMas_key:)], \
@"Cannot attach mas_key to %@", obj); \
[obj setMas_key:key]; \
} \
}

static inline id _MASBoxValue(const char *type, ...)
#define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value))

//View+MASShorthandAdditions.h
#define MAS_ATTR_FORWARD(attr) \
- (MASViewAttribute *)attr { \
return [self mas_##attr]; \
}

#define MAS_ATTR_FORWARD_AVAILABLE(attr, available) \
- (MASViewAttribute *)attr available { \
return [self mas_##attr]; \
}

//MASConstraint.h
#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__)))


#ifdef MAS_SHORTHAND_GLOBALS

#define equalTo(...) mas_equalTo(__VA_ARGS__)
#define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__)
#define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__)

#define offset(...) mas_offset(__VA_ARGS__)

runtime 的运用

主要是给 view 添加关联对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//MASViewConstraint.m
@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

//View+MASAdditions.m
- (id)mas_key {
return objc_getAssociatedObject(self, @selector(mas_key));
}

- (void)setMas_key:(id)key {
objc_setAssociatedObject(self, @selector(mas_key), key, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

mas_key 是为了调试布局用的

Auto Layout Guide