vlambda博客
学习文章列表

Masonry框架源码分析

相信大多数iOS开发者对Masonry框架并不陌生 , 本文是笔者通读Masonry的代码之后的一篇总结, 也希望可以帮助大家更好的理解该框架. 怎奈笔者才疏学浅, 如有遗漏或错误也欢迎大家评论区指出, 大家一起进步!

 

iOS布局的演进

在说Masonry, 先简单介绍一下iOS开发屏幕适配的发展过程. 在iPhone3Gs/4/4s时代, 手机屏幕尺寸都是一样的, 对于开发者来说基本不用适配,彼时的屏幕布局基本都是采用frame, 但是随着iPad的出现, frame布局便不能满足需求, 苹果开始推出AutoResizing布局, 这种布局核心内容就是: 以父容器为参照物来对子空间进行frame布局, frame不再是直接写死的值, 而是可以根据父视图的大小变化, 但是这种布局方式的缺点也很明显, 就是不能设置兄弟视图之间的关系, 所有苹果煞费苦心的推出了AutoLayout, AutoLayout的出现基本弥补了AutoResizeing不足, iOS开发的屏幕适配变得更加轻松.

 

苹果原生AutoLayout布局与Masonry比较

虽然苹果的初衷很好, 但是无奈苹果的NSLayoutConstraint布局实在是太过于臃肿了, 所以在github开始涌现出各种各样的三方布局框架, 其中就有今天的主角Masonry. 笔者截取了部分布局代码大家感受一下

 
   
   
 


上面只是对一个空间进行的代码, 而对于iOSAPP来说, 每个页面数个或数十个View都是很常见的事, 如果采用这种布局方法, 估计整个类里面全都是布局代码了, 这必然会给代码的阅读与维护带来很大的不便.

其实苹果还有一种自动布局的方式相对与上面的布局方法稍微好一点, 那就是VFL布局, 感兴趣的可以去了解一下, 这里贴上我之前总结的一个帖子 iOS开发之VFL布局总结

然后大家看一下使用Masonry实现上面同样的功能的代码

 
   
   
 


综合以上两个示例对比, 高下立见, 使用Masonry布局代码简洁美观, 使用原生布局代码臃肿不堪,所以如果你是使用代码来进行自动布局, 有什么理由不用Masonry呢!

 

 

Masonry源码分析(正题)

Masonry框架整体来说并不是一种新的布局方式, 它仅仅是对NSLayoutConstraint做了一层封装, 所以对于框架背后必然还是要进行一堆原生的代码操作, 所以我们才需要进行一窥究竟!

我们按照调用顺序来介绍来一一介绍

1. View+MASAdditions

View+MASAdditions 此文件看起来内容很多, 但是仔细观察, 主要分为两部分, 第一部分就是给View扩展属性(mas_left, mas_right, 等属性), 第二部分就是给View扩展方法( mas_makeConstraints: 等方法), 为了便于分析文件精简如下

 
   
   
 



由于后面三个方法实现几乎一样, 所以这里只对mas_makeConstraints方法进行简单的分析

 
   
   
 


要使用AutoLayout第一件事就是要关闭当前View的translatesAutoresizingMaskIntoConstraints,

接着通过工厂类MASConstraintMaker生成了一个constraintMaker, 这个也就是我们在Block回调中调用make实例, 然后执行Block代码块, 在代码块中make分别去执行.top, .left等操作完成 最后执行install操作;

2. MASConstraintMaker

在执行block(constraintMaker) 实际上就是再执行make.top.left.bottom.right.equalTo(self.view);

我们来看make.top的实现原理, 这里个也是Masonry巧妙的地方, 利用block的特性实现了链式调用.

 
   
   
 


按照顺序分析, 当调用make.top是最终会来到 - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute 方法中, 这个方法做了一下几件事

第一件事生成viewAttribute属性, 然后通过viewAttribute生成newConstraint这两个实例所对应的类在后面介绍, 这里先标记上, 因为调用constraint参数传递的是nil 所以这里第一个if语句不执行, 直接执行第二个if分支里, 在第二个分支中,给新创建的newConstraint设置代理, 然后将newConstraint添加到maker所持有的数组中, 到这里第一个属性top就完成了记录.

整体来看就是调用top方法会生成一个top的约束, 然后将这个约束添加到maker所持有的数组constraints中

 

 

3. MASConstraint

MASConstraint实际上是一个抽象类, Masonry巧妙地使用了面向对象的多态特性进行编程. MASConstraint类中定义了很多抽象方法都需要在子类中实现, 这里摘取几个例子如下

 
   
   
 


上面摘取的这些方法是MASConstraint中的实现, 我们可以看到方法体都是直接调用了一个宏MASMethodNotImplemented(), 我们们顺着这个宏发现, 实际上就是一个抛出错误的处理, 如果子类不实现这个方法, 则调用时就会来到父类的这个方法中最终抛出错误, 间接达到java中抽象类的效果! 需要注意的是这个错误是运行时错误, 所以如果不调用依然是无法发现错误的.

