iOS开发中用户界面开发很重要的一部分,也是工作量很大的一部分,将系统提供的控件或自定义控件在界面做精准布局是一项基础而重要的工作,原始方法就是使用系统提供的frame进行坐标及大小布局定位,这也是早期开发人员常用的方法,但随着iPone手机屏幕的大小多样化,使用frame绝对布局也越来越难以满足实际的工程开发,这时候就需要借助自动布局来更优雅的解决多种屏幕适配问题,同时大大减轻工作量。此时苹果也不失时机的退出了自动布局的API,也就是NSLayoutConstraint的相关使用。
首先具体分析一下苹果的原生实现:
看一个具体例子:
其核心代码主要是这行
NSLayoutConstraint其实描述的是两个view之间的关系,其中第一个参数view1 是将要被约束的对象,参数view2是提供约束的依据,ralation、attr2、multiplier、constant提供了约束关系,其公式为:view1.attribute1 = multiplier × view2.attribute2 + constant,其中attribute1和attribute2分别为NSLayoutAttribute类型的参数,
可以看出这是一个枚举类型,提供了约束的元素,在代码示例中attr1和attr2都是NSLayoutAttributeTop,也就是说都是依据上边缘加约束的,relation是NSLayoutRelation类型的,
表示了attr1和attr2之间的关系,示例中使用NSLayoutRelationEqual表明是完全equal的,multiplier为CGFloat类型的,表示了乘数也就是倍数关系,constant表示偏移量。
在这个示例中,subView是被约束对象,self.view就是约束的依据,整个约束意思就是,subView的上边缘距离self.view的上边缘向下偏离40个point。这行代码最终就是构建这样一个NSLayoutConstraint类型的约束,然后加到数组array中,最终调用[self.view addConstraints:array];把这些约束加到父视图上。
ok,原生API的约束就是这些,但从代码看来明显比较繁琐复杂,不过无关紧,masonry的横空出世为我们提供了简便易用的API,在理解了autolayout的原理后在工程中直接使用masonry即可。
接下来就是本文要重点讲解的masonry源码。
首先,还是来大致浏览一下masonry的框架结构:
接下来,还是因循旧例,从一个API的简单调用为入口逐步分析其工作原理。
上面是对self.collectButton加的一个约束,self是其superview,下面开始逐行分析其代码。
首先看mas_makeConstraints这个方法:
可以看出这个方法是View+MASAdditions分类的方法,设置self.translatesAutoresizingMaskIntoConstraints = NO;意思是将绝对布局转换为自动布局,也就是如果使用了setFrame的方法,会将其自动转化为autoLayout。
紧接着,MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];这行是构建一个MASConstraintMaker *类型的对象constraintMaker,下面仔细分析一下这个类以及其构造方法:
从源码注释和类提供的接口以及使用的方法中,都可以看出这个类主要是用来构造MASViewConstraint约束的。
再看构造方法:
可以看出构造方法中主要是将加约束的view传递给maker,并初始化一个数组constraints,初步可以看出这个数组是用来存储约束的。
紧接着就执行block(constraintMaker);方法了,再回头看这个block。
这里使用了链式调用,使Objective-C语言在这里实现了函数式调用,这是massory很经典的一部分。
以第一行为例make.right.equalTo(self).offset(-YZLAYOUTRATE(90));点进去继续分析:
可以看出是MASConstraintMaker类先声明一个MASConstraint类型的只读属性right。然后在其getter方法中调用[self addConstraintWithLayoutAttribute:NSLayoutAttributeRight];方法返回这个对象。
进入这个方法:
其参数是上文提到的原生接口的约束对象类型NSLayoutAttribute,这里传的也是对应的NSLayoutAttributeRight。再进入:
首先构建一个对象MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];进入可看到:
这个MASViewAttribute *viewAttribute对象其实就是记录一个视图控件view和一个约束项,相当于将一个view和layoutAttribute绑定起来,也就是在这个view上加上这样一个约束。
紧接着这构造一个对象MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]。进去可看到:
可以看出MASViewConstraint类是对NSLayoutConstraint的封装管理,layoutPriority是约束等级,默认是MASLayoutPriorityRequired,其实对应的就是原生接口中的UILayoutPriorityRequired,layoutMultiplier为倍数,也就是原生API中的参数multiplier。
紧接着
从前边的调用可知constraint为空,所以直接走下面一个选择支,将刚创建的newConstraint的id<MASConstraintDelegate>类型的delegate赋给maker,并将其加入到make初始化话创建的self.constraints数组中。最后将newConstraint返回,这样回到从前make.right.equalTo(self).offset(-YZLAYOUTRATE(90));中make.right就返回了。
下面接着分析这个链式调用中make.left.equalTo(self).offset(YZLAYOUTRATE(18));中的equal部分。点开这个方法:
首先可以看出这一步的链式调用实现是由于make.right返回一个MASConstraint*类型的对象,而这个类有一个equalTo的成员方法,因此可以使用点语法实现这一步的链式调用。从源码和注释中也可以看出这一步主要是设置relation关系为NSLayoutRelationEqual,对应也就是原生API中的relation参数。进一步查看其实现,发现这一步返回的是一个MASConstraint * (^)(id)类型的block。这一步就比较绕了,要仔细分析。
下边烧脑的游戏就开始了,看官且耐心听,首先是上一步返回的MASConstraint * 类型的constraint对象调用equalTo方法返回一个MASConstraint * (^)(id)的block,可看出这个block有一个id类型的入参attr(从注释中看,其类型可以为MASViewAttribute, UIView, NSValue, NSArray这几种类型),一旦返回这个block便立即调用,在这个示例中是将self传了进去(书中代言,self其实是这个视图的superView,符合参数要求),这一点是我理解时间比较长的时间,刚开始看时发现equalTo方法没有参数,如何后边跟着参数,后来发现是直接调用equalTo方法返回的block,贻笑大方了。
下面边开始分析其返回的block调用:
由于传入的是UIView*类型的attr,所以直接走else的选择支,将relation也就是默认的NSLayoutRelationEqual赋给self.layoutRelation,需要仔细分析的是self.secondViewAttribute = attribute;
同上,直接走第二个选择支:_secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
再次来到这个方法:
是否觉得似曾相识呢,对的就是第一个链式调用make.right的调用中调用过的,完成对@property (nonatomic, strong, readonly) MASViewAttribute *firstViewAttribute的赋值,而这里完成对@property (nonatomic, strong, readonly) MASViewAttribute *secondViewAttribute;的赋值。前面讲过,MASViewAttribute其实就是将一个视图控件view和一个约束绑定起来,从调用的参数可以看出是_firstViewAttribute的layoutAttribute,在这个实例中也就是NSLayoutAttributeRight。
然后回到block的执行方法,最后return self;将MASConstraint *类型的constraint返回供下一步调用。
现在走到make.right.equalTo(self).offset(-YZLAYOUTRATE(90));这行链式调用的最后一个调用.offset(-YZLAYOUTRATE(90))。
同equalTo方法一样,也是返回一个block并立即调用,这个block的入参是个CGFloat类型,所以参数传入(-YZLAYOUTRATE(90)(其实最终也是个CGFloat类型)。现在来看这个block,首先调用self.offset= offset;,其实是多态调用了
最终设置@property (nonatomic, assign) CGFloat layoutConstant;这个参数。
到此为止第一行的链式调用都分析完了,总结一下就是:先通过maker构造一个MASViewConstraint *类型的constraint,然后将constraint加入maker的数组constraints,然后通过一系列的链式调用将一个约束的各个参数加到constraint,maker其实是构造器,每个constraint都是一个约束的的抽象。
紧接着在回到工程中的调用,希望同学们不要迷路
紧接着又是两行类似的链式调用,可以看出第二行链式调用和第一行的相类似,是根据top这个属性加约束的。最后一行是约束其size的,下面具体分析:
进入make.size:
不同于上两行的约束,这里使用的是composite attributes组合约束,下面分析其代码:
除去系统版本判断及安全性相关部分,由于传入的参数是MASAttributeWidth | MASAttributeHeight,所以会进入if (attrs & MASAttributeWidth) [attributes addObject:self.view.mas_width]; if (attrs & MASAttributeHeight) [attributes addObject:self.view.mas_height];这两个选择支,简单看一下self.view.mas_width中代码:
同样调用的是View+MASAdditions类别中方法,和前边相同返回是一个MASViewAttribute *类型的对象,其_layoutAttribute属性是NSLayoutAttributeWidth,self.view.mas_height相同。接着走到这里:
同上两行的链式调用相同,构造一个constraint并将其加入到self.constraints中,最后返回constraint。
下面重点分析一下后边make.size.mas_equalTo(CGSizeMake(YZLAYOUTRATE(42), YZLAYOUTRATE(42)));的mas_equalTo部分:
这里是一个宏,继续展开这个宏:
可以看出这个宏主要作用就是将基本数据类型转化为NSObject*类型,这样我们在工程中就可以直接调用基本数据类型,比如这里make.size.mas_equalTo(CGSizeMake(YZLAYOUTRATE(42), YZLAYOUTRATE(42)));直接传入的就是CGSize类型,会在这个内联函数中被转化为NSValue类型,然后回到这个宏:#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__))),可以看出和上两行的链式调用中的equalTo是相同的不再赘述。
以上block中代码都已经分析完毕,再次回到工程中调用
是否还有印象呢,这就是文章伊始分析这个框架时刚进入框架时的分析,现在mas_makeConstraints函数中前三行已经分析完。
现在要分析最后一行return [constraintMaker install];
self.removeExisting默认是NO,至于啥时候为YES下边会有具体分析,这里暂且继续往下走,
self.updateExisting默认也是NO,至于啥时候是YES下边也会有分析,现在继续往下走,
就进入[constraint install]这个方法。
这是最后一个难啃的重要方法,想着即将吃透框架的喜悦,且暂且平复激动的心情逐行分析这个方法:
self.hasBeenInstalled默认是NO。
在这个示例中此时self.layoutConstraint还不存在,所以不进入选择支。继续往下走,
这里大家是否看到熟悉的身影,回到文章开篇那个原生自动布局的示例:
这里就是根据之前构造的MASViewConstraint *类型的contraint对象构造一个约束layoutConstraint。不用想,下一步肯定就是要把这个layoutConstraint约束加到具体的view视图上。
在这个示例中进入第一个选择支,进入这个方法:
这个方法是用来寻找距离self.firstViewAttribute.view和self.secondViewAttribute.view最近的共同superview。可以猜想一下步是不是要将约束加到这个superview上。紧接着便有self.installedView= closestCommonSuperview;这个赋值。果然是要将这个约束添加到这个共同superview上。往下走:
走到第二个选择支:[self.installedView addConstraint:layoutConstraint];将约束添加到共同superview上。然后将self.layoutConstraint= layoutConstraint;赋值是回应这个方法开头
防止反复布局同一个约束减少性能消耗。就此这个方法也就分析完了。再回到
在for循环中将所有的约束全部添加,然后方法结束回到
就此示例
这个约束添加过程全部分析结束。
下边再额外分析两个重要的API,同时回答前边两个问题。
这个方法是更新之前的约束。可以看到在这里将这个constraintMaker.updateExisting=YES;属性设置为YES。
这个方法是删除之前建立的约束,从新创建约束,可以看到在这里将这个属性constraintMaker.removeExisting=YES;设置为YES。
就此这个框架的源码的一个分析就先到这里,日后有时间可以继续补充细节。