浅谈Masonry的使用技巧

前言


讲真的,搞事搞了四五年的时间了,一直觉得AutoLayout布局方式比较影响性能,所以一直使用着最原始的Frame布局方式,但是随着机器性能的不断提高,我觉得AutoLayout这种布局方式已经可以基本忽略对性能方面的影响,而且在复杂布局方面AutoLayout有着Frame布局没有的优势,简单粗暴。这两天,我也是刚刚开始使用 Masonry ,说真的,真香。


这里需要简述一下Frame布局方式和Masonry布局方式各有什么优缺点。我觉得这种谁强谁弱的问题要去辩证的看待,Frame布局方式在简单UI排版情况下,较为简单,而且性能极高,但是不利于扩展,例如如果是横竖屏情况就基本上需要使用两套布局方案,非常的麻烦和繁琐。反观Masonry布局,使用起来较为方便,可以轻松应对各种布局方式,但是在性能上存在问题,使用不当容易造成性能问题,性能方便要比Frame布局方式差很多。整体来说,简单布局使用Frame布局,复杂布局使用Masonry布局,至于界限需要自己把握(这就跟解耦和耦合的问题一样,界限完全是自己来掌控的,自己把握好度即可)。


约束的常识


在写Masonry之前,我想先来聊聊约束的基础知识,我们首先要了解一个View的约束需要确定的是两个因素,一个是宽高信息,另外一个是位置信息。 只有确定这两个因素才能真正的确定一个View的约束,否则约束会爆警告。不管你怎么加约束,其实最后归根到底都是确实的这两个信息,那么我们了解这个有什么好处呢?我们可以通过约束转化来了解我们多添加了约束,是否缺失了某个约束,这种思想可以帮助我们快速查询问题所在。

但是有很多童鞋会发现在使用 Masonry 的时候,如果控件是UILabel,UIImageView,UIButton等这些组件及某些包含它们的系统组件只需要指定控件的位置约束,根本不需要指定宽高约束即可完成布局任务,这是为什么呢?这是因为这些控件中有 intrinsicContentSize 这个属性,intrinsicContentSize的作用其实很简单,它会自己根据内容计算出控件的固有宽高,在布局过程当你不指定宽高约束的时候,它就会生效。具体的内容我会在下面说到。这里就不过多叙述了。


Masonry的使用


Masonry的使用百度上随意一搜索就是一大堆,这里也简单的介绍一下吧。

首先,Masonry的添加布局主要有三个,三个方法的作用分别是创建约束;更新某个约束,其他约束不变;移除先前所有约束,添加新到的约束。这三个方法根据场景需要合理使用,否则可能造成内存问题,优化方式下面我们会来聊一下,这里就不过多叙述了。

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

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

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

假设我们有一个subView视图(添加约束之前要先添加到父视图上,这里就不多比比了),我们该怎么给这个subView视图添加约束呢?

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self.view).offset(10);
    make.top.equalTo(self.view).offset(10);
    make.right.equalTo(self.view).offset(-10);
    make.bottom.equalTo(self.view).offset(-10);
}];

上面的写法我们可以把 self.view → self ,Masonry内部会自动处理的,形式如下所示。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.left.equalTo(self).offset(10);
    make.top.equalTo(self).offset(10);
    make.right.equalTo(self).offset(-10);
    make.bottom.equalTo(self).offset(-10);
}];

当然了,每一个控件都写这么四个约束(上,下,左,右),肯定是能把人累的半死,我们可以设置 edges 来简化我们的代码,上面的代码就转化成如下代码形式。而且我们发现下面的代码,-10已经变成了10,这是因为 insets 已经帮我们自动处理过了,这点我们需要注意了。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(self).with.insets(UIEdgeInsetsMake(10, 10, 10, 10));
}];

如果我们想设置一个具体的数值该怎么办呢?例如宽度我们想设置成10个单位,我们就可以如下设置。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(@10);
}];

我们发现上面代码还有个问题,那就是 equalTo 这个函数的参数必须是一个对象类型,这就很尴尬了,为啥,书写太麻烦,这时候我们可以使用 mas_equalTo 这个函数,示例如下所示。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.mas_equalTo(10);
}];

