使用Autolayout也有一段时间了,auto layout的基本概念非常简单,都是围绕约束进行的,API更是只有两个,但是使用起来感觉很麻烦。最近看到我们这边其他部门的应用使用了很多Masonry来处理UI,看起来非常清爽,链式调用看起来非常容易阅读,使用起来非常方便。但是这种之前ASI给的教训非常深刻,尤其这种大规模基础性地使用第三方开源库,需要确保可控才敢用,至少可以读懂代码并且能够局部优化代码,这是我认为的可控。尤其是这种非常基础的类库,会分布在各个模块之中,一旦出现不兼容,几乎是无法重写的。于是花了一两天时间,把这个代码研究了一下,发现比我想象中要好一些,代码设计地也非常简洁巧妙。
一. 源文件说明
1. MASConstraint: 这个是虚类是用来实现链式调用的父类。MASConstraint子类可以用来表示单独的一个NSLayoutConstraint约束(MASViewConstraint)或者一组NSLayoutConstraint约束(MASCompositeConstraint)。
2. MASConstraint+Private.h: 用于隐藏MASConstraint的私有方法,这些私有方法不会被外部调用者获取,但是类库内部却可以得到,这是个很好的设计模式,在继承中相当于protected。通过定义了较为重要的MASConstraintDelegate代理。
3. MASCompositeConstraint: 这个类用于表示一组NSLayoutConstraint约束,内部包含一个MASViewConstraint的数组做为childs。当调用这个类的类似equalTo或者install等方法的时候,这个类就会调用它的childs所对应的方法。这个类相当于一个MASViewConstraint容器,用于可以方便进行多个属性的操作,可以大大减少工作量。
4. MASViewConstraint:这个是Masonry DSL语法的核心解析类,用来表示对应NSLayoutConstraint,并将表示的属性在install的时候解析为对应的NSLayoutConstraint类,并加入到对应View中。
5. MASConstraintMaker: 这个是整个DSL过程的控制中心,控制整个添加过程。MASViewConstraint和NSCompositeConstraint都在这个maker中生成,maker并且会管理这些constraint的引用,在合适的时候,将这些constraint解析出来。重要的核心方法是实现了 MASConstraintDelegate 中的这些方法,这几个方法中是生成constraint的核心方法。注意,NSViewConstraint类如果没有加入到NSCompositeConstraint中,它的MASConstraintDelegate是maker;如果它是NSCompositeConstraint的child,则它的delegate是MASCompositeConstraint,但是最终还是会之中maker中的delegate方法。而MASCompositeConstraint的MASConstraintDelegate是maker。注意这个类提供的语法糖,用于方便地进行一组约束的操作,例如edges/size/center等属性。
6. View+MASAdditions:使用mas_makeConstraints/mas_updateConstraints/mas_remakeConstraints等核心入口方法来设置约束,最常用的的核心是mas_updateConstraints,一般来说,使用这个方法即可。
7. MASViewAttribute:用来封装UIView和它的NSLayoutAttribute属性,简单的hold这些引用。
8. MASLayoutConstraint:简单继承NSLayoutConstraint
9. MASUtilities.h:这个文件有个点需要注意,这里定义了MASBoxValue宏,用来将类似int/CGRect/CGSize等值使用NSValue进行封装,变为NSObject对象,可以使MASConstraint的类似equalTo等函数有一个便捷函数mas_equalTo可以使用,例如maker.left.equalTo(@(100))可以写为 maker.left.mas_equalTo(100)。mas_equalTo实际上是#define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__)))宏。
10. NSArray+MASAdditions:一些辅助方法。
二. 代码阅读
通过下面的代码的执行过程说明一下masorny的执行过程
[testView mas_updateConstraints:^(MASConstraintMaker *make) {
make.top.left.width.height.equalTo(@(100)).priorityHigh();
}];
先说block中的内容执行:
1. make.top:
MASConstraintMaker的top方法 -> addConstraintWithLayoutAttribute:方法 -> [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute] ,可以发现最终调用的是MASConstraintMaker的MASConstraintDelegate的constraint:addConstraintWithLayoutAttribute:方法,在这个方法的实现内,生成了MASViewConstraint对象作为返回值。可以说这个方法是链式调用的核心方法,由于constraint为nil,所以这里执行的代码很简单。注意这里的把newConstarint的MASConstraintDelegate设置为self,也就是说新生成的MASViewConstraint的delegate是MASViewConstraint。所以make.top的返回值是MASViewConstraint类。
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
….
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
2. make.top.left:
这里的left执行的是MASViewConstraint的left方法,接着调用MASViewConstraint的addConstraintWithLayoutAttribute:方法:
- (MASConstraint *)left {
return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
注意,这里的返回值,不是self,而是addConstraintWithLayoutAttribute方法的返回值。而MASViewConstraint的这个方法的实现如下:
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
}
之前我们知道这个MASViewConstraint的delegate就是MASConstraintMaker,所以会再次执行
- (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint {
NSUInteger index = [self.constraints indexOfObject:constraint];
NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint);
[self.constraints replaceObjectAtIndex:index withObject:replacementConstraint];
}
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
if ([constraint isKindOfClass:MASViewConstraint.class]) {
//replace with composite constraint
NSArray *children = @[constraint, newConstraint];
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self;
[self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint];
return compositeConstraint;
}
if (!constraint) {
newConstraint.delegate = self;
[self.constraints addObject:newConstraint];
}
return newConstraint;
}
由于constraint参数为MASViewConstraint类,这里我们可以看出来,最终的返回值不再是MASViewConstraint,而变成了MASCompositeConstraint类,这个类不仅仅包含left约束(MASViewConstraint),同时也会把top约束(MASViewConstraint)也加入进去,同时会把maker中的constraints数组中top约束(MASViewConstraint)删掉,替换为刚刚生成的MASCompositeConstraint对象。注意,在MASCompositeConstraint对象的initWithChildren方法中,会把所有的child的delegate设置为MASCompositeConstraint对象,而MASCompositeConstraint的delegate则会变成maker。
所以,我们可以看出,make.top.left的返回值是一个MASCompositeConstraint对象,里面包含了top和left约束对象(MASViewConstraint)。
3. make.top.left.width:
这里的width是执行MASCompositeConstraint对象的width方法,接着调用MASCompositeConstraint的addConstraintWithLayoutAttribute:方法
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
[self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
return self;
}
这里一定要注意,跟MASViewConstraint和MASConstraintMaker的addConstriantWithLayoutAttribute方法不一样,这里的返回值,不是新生成的MASConstraint对象,而是self,也就是还是这个MASCompositeConstraint对象。这个类调用的是MASCompositeConstraint的方法:
- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
id strongDelegate = self.delegate;
MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
newConstraint.delegate = self;
[self.childConstraints addObject:newConstraint];
return newConstraint;
}
在这个方法中,最终还是会调用delegate的方法,也就是MASConstraintMaker的方法,上面我们可以看到在MASConstraintMaker的这个方法中,只执行了一部分代码:
- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
...
return newConstraint;
}
所以在maker中仅仅是生成MASViewConstraint而已,最终这个对象会被加入到MASCompositeConstraint的child中,并且这个MASViewConstraint的delegate会被设置为MASCompositeConstraint。
所以make.top.left.width执行返回的仍然是make.top.left时生成的MASCompositeConstraint对象,只是会为width生成一个MASViewConstraint约束,并且加入到MASCompositeConstraint对象的child中,并不会把这个约束加入到maker的constraints数组中。
至于,make.top.left.width.height是跟这个一样的执行步骤,不再赘诉。
4. 至于equalTo的调用过程,是使用OC的Dot notation语法,使用这个语法可以直接调用无参数的方法。然后直接使用返回的block进行调用,实现了对约束的设置。
5. mas_updateConstraints方法中,上述block中的内容执行完之后,会调用maker的install方法,这个方法中,会遍历maker中constraints数组,然后调用MASConstraint的install方法。而MASCompositeConstraint对象的install方法会直接调用其child的install方法,并不会额外进行其他操作。所以最终的核心操作是MASViewConstraint的install方法,这个方法中,会生成NSLayoutConstraint约束,然后添加到对应的view中。这个地方其实比较简单,看代码即可。
三. 注意点
1. 通过链式调用可以设置多个属性,比如maker.left.right.top等,这些方法会返回一个MASCompositeConstraint对象,代表left/right/top这些约束的所有MASViewConstraint对象都会被添加到MASCompositeConstraint之中,通过这种方式实现链式调用,可以大大减少代码量。同时通过maker提供的center/edges/size函数也返回MASCompositeConstraint对象,里面同时包含了MASViewConstraint对象,也可以实现同时设置多个属性。注意MASCompositeConstraint源文件中的这段代码,注意这里返回的不是新生成的MASViewConstraint对象:
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
[self constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
return self;
}
四. 代码阅读心得
1. 使用NSException来解决OC中没有虚函数语法的问题,如果子类未实现虚方法,则直接抛出异常;在iOS中,我们对异常的使用远远不足,思路太过局限于C的思路,比如说出错之后,往往使用返回-1这种方式解决,这点在处理一些比较突出的异常时就非常不够用。例如,在JSON解析的时候对应的数据不对的问题,这个问题再跟服务器约定之后,一般不会出问题,但是一旦遇到问题就对客户端的用于体验造成非常不好的影响,现在的思路是可以使用@try{}@catch来解决,后面会在这方面做一些尝试。
2. 大量使用NSAssert断言工具来调试代码,我自己这方面之前更加喜欢使用if来避免进行各种容错,虽然这种方式,可以避免crash等严重问题,但是开发过程中,会降低开发效率。
3. 对block的灵活使用,这部分也是我最近思考比较多的地方,之前我对block的使用也觉得较多,但是现在看来,使用深度和灵活度都不够,一方面用来替代delegate做回调,这种方式非常灵活便捷,并且逻辑清晰。但是看masonry中的时候,才发现使用block做返回做参数,可以把写代码的过程变得简洁很多,在masonry中,equalTo这些方法的实现就是最好的例子。
4. 类似MASConstraint+Private.h的方式使用category实现类似protected的内部方法继承,这个也比较有意思,也是我之前一个挺困扰的问题,在写类库的时候,可以避免公开接口混乱以及过大的问题,从而很好地划分公开以及内部方法。
5. 类库的封装,调用时要足够简单便捷,通过隐藏大量的内部实现细节,只暴露必要的部分,这样第三方使用者使用起来才不会产生困惑。这部分我自己做的不足,很多时候写代码和类库的时候,写的过于冗余和啰嗦,这也可能是OC没有命名空间带来的习惯影响,后续转向swift之后肯定会大大改善这些问题。
6. Dot Notation语法的使用,也是masonry可以实现链式调用的基础语法,使用dot notation调用方法(无参数方法)。结合block做返回变量来进行使用,就可以把代码变得非常简单,这种方式就实现所谓的函数式编程。注意,为了更好地维护代码规范,dot notation调用无参数方法这个语法要慎用,一般来说,方法调用就要使用”[]"语法,只有变量使用 “.”,可以避免混乱,增加代码的可阅读性。参考:http://stackoverflow.com/questions/2375943/objective-c-dot-notation-with-class-methods,http://underscorem.org,还有就是参考MASViewConstraint的equalTo等方法的实现代码。