iOS开发Masnory源码解析(一)
前言:
Masonry是开发中经常使用的一个布局轻量级框架。使用Masonry,我们可以在代码中非常简洁的对控件进行布局,控件之间布局的依赖关系也是一目了然。Masonry可以大大的简化了我们的布局代码,比如:

Masonry虽然是一个轻量级框架,但是底层的实现是一个非常典型的链式编程。Masonry使用了工厂模式和大量对block的使用也非常值得我们研究。
本篇文章我们通过对Masonry的几个常见类的分析,来理顺Masonry底层代码的调用流程,进而理解Masonry的实现原理。
一、NSLayoutConstraint 系统自动布局
不难发现,Masonry是对系统自动布局框架的二次封装。在研究Masonry之前,我们了解一下系统布局框架。
系统框架的布局大致过程就是使用NSLayoutConstraint创建几个需要的约束,将它们分别添加到对应的视图上去。例如:

从上图可以看出,我们可以通过创建宽、高、左边距、上边距来这4个约束确定purView在父视图中的位置。
代码中,可以发现我们设置了translatesAutoresizingMaskIntoConstraints这个属性为NO。这是因为这个属性在默认的情况下是YES,我们在设置控件的frame的时候,系统会根据frame为我们自动设置约束。如果我们要自己手动为控件添加约束的话,就需要把这个属性置为NO。在添加的约束的时候我们需要先将视图添加到父视图上去,不然的话会crash。
我们可以进入到创建NSLayoutConstraint约束这个方法中可以看到如下图:

创建一个约束需要7个参数,view1:要约束的对象,attr1:约束的类型(可以点进去看看,是一个枚举,例如NSLayoutAttributeLeft),relation:与参照控件之间的关系(常量),包括等于、大于等于、小于等于(例如NSLayoutRelationEqual 是指等于),view2:参照的控件,attr2(同attr1,例如NSLayoutAttributeRight),multiplier:倍数,c:偏移量(比如距左50)。
创建完约束后,我们需要把它们添加到作用控件上才能生效。在添加约束的时候目标控件需要遵循以下规则:
(1)对于两个同层级 view 之间的约束关系,添加到它们的父 view 上。

(2)对于两个不同层级 view 之间的约束关系,添加到他们最近的共同父 view 上。

(3)对于有层次关系的两个 view 之间的约束关系,添加到层次较高的父 view 上。
(4)对于比如长宽之类的,只作用在该 view 自己身上的话,添加到该 view 自己上。
以上就是使用系统NSLayoutConstraint创建约束和添加约束的大致过程。万变不离其中,Masonry的最底层就会回到上述系统类的调用。
二、Masonry的几个重要类
先了解一下这个类的大概用处

MASConstraintMaker:Masonry的核心类之一,负责调度约束,添加约束。
MASConstraint:约束的抽象类,是MASCompositeConstraint和MASViewConstraint父类。
MASViewConstraint:是支持AutoLayout真正的约束对象,它包含了创建一个NSLayoutConstraint的必要属性,最后都是这个对象通过MASConstraintMaker的调度添加到对应的作用控件上。
MASCompositeConstraint:其中的一个重要属性childConstraints是可变数组,包含着若干个MASViewConstraint对象,当在布局的时候,会遍历这个数组,逐一在进行install方法的调用,将约束MASViewConstraint对象添加到对应作用控件上。
MASLayoutConstraint:NSLayoutConstraint的封装。
MASViewAttribute:创建NSLayoutAttribute的约束类型以及相关属性。
三、源码分析
我们通过对Masonry的一个简单的使用来逐步分析底层代码的调用过程。

点mas_makeConstraints这个常用方法进去可以看到如下图:

这是一个参数为block的方法,block的实现在外部调用的地方。这个block的参数为MASConstraintMaker对象。
这个方法中首先可以看到设置self.translatesAutoresizingMaskIntoConstraints = NO。在上述对通过NSLayoutConstraint创建约束时有过说明,使用Autolayout创建约束时必须将这个属性置为NO。Masonry是基于Autolayout的,所以也需要将这个属性置为NO。
接下来就是创建一个MASConstraintMaker对象constraintMaker,在block(constraintMaker)的时候调用这个block,然后通过block将这个constraintMaker对象传到外面的调用,也就是图1的make。
然后开始调用make.top。点进去top,可以看到如下方法

接着点进去