上面书写的好像也不是一个最优的方案,虽然我们解决了后面的问题,但是前面的代码字母数又多了(懒癌发作😂),这时候我们可以在我们的文件之前加上一个 #define MAS_SHORTHAND_GLOBALS 这样的宏定义,就可以直接使用equalTo(10)了,如下所示。

#define MAS_SHORTHAND_GLOBALS

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(10);
}];

至于为啥可以这么做,我就直接截图了,这里不过多解释了。。。

如果我们想要设置subView的宽度等于父视图的宽度的50%,这时候我们该怎么编写我们的约束呢?我们可以用到 multipliedBydividedBy这两个方法,一个是乘法,一个是除法,较为简单,示例代码如下所示。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(self).multipliedBy(0.5);
}];
[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(self).dividedBy(2);
}];

如果我们想要subView的宽度等于高度的2倍,这时候该怎么办呢?我们需要指定equalTo()里面具体的值(实际上是传一个 MASViewAttribute 对象),而不是简单的传一个控件对象,示例代码如下所示。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(subView.mas_height).multipliedBy(2);
}];

如果我们想让subView和subView2,subView3的宽度相等,怎么办?如果三者宽度都是100个单位,我们又该怎么办?示例代码如下所示。(其实都在官方文档中了,懒癌持续发作中。。。。)

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(@[subView2.mas_width, subView3.mas_width]);
}];
[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(@[subView2, subView3]);
}];
// 三者相等,并且宽度为100个像素点
[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(@[subView2, subView3,@100]);
}];

接下来,我们就简单叙述一下约束优先级设置,Masonry为我们提供了三个优先级的方法,priorityLow()priorityMedium()priorityHigh(),这三个方法内部对应着不同的默认优先级,当然我们也可以使用priority() 设置具体的数值。示例代码如下所示,关于约束优先级具体使用也会在后面的模块中说到。

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.width.equalTo(subView4).priorityLow();
   make.width.equalTo(@[subView2, subView3,@100]).priorityHigh();
   make.width.equalTo(@300).priority(888);
}];

接下来我们来玩个不一样的,如果我们想让subView的宽度是 父视图的宽度的30% + 10个单位长度,这时候我们该怎么设置呢?其实这时候有点类似于 CSS 中的 calc() 函数,我们肯定不能设置两条约束条件,如果那样设置了,后面的约束条件就会把前面的约束条件给覆盖掉,对此我们如下设置即可。(offset方法和multipliedBy方法顺序无影响)

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
  make.width.equalTo(self).offset(10).multipliedBy(0.3);
}];


约束优先级 以及 intrinsicContentSize相关问题


约束优先级以及intrinsicContentSize的相关问题是我们不得不提到的问题.

首先来说一下为什么要有约束优先级,我们给定一个场景,假设我们设置在一个superView(宽度为 200)中的一个View子视图的左右边距都为0,然后第二个约束是视图的宽度为100,这时候就会出现问题,因为如果左右边距都为0,那么视图宽度为200,这样和第二个约束条件就发生了冲突,系统是不允许这样的问题出现的.那么我们想不在删除约束的情况下,该如何解决这种问题呢?这时候我们就需要通过设置约束优先级来解决这一类问题,系统通过比较两个”相互冲突的约束”的优先级,从而忽略低优先级的某个约束,达到正确布局的目的约束优先级默认都是1000.所以我们给设定一个根据具体情况设置一个合适的值即可,代码如下所示.

[subView mas_makeConstraints:^(MASConstraintMaker *make) {
   make.left.right.equalTo(self);
   make.width.equalTo(@100).priority(888);
}];

约束优先级主要是应对与单个视图中多个约束发生冲突的时候解决问题的方案.而 intrinsicContentSize 主要应对于多个视图约束发生冲突的解决方案,我们就对着具体的实例来进行分析.

在最前面我们说到 在AutoLayout中, intrinsicContentSize的作用其实很简单,它会自己根据内容计算出控件的固有宽高,在布局过程当你不指定宽高约束的时候,它就会生效。

这个属性是非常的好用,但是也会出现对应的问题.例如我们现在有两个Label,两个Lable的约束条件如下所示.

[label1 mas_makeConstraints:^(MASConstraintMaker *make) {
   make.left.equalTo(superView);
   make.top.equalTo(superView);
}];