这里我们依然使用 make.top.left.bottom.right.equalTo(self.view) 这个例子来将进行分析, 第2部分讲到make.top最终是调用到MASConstraintMaker中的 - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute 而make.top的返回值大家可以看到实际上是MASConstraint, 因此当make.top再接着调用.left的时候已经变成了MASConstraint 的实例进行.left的调用, 然后我们来看MASConstraint中的left方法(这里再强调一下MASConstraint 中的链式调用, 比如.left 实际上是走的left的Getter方法, OC的语法糖可以让我们实现用点语法替代get方法, 而该方法返回值又是MASConstraint 类型, 所以可以实现链式调用)

 
   
   
 


我们可以看到.left, .top等方法实际上最终都会调用 addConstraintWithLayoutAttribute方法, 而在MASConstraint中该方法实际上是一个抽象方法, 并无实质的内容实现, 所以很明显这个问题我们需要放到子类实现中来说了

  • MASViewConstraint

    看头文件实现, 我们可以得知MASViewConstraint 继承了 MASConstraint, 因此MASViewConstraint拥有父类的所有特性, 因为父类在上面已经介绍过, 这里只说明一下MASViewConstraint 特有的东西

       
         
         
       


    因为之前还未介绍MASViewAttribute, 所以这里先大概说一下, MASViewAttribute类实际上是约束的模型, 主要用来记录约束内的关系, 如果不太明白, 你可以看一下第4部分MASViewAttribute的介绍, 然后再回来看这个.

    我们看到MASViewConstraint中有两个 属性firstViewAttribute 和 secondViewAttribute, 关于这两个属性我们来看一下原生的自动布局实现我们就明白了

       
         
         
       


    通过上面的代码片段, 我们可以发现, 约束是有两部分组成的, 也即是第一个view的某个约束属性, 和第二个View的某个约束属性的关系, firstViewAttribute实际上就是用来存储blueView和约束NSLayoutAttributeLeft secondViewAttribute 中记录的是self.view和对应的NSLayoutAttributeLeft.

    在描述MASConstraint 中提到.left/.top等这些方法实际上最终会调用到 - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute , 而这个方法是抽象方法,在MASConstraint中并没有实际实现, 所以我们接着说这个方法

       
         
         
       


    我们可以看到MASViewConstraint中- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute, 直接调用了代理方法 constraint:addConstraintWithLayoutAttribute:, 看到这里可能稍微有点绕, 比如这个delegate是谁, 在哪里设置的这个delegate, 没关系慢慢分析还是可以找到线索的, 我们再回到 make.top的最终调用, 如下

       
         
         
       


    仔细看, 在方法内部最后一个if分支中正是给newConstraint设置代理的代码, 也就是说 MASViewConstraint 的代理实际上就是MASConstraintMaker, 所以当make.top再去调用.left的时候, 实际上最终还会来到MASConstraintMaker中上面这个方法来添加属性, 我们来一步一步的分析这个方法, 由于MASViewConstraint中 调用代理方法时第一个参数constraint并不为nil 所以, 上面这个方法调用会和在MASConstraintMaker中直接调用有所不同.

    make.top的返回值是一个MASViewConstraint类型, 所以这里直接进入了第一个分支, 在第一个分支中创建了一个MASCompositeConstraint类型的实例(这个类接下来会分析), 然后return, 结束了方法调用!

  • MASCompositeConstraint

    这个类是MASViewConstraint子类一个约束组合类, 作用就是把多个约束组合在一起, 当 make.left.top执行结束后实际返回值类型是MASCompositeConstraint 接下来再接着执行 make.left.top.bottom时, 实际上是执行的MASViewConstraint中的bottom方法, 最终会调用子类MASCompositeConstraint 里面的 - (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute 方法, 我们来看这个方法的实现

       
         
         
       


    通过分析上面的代码我们可以发现, 最终还是要调用 MASConstraintMaker 中的添加约束的方法中, 这里就不再重复, 最后返回的依然是 MASCompositeConstraint类型的约束, 然后接着执行 make.left.top.bottom.right 返回MASCompositeConstraint类型的实例;

     

接下来, 分析equalTo方法, 在父类MASConstraint中定义并实现了equalTo方法, 但是方法实现实际上调用的是equalToWithRelation方法, 而这个方法在MASConstraint类中做了一个空实现, 并且要求子类分别实现, 所以我们分别来看 MASViewConstraint 和 MASCompositeConstraint中的实现

 
   
   
 


观察上面的两个类中的equalToWithRelation方法的实现, 我们可以发现MASCompositeConstraint 的实现 最终就是用自己持有的childConstraints中的各个constraint去掉用equalToWithRelation, 所以这里最终还是执行到MASViewConstraint中去, 所以我们只需要看MASViewConstraint中equalToWithRelation的实现!

由于要实现.equalTo(xxx)这样的函数式编程, 所以equalToWithRelation内部返回的是有个有参数的block, 这样外部调用时就可以达到函数式编程的效果, block返回值是MASConstraint类型以达到链式调用的效果.

bolck内部有个if分支 判断条件是[attribute isKindOfClass:NSArray.class], 这里笔者还有一点疑惑, 暂时没有找到什么时候会来到这个分支, 所以也请各位看官读者指点迷津, 在分支的else语句里 执行了self.secondViewAttribute = attribute; secondViewAttribute属性前面已有描述这里不再赘述, 不过有一点需要着重说一下, 就是secondViewAttribute的Setter方法, 这里mas的作者巧妙的实现了Setter方法, 我们可以来做一下分析

 
   
   
 


在这个方法中对secondViewAttribute进行了三个种类判断, 如下:

1.判断是不是NSValue类型则说明是直接对第一个约束设置了常量, 例如: make.width.equalTo(@100);

2.判断是不是MAS_VIEW(是一个宏, 在iOS开发时对应的是UIView), 如果是, 则创建一个secondViewAttribute, 而secondViewAttribute的layoutAttribute和firstViewAttribute的layoutAttribute,

3.判断是不是MASViewAttribute, 如果是则直接设置给属性secondViewAttribute;

综合以上三点, 所以我们的equal()方法中可以是NSNumber, 也可以是view.mas_xxx, 也可以直接是一个view类型

 

通常调用Masonry还有一种形式是这样的: make.left.equalTo(view).offset(10); 前面内容都有分析, 现在单独看.offset()方法; 在MASConstraint类中offset定义依然是一个block属性, 但是这里稍有不同, .offset()实质上调用的是offset的getter方法如下:

 
   
   
 


看完以后可能会有点蒙, 尤其是block内部的实现 self.offset = offset, 为何一个CGFloat的值可以赋值给block类型的属性呢? 这里得说一下, 实际上 self.offset = offset中的self.offset是在调用offset的setter方法, 而在MASConstraint类中的setter方法依然是一个抽象方法, 本类中进行的是空实现, 所以在MASViewConstraint 和MASCompositeConstraint 两个子类中分别进行了实现, 而两个子类中的实现最终都会来到MASViewConstraint中setOffset方法如下:

 
   
   
 


此方法实质就是记录了当前约束的偏移量, 以待后续使用

 

4. MASViewAttribute

MASViewAttribute是用来记录view和要指定的约束的, 它的内容较少, 比较简单, 包含当前view属性,和当前view的指定约束

例如第一个item的left约束, 通过构造方法可以生成MASViewAttribute 实例;

 

5. 约束的安装

通过以上四个部分的分析, 我们已经完成了block代码块中的所有分析, 接下来继续来看 View+MASAdditions 中的 mas_makeConstraints方法, 在方法内部执行完block之后, 紧接着执行 [constraintMaker install]; install方法如下

 
   
   
 


install方法, 首先判断该约束是否已经存在, 如果存在则需要先uninstall, self.removeExisting默认是NO, 因为在执行mas_updateConstraints或者mas_remakeConstraints方法中将其设置为YES, 而这两个方法最终都会调用insatll方法; View+MASAdditions 分类中install方法最后就是遍历 maker持有的constraints数组, 分别进行安装由于数组中的约束可能MASViewConstraint类型, 也可能是MASCompositeConstraint类型, 所以再这两个类中分别有实现install方法, 不过最终调用还是来到MASViewConstraint中的install方法, 这里我们只对MASViewConstraint中的install方法进行分析, 方法实现如下

 
   
   
 


核心内容就是将MASViewConstraint中所持有的数据, 进行解析 ,并调用系统的自动布局方法进行设置约束, 这里不做赘述, 但看下面这段

 
   
   
 


我们调用系统的自动约束布局时, 需要清楚将约束安装到哪个view上, 而上面这段正式找到要安装的view, 按照系统自动约束的规则, 如果是size, 宽高约束需要作用的view本身, 如果是上下左右约束需要找到合适的view上, 所以通过以上判断获取到合适的installedView, 第一个分支中mas_closestCommonSuperview方法是求两个视图的最近父视图, 这个方法可以着重看一下, 这里不再赘述!

 

总结

通过以上分析, 我想对各位读者分析Masonry框架有很大帮助, 还有不少细节需要读者自行分析! 最后,笔者在总结一下Masonry中的重点内容:

  • 链式编程/函数式编程

    这个在Masonry中多数方法都是采用的这个编程方式, 虽然框架内部实现相对复杂, 但是对于调用这来说极其简洁明了, 这个是一个优秀框架最难得的地方; 由于OC的方法调用是用方括号实现, 所以在实现链式编程时相对比较麻烦一点, 但是作者巧妙使用block, 以及OC中Getter和Setter的语法糖(点语法), 在外形上实现了链式编程的效果, 这一点值得学习和深思!

  • 抽象类实现

    由于Xcode并没有抽象类的校验, 所以抽象类类中抽象方法极其容易忽略或者忘记, Masonry作者采用了OC的多多态特性, 在父类中进行了抛出错误的空实现一次来达到父类方法子类必须实现的效果!

  • Setter和Getter方法灵活应用

    在本文第3部分末尾有描述, offset 的 get方法获得获取的Block类型, 而Setter方法传入的是CGFloat, 这里实际上只是巧用OC语法糖实现了self.offset = offset 看起来好像类型都不匹配的代码!