这是个代理方法的实现,当然本类对象也可以直接调用。在这个方法中可以看到创建了一个layoutAttribute为NSLayoutAttributeTop的一个约束(initWithFirstViewAttribute方法中保存了一个_firstViewAttribute)。由于在图3中传进来的constraint是一个nil,所以代码会走到第二个if里,指定newConstraint的代理为本类对象,然后返回newConstraint。点进去delegate可以看到MASConstraint遵循两个协议如下图:

上图4返回一个newConstraint对象后接着会调用.left,也就是调用到make.top.left(实际上是newConstraint.left)。这个时候left的调用者是MASViewConstraint类的对象。点击left调用到抽象类MASConstraint的left方法如下图:

调用到上面这个addConstraintWithLayoutAttribute的时候,子类MASViewConstraint这个类已经实现这个方法,所以会调用到MASViewConstraint里的方法:

这就是利用OC的多态来实现的工厂模式。
刚才在图4返回的newConstraint指定了MASConstraintMaker的对象constraintMaker为代理,所以在图7中[self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute]又回到了图4的那个方法中。这一次会走到图4中方法的第一个if条件,创建了一个MASCompositeConstraint的对象compositeConstraint,并把信创建的约束和上一个约束对象传了进去,并指定compositeConstraint的代理为constraintMaker,然后返回这个compositeConstraint对像。在MASCompositeConstraint这个类的initWithChildren方法中可以看出将创建的两个约束都保存在这个compositeConstraint对象中。
这时调用- (void)constraint:(MASConstraint*)constraint shouldBeReplacedWithConstraint:(MASConstraint*)replacementConstraint这个方法,也就是图4的上个方法,将self.constraints里面第一次存放的newConstraint替换成compositeConstraint这个对象
返回compositeConstraint对象后就开始调用.bottom了,也就是make.top.left.bottom(实际上是compositeConstraint.bottom)。同样的道理,会调用MASCompositeConstraint里的方法addConstraintWithLayoutAttribute,并返回他自己。

接着调用下图第二个方法。其中,strongDelegate就是在图4中compositeConstraint指定的代理constraintMaker。

由于compositeConstraint的代理为constraintMaker,所以还会调用到图四的方法中并返回一个新的newConstraint加入到compositeConstraint的childConstraints中(这个时候childConstraints已经包含top、left、bottom3个约束)。
由于图8中返回的是compositeConstraint自己,所以make.top.left.bottom.right就是compositeConstraint.right,这样的话.right会重复图8、图9两个步骤(如果后续链式添加新约束,会一直重复这两步),并将返回的newConstraint加到childConstraints中(这个时候childConstraints已经包含top、left、bottom、right4个约束,已经可以确定该控件的位置了)。
约束创建完之后,可以发现make.top.left.bottom.right后面链式调用了equalTo方法。点进去查看

可以看出这是一个返回值为一个返回值为MASConstraint的block的方法(很拗口,就是这个方法返回值为一个block,而这个block的返回值为一个MASConstraint对象),之后会调用MASConstraint的子类MASCompositeConstraint的方法equalToWithRelation(因为make.top.left.bottom.right返回的还是compositeConstraint本身)

这个方法里面会遍历childConstraints的4个MASViewConstraint对象(前面已经分析出了),每个MASViewConstraint对像都会调用MASViewConstraint里面的equalToWithRelation方法,如下图

根据图1可知,attribute是self.view,equalToWithRelation是NSLayoutRelationEqual,所以会走到该方法的else中。点击这个setSecondViewAttribute的方法中可以看到代码会走到如下图的箭头那个地方去创建了第二个MASViewAttribute,也就是_secondViewAttribute

这个时候4个约束已经创建和配置好了,下一步就是装载这4个约束了。
图2中constraintMaker调用install方法

走到箭头所示的位置,上面分析可知constraintMaker里的MASConstraint对象被替换后是MASCompositeConstraint对象,所以[constraint install];实际上调用的是MASCompositeConstraint里面的install方法

把self.childConstraints里面4个MASViewConstraint对象遍历后分别install(MASViewConstraint里的install方法)


这个方法可以清晰的看出MASLayoutConstraint创建约束终归调用到了系统自动布局的方法,系统创建约束的7个参数我们也依旧准备好了(后面的两个参数默认下都是0,下一篇文章我会补充这个参数传进来的过程)。并且把作用控件self.installedView通过用NSLayoutConstraint添加约束的规则赋好了值。
[self.installedView addConstraint: layoutConstraint];把约束添加到作用控件上。