[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
   make.left.equalTo(label1.mas_right);
   make.top.equalTo(superView);
}];

上面的情况完全没有任何的问题,因为 intrinsicContentSize 属性的原因,我们轻松完成布局任务,但是当我们给 label2 添加一个 右边距等于superView.代码如下所示.

[label2 mas_makeConstraints:^(MASConstraintMaker *make) {
   make.left.equalTo(label1.mas_right);
   make.right.equalTo(superView);
   make.top.equalTo(superView);
}];

这时候就会出现问题,label1 和 label2 必然有一个不能满足 intrinsicContentSize 约束条件,必然有一个需要拉伸才能完成约束布局任务,我们称这种问题叫做 Intrinsic冲突.

解决 Intrinsic冲突 一共有两种方案,一种是直接指定冲突的label 1 和 label 2的宽高约束信息.第二种就是利用 content Hugging/content Compression Resistance.原始方法如下所示.

- (void)setContentHuggingPriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis API_AVAILABLE(ios(6.0));
- (void)setContentCompressionResistancePriority:(UILayoutPriority)priority forAxis:(UILayoutConstraintAxis)axis API_AVAILABLE(ios(6.0));
  • Content Hugging 约束(不想变大约束)表示:如果组件的此属性优先级比另一个组件此属性优先级高的话,那么这个组件就保持不变,另一个可以在需要拉伸的时候拉伸。属性分横向和纵向2个方向。

  • Content Compression Resistance 约束(不想变小约束)表示:如果组件的此属性优先级比另一个组件此属性优先级高的话,那么这个组件就保持不变,另一个可以在需要压缩的时候压缩。属性分横向和纵向2个方向。 意思很明显。上面UIlabel这个例子中,很显然,如果某个UILabel使用Intrinsic Content Size的时候,另一个需要拉伸。 所以我们需要调整两个UILabel的 Content Hugging约束的优先级就可以啦。

所以我们可以通过设置这两个方法来解决 Intrinsic冲突 问题,假设 我们想让 label 2 拉伸,label1尽量不拉伸,我们就可以设置如下代码.具体代码如下所示.

[label1  setContentHuggingPriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisHorizontal];
[label2  setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal];


Masonry的全新开始 → iOS12


我们都知道,其实Masonry是封装系统的NSLayoutConstraints,简化了代码,但是在iOS12之前NSLayoutConstraints存在着致命的问题,那就是性能问题,其实这个在iOS12之后也会存在只是小了很多。那么在iOS12之前到底是什么原因导致这些问题呢?接下来我们逐一分析各种情况。

AutoLayout使用的布局算法其实是 Cassowary,在WWDC2018,官方对其性能问题提出了说明,如下图所示,我们可以清楚的看到iOS12前的AutoLayout布局性能是成指数性增长的。

但是不是所有的布局都有这样的问题呢?答案当然是否定的,如下图所示。所以说AutoLayout只是在某些情况存在着问题。

那么真正的原始是什么呢?因为iOS12之前,当有约束变化时都会重新创建一个计算引擎 NSISEngier 将约束关系重新加起来,重新计算。涉及到约束关系变多时,新的计算引擎需要重新计算,最终导致计算量指数级增加。

iOS12的AutoLayout更多的利用了Cassowary算法的界面更新策略,使其真正完成了高效的界面线性策略计算。使其尽量成线程增加,减少性能问题,最后允许我唠叨一句,讲真的,性能再强也是干不过Frame布局方式的,但是胜在简单方便。


添加约束的性能探讨


在看这个模块之前,我建议各位大佬可以去看一下浅谈Constraints,Layout,Display的点点滴滴,对系统的约束,布局,绘制API的执行顺序有个大致的了解,再来做性能优化可能会更加的的得心应手.

在masonry的基础用法中,我们提到有三个约束的基础方法,分别是 mas_makeConstraintsmas_updateConstraintsmas_remakeConstraints。很多童鞋使用Masonry过程中觉得滑动不流畅,界面卡顿,这是为啥呢?相对于Frame布局来说,AutoLayout布局确实会影响性能,但其实主要原因是没有很好理解上面三个添加约束方法,下面我们就这三个约束方法来具体讨论。

