关于 Masonry 的一些思考(下)

2,578 阅读9分钟

前言

本篇文章是笔者对上篇文章《关于 Masonry 的一些思考》的一些自己的解答,哪里有理解不到位的地方,请尽情拍砖。如果想先看无答案版,请前往上篇文章 《看完 Masonry 源码后的几点思考?》。

图片来自戴铭文章 《读 SnapKit 和 Masonry 自动布局框架源码

关于 Masonry 思考的解答

1. Masonry 都做了些什么?

Masonry 是一个让开发者用简洁优雅的语法来调用原生 AutoLayout 进行布局的轻量级框架。Masonry 拥有自己的 DSL 布局语言,让我们可以更具象地描述约束的增加与更新,让约束的代码也变得更加简洁易读、容易理解。

DSL 是一种基于特定领域的语言,它使工作更贴近于客户的理解,而不是实现本身,这样有利于开发过程中,所有参与人员使用同一种语言进行交流。简单来说,就是我们只需描述出我们想要什么效果,而毋需涉及底层实现,这无疑降低了工作过程中沟通协调的门槛。

语言过于苍白,让我们 show code:

原生 AutoLayout 实现一个红色 view 布局

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],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeLeft
                                multiplier:1.0
                                  constant:padding.left],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeBottom
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeBottom
                                multiplier:1.0
                                  constant:-padding.bottom],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeRight
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeRight
                                multiplier:1
                                  constant:-padding.right],

 ]];

使用 Masonry 进行布局:

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

代码甚至可以再精简下:

UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10);
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(padding);
}];

以上代码来自 Masonry 的 github 介绍

经过上面的代码比较,Masonry 语法的简洁优雅效果是浅显易见的。代码不仅变得精简,而且阅读成本也基本降到了最低。

2. 下面代码会发生循环引用吗,为什么?

[self.view addSubview:btn];
[btn makeConstrants:^(MASLayoutConstraint *make){
make.left.equalTo(self.view).offset(12);
}];

答: 不会发生循环引用,方法中 block 参数虽然引用 self.view,间接持有了 btn,但是 block 参数是个匿名 block,并且在方法实现里未额外引用这个 block 参数, block 并未被 btn 所持有,也就不存在两者相互持有、循环引用。block -> self.view -> btn -(未引用)- block

而上述方法定义中也明确使用 NS_NOESCAPE 修饰 block 参数, 这个修饰符表明 block 在方法执行完前就会被执行释放,而不会对 block 进行额外的引用保存。

- (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block

在代码中 Masonry 也确实是这么做的:

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

    return [constraintMaker install];
}

从上面代码中,可以清除地看到,block 参数在 return 之前就被执行,并未被其他对象引用。

更多关于 NS_NOESCAPE 的介绍

额外拓展,很多同学对 block 的循环引用都不太了解:同样是匿名 block 参数,系统动画

[UIView animateWithDuration:100 animations:^{
        NSLog(@"%@",self);
    }];

不会造成循环引用,而 MJRefreshheader 初始化方法

self.scrollView.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:^{
        NSLog(@"%@", self);
    }];

为什么会造成循环引用?

MJRefreshheaderWithRefreshingBlock: 方法内部,返回的 MJRefreshNormalHeader 对象强引用了这个 block,而这个返回对象最后又被 self.scrollView.mj_header 强引用了,也就造成了 self -> scrollView -> mj_header -> block -> self 的强引用闭环,因此会造成循环引用。

headerWithRefreshingBlock: 实现代码:

+ (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock
{
    MJRefreshHeader *cmp = [[self alloc] init];
    cmp.refreshingBlock = refreshingBlock;
    return cmp;
}

系统的动画实现方法中,self 并未和这个 block 产生关联,但 block 确实持有了 self,但笔者猜测 block 对self 并不是强引用,因为如果在这个动画时间内控制器执行 POP 操作,self 会立即被释放掉,也就是说除了导航控制器栈,self 并未被额外的强引用,否则 self 不会被释放。

self 未引用 block (弱)-> self 。因此也不存在循环引用

想一想,当方法中使用匿名 block、匿名对象作为参数,这些匿名对象是被谁持有?会在什么时候释放呢?欢迎在评论中探讨。

3. MAS_SHORTHANDMAS_SHORTHAND_GLOBALS 宏是做什么用的?它的效果是如何实现的呢?

MAS_SHORTHAND 宏可以在调用 Masonry api 的时候省去 mas_ 前缀

MasonryView 定义了 一个 View+MASAdditions 分类。在这个分类中,所有的成员属性和方法都是带有 mas_ 前缀的。Masonry 还另外定义了 View+MASShorthandAdditions 分类,在这个分类中所有的所有属性和成员变量都不带 mas_ 前缀。但这个分类被 #ifdef MAS_SHORTHAND #endif 所包裹。 效果如下:

//MASShorthandAdditions 分类
#ifdef MAS_SHORTHAND  
...
...(不带有 mas_ 前缀的成员变量和方法)
...
#endif

这样只有定义了 MAS_SHORTHAND 之后这个分类才会被编译,而这个分类内部所有属性的 get 方法、对外的接口方法实现还是调用的带有 mas_ 前缀的方法,对于我们开发者来说,只是在 mas_ 属性与方法外面包裹上了一层语法糖。

不带有 mas_ 前缀方法的实现:

//属性的 get 方法宏,在属性前拼接 mas_ 前缀,调用带有前缀的属性 get 方法
#define MAS_ATTR_FORWARD(attr)  \
- (MASViewAttribute *)attr {    \
    return [self mas_##attr];   \
}
//不带有 mas_ 前缀的 API,内部会调用带有 mas_ 前缀的 API
- (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block {
    return [self mas_makeConstraints:block];
}

MAS_SHORTHAND_GLOBALS 宏会将 equalTo()greaterThanOrEqualTo()offset() 宏定义为 mas_equalTo()mas_greaterThanOrEqualTo()mas_offset()

而带有 mas_ 前缀的方法会将括号内的 block 参数从基本数据类型转化为 NSValue 对象类型

#define mas_equalTo(...) equalTo(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)

#endif

4. MasonrymakeConstraints:updateConstraints:remakeConstraints: 有什么区别,分别适合那些场景?

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

remakeConstraints: 和上面代码的唯一区别就是增加了constraintMaker.removeExisting = YES;

[constraintMaker install] 时,如果 removeExisting 判断为 true,会将已安装的约束全部执行 [constraint uninstall] 卸载;

updateConstraints: 和上面代码的唯一区别就是在调用 block 之前增加了一句 constraintMaker.updateExisting = YES 标示。

[constraint install] 执行时,会判断 updateExisting 的值, 如果为 true 会接着判断约束和已安装的约束是否相似,(判断是否相似的规则是,两条约束只有 constant 常量值不一样,其它诸如 firstItem secondItem firstAttribute secondAttribute relation multiplier priority 必须和之前约束完全一致,才为相似约束。),如果存在相似约束,则进行约束更新,否则就新增这条约束。因此我们要十分注意 updateConstraints: 新更新的约束会不会和已有的约束冲突, 例如当我们之前约束为 make.right.equalTo(self.view).offset(-12); 更新后为 make.right.equalTo(self.view.mas_centerX).offset(-15); 这是两条不相似的约束(secondAttribute 不一样),如果更新约束,会造成约束冲突。

5. 描述下代码 make.left.right.top.equalTo(self.view).offset(0) 都做了些什么?

make.left 生成并返回 MASViewConstraint 对象,需要注意的是:

  • 该对象已保存了调用 viewFirstView) 和 leftFirstAttribute

  • 该对象已被添加到 makeconstraints 数组内保存

MASViewConstraint.right 生成并返回了 MASCompositeConstraint 对象,需要注意的是:

  • MASCompositeConstraint 对象保存了包含 lefttop 的两条 MASViewConstraint 对象
  • makeconstraints 数组之前保存的 MASViewConstraint 对象被替换为该 MASCompositeConstraint 对象

MASCompositeConstraint.top 返回之前的 MASCompositeConstraint 对象,需要注意的是:

  • MASCompositeConstraint 增加了一条 top 约束

MASCompositeConstraint .equalTo(self.view) 返回之前的 MASCompositeConstraint

  • 遍历 MASCompositeConstraint 保存的几条约束, 为他们设置 layoutRelationsecondViewsecondAttribute
  • equalTo() 参数是 view 类型,secondAttribute 依旧是 nil,会在最后约束安装时如果判断为 nil 则值初始化为 FirstAttribute

MASCompositeConstraint.offset 无返回值

  • 遍历 MASCompositeConstraint 保存的几条约束,为他们设置 layoutConstant

最后约束安装时 执行 [constraintMaker install]; 就会根据 firstView FirstAttribute layoutRelation secondView secondAttribute layoutConstant 来生成原生的约束 NSLayoutConstraint ,并将原生约束添加到 firstView secondView 最近的公共父视图上生效。

6. Masonry 是如何做到链式优雅调用的?

链式编程思想:简单来说,是将多个操作(多行代码)通过点号(.)链接在一起成为一句代码,使代码可读性好。a(1).b(2).c(3)

链式编程特点:方法的返回值是 block , block 必须有返回值(本身对象),block 参数就是需要操作的值。

make.left.right.top.bottom.equalTo(self.view).offset(12) 链式调用的具体过程是什么样的呢?

首先 Masonry 定义了一个 MASConstraint 抽象类 上面所有的方法返回值都是 MASConstraint 类型,而所有的调用者除了第一个为 MASConstraintMake 类型,其它都是 MASConstraint 类型调用。所以前一个方法的返回值正好作为下一个方法的调用者,而调用过的所有方法修改的约束都被 makerconstraints 所记录下来。随后在 [constraintMaker install]; 的时候遍历 constraints 执行 [constraint install]

7.MASViewConstraint 为什么要弱引用一个 MASLayoutConstraint 的实例对象,它又用这个对象做了什么?

Masonry 库最后都会生成一个 MASVIewConstraint 对象,Masonry 会根据这个对象生成系统原生 NSLayoutConstraint 约束的创建,而后期可能要对这个原生约束进行一些移除操作。需要记录这个原生约束对象。

8.MASConstraintMaker 持有一个 constraints 数组, 而 MASViewConstrint 类也有一个用来记录约束的数组,这两个数组都是用来记录生成的约束,那这两个数组有什么区别吗?各自的作用又是什么?

MASConstraintMaker 的 数组是记录 本次 Masonry API 调用生成的约束,最后 make 将这个数组内的约束遍历安装 install。 数组里存储的是 MASViewConstraintMASCompositeConstraint 对象

MASViewConstrint 类的数组,记录的是 Masonry 调用者 View 已经安装了哪些约束,这个数组在后期调用者调用 updateConstraints: 时判断,更新的约束是否已经安装了 ,remakeConstraints: 方法时,需要根据数组将已经安装过的约束移除。数组里存储的都是 MASViewConstrint 对象。

后记

尽管笔者水平有限,但对这些问题的拙劣见解还是奉上,希望可以给读 Masonry 源码的小伙伴带来些不一样的视角,如果对于文中有解读不当的地方也请您不吝指出。