在Masonry官方文档中,作者建议我们把约束放在 updateConstraints 这个方法中,然后使用 mas_remakeConstraints 方法添加约束。虽然看上去没有任何的问题,但是只能说是性能一般,没有做到最优方案,最优方案一定是按照 具体情况具体应变 的原则进行编写代码.但是我们也要了解作者的良苦用心,毕竟说多了都是错,虽然这种方案在性能上不一定是最优方案,但是在容错方面确实最强的.

对于具体情况具体应变,我们假定下述情况,逗比的我认为应该这么做....

① 当视图的约束布局是一定的,后期没有发生改变,那么我认为应该使用 mas_makeConstraints来添加约束,但是要注意的是 mas_makeConstraints 一定只能调用一次,反复调用的 mas_makeConstraints 是会造成性能问题的.

② 当视图的约束布局是部分固定,一部分可能会根据情况发生约束上的改变,这时候,我个人认为 mas_makeConstraintsmas_updateConstraints 进行结合使用才是最优方案,虽然使用 mas_remakeConstraints 可以很方便的解决这个问题,但是 mas_remakeConstraints 性能是不如 mas_updateConstraints的,这里不建议使用了.

③ 当视图的约束布局只要数据法神改变,约束布局就会发生改变,这种情况基本上就是 mas_remakeConstraints 这种方案了.虽然它的性能较低,但是它确实解决这种问题的最优方案.


下面我们就对着具体的例子来进行Masonry性能的探讨.

  • 情景: 在自定义Cell中,有很多的童鞋虽然在 updateConstraints 中写了布局方法,但却是使用了 mas_makeConstraints 方法,并且在setData方法中不断调用[self setNeedUpdateConstaints];如下所示.
- (void)updateConstraints {
    [self.subView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.equalTo(self).with.insets(UIEdgeInsetsMake(4, 4, 4, 4));
    }];
    [super updateConstraints];
}

这是因为Masonry的mas_makeConstraints方法是添加约束的方法,每执行一次,它就会添加一遍所有的约束,原来的约束也是一直会存在的,这样每一次执行就会导致内存的增加.造成性能问题.

回归到这个问题上来,那么我们按照官方中的方案使用 mas_remakeConstraints 就一定是最优方案吗? 不然,我们要看清楚,官方的Demo反复执行 [self setNeedUpdateConstaints] 的概率要比Cell中的setData方法不断调用[self setNeedUpdateConstaints]的概率要低得多,所以官方的性能问题不太严重,但是我们这样做性能依然会出现问题.先不谈解决方案,我们先瞅一眼苹果官方对updateConstraints的说明.

在苹果的官方文档中 updateConstraints 方法简介中,其实也算对各个开发者进行了警告说明,原文如下所示.意思就是官方想让各位重写此方法的大佬一定要保证该方法的高效性,约束尽量不要每一次调用该方法都把原来的约束都删除,然后重新搞一遍, 对于可能修改的约束要进行验证在执行对应的操作.而mas_remakeConstraints就是把原来约束都删除,然后添加新的约束.所以在这种情况下仍然不是最优的方案.

Your implementation must be as efficient as possible. Do not deactivate all your constraints, then reactivate the ones you need. Instead, your app must have some way of tracking your constraints, and validating them during each update pass. Only change items that need to be changed. During each update pass, you must ensure that you have the appropriate constraints for the app’s current state.

对此,我们要分两种情况来分析我们到底应该怎么布局我们的约束代码.

一、当Cell的数据发生改变,视图的约束布局也不会发生改变,也就是说视图的布局从一至终都是一致的,这时候,我们应该在 addSubView 之后使用 mas_makeConstraints 直接添加我们的约束.

二、当Cell的数据发生改变,视图的约束布局可能发生改变,这时候,我们应该在 addSubView 之后使用 mas_makeConstraints 直接添加我们的所有约束.然后在 updateConstraints 方法中使用 mas_updateConstraints 根据具体的数据情况更新布局.这才是较为合理的解决方案.


总结


作为一个才使用一周的我只能体会到这里.说真的,Masonry的使用也不是说一个项目中就一定只能使用 Masonry 不能 Masonry 和Frame 混合使用,我觉得应该根据情况具体分析,具体解决.灵活运用才是解决问题的最佳方案.如果有任何问题,欢迎在评论区指导批评骚栋,谢谢大家了.

参考博客


推荐阅读更多精彩内容