天天看点

Masonry源码分析从mas_makeConstraints开始MASConstraintMakerMASConstraintMASViewConstraintMASViewAttributeInstallMASCompositeConstraint总结

作者:代培

地址:http://daipei.me/posts/source_code_analysis_of_masonry/

转载请注明出处

我的博客搬家了,新博客地址:daipei.me

AutoLayout是个好东西,但是官方的API实在不好用,Masonry应时而生为AutoLayout提供了简洁的接口,我们的项目中的布局全部都是用Masonry,可以说离了它有些寸步难行。

Masonry使用起来是十分简单的:

[self.aView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.equalTo(self.view);
        make.top.equalTo(self.view.mas_top).offset();
        make.width.height.mas_equalTo();
}];
           

从mas_makeConstraints开始

Masonry中使用最多的就是

mas_makeConstraints:

这个方法,这是用于第一次添加约束时使用的方法,关于设置约束,一共有三种方法:

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

从方法名可以很容易看出这三个方法分别是什么作用,第二个方法是更新约束时使用的,第三个方法是重新添加约束时使用的,也就是以前的约束不需要时完全重新设置约束,需要注意的是如果要重新设置约束一定要用第三个方法,连续调用第一个方法容易引起约束的冲突,虽然程序不一定会crash。

这三个方法会返回一个数组,这个数组中是新添加的约束,不过我从来没有用到过这个返回值,如果不是看源码,其实都不知道这些方法是有返回值的。

下面看一下

mas_makeConstraints:

的实现:

- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
    self.translatesAutoresizingMaskIntoConstraints = NO;
    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    block(constraintMaker);
    return [constraintMaker install];
}
           

首先将

translatesAutoresizingMaskIntoConstraints

这个属性设置为NO

By default, the autoresizing mask on a view gives rise to constraints that fully determine

the view’s position. This allows the auto layout system to track the frames of views whose

layout is controlled manually (through -setFrame:, for example).

When you elect to position the view using auto layout by adding your own constraints,

you must set this property to NO. IB will do this for you.

这句话的意思大致就是最终系统都是用

constraints

的方式来组织视图,但是如果这个设置为

YES

系统会将你设置的Frame之类的属性转换为

constraints

,但是如果你要自己添加约束,也就是如果你要使用AutoLayout的话,就必须将这个属性设置为

NO

。如果你用InterfaceBuilder的AutoLayout,会自动将这个属性设置为

NO

第二步是用当前View来实例化一个

MASConstraintMaker

类型的maker,这里的self是调用

mas_makeConstraints:

的view。

第三步执行传入的

block

中的代码,将刚刚实例化的

maker

传入

block

,用于配置这个

maker

Note:曾经产生过一个疑惑,就是我们在使用Masonry进行布局的时候,在block中都是直接引用self的,为什么不会产生循环引用?看完源码就明白了其中的原因,首先这个block肯定是强引用了self的,假设我们是在一个VC中进行的布局(大多数情况下是这样),这个self就是VC,然后这个VC强引用了调用Masonry接口的View,但是这个view没有引用这个block,事实上这个block没有被任何对象引用,所以这个block在执行完以后就会被释放了,block引用了self,但是self没有直接或间接引用block,所以不会存在循环引用的问题。

最后向这个

maker

发送

install

的消息,将用户设置的约束添加到view上。

MASConstraintMaker

首先看看它的初始化方法:

- (id)initWithView:(MAS_VIEW *)view {
    self = [super init];
    if (!self) return nil;

    self.view = view;
    self.constraints = NSMutableArray.new;

    return self;
}
           

这里的

MAS_VIEW

是一个宏:

#if TARGET_OS_IPHONE || TARGET_OS_TV
    #define MAS_VIEW UIView
#elif TARGET_OS_MAC
    #define MAS_VIEW NSView
#endif
           

这个里使用宏的意图比较明显,Masonry希望不仅仅支持iOS,同时也支持tvOS和macOS

maker

保持了当前view的引用,当然这里的引用是弱引用,虽然强引用也不会引起循环引用,但是这里弱引用其实和合理,因为如果view都不存在了,这个

maker

也没有存在的必要了,view不应该因为maker的引用而引用计数加1。

同时

maker

实例化了一个可变数组

constraints

,这个数组中保存的就是要添加到当前view的约束。

我们看一下当调用

make.left

时会发生什么:

- (MASConstraint *)left {
    return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft];
}
- (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
}
- (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;
}
           

我们看的最终调用的是

-constraint: addConstraintWithLayoutAttribute:

这个方法,我删去了其中暂时无关的代码,不过删去的代码在后面还会提到。

因为传入的

constraint

nil

,所以直接进入这个if判断,在这个判断中将新生成的

constraint

的代理设为

maker

,并将其加入

self.constraints

这个数组中。

最后将新生成的

constraint

返回。

MASConstraint

在上一节中

make.left

就是返回了一个

MASConstraint

对象,下面看一下

make.left.equalTo(self.view)

这句话是怎样调用的:

// MASConstraint.h
- (MASConstraint * (^)(id attr))equalTo;
           

在MASConstraint.h中有这样一个接口,我看了半天才搞明白这是一个什么函数,这是一个返回值为block的函数,返回的这个block的返回值是

MASConstraint

,接受一个

id

类型的参数,我们看它的调用方式:

.equalTo(self.view)

,这其实比较奇怪,因为我们知道OC中方法是不能用点语法调用的,只有属性才可以,所以其实这里可以把

equalTo

理解为一个block类型的属性,让这个方法实际上就是这个block的

getter

方法。

这个方法中的实现是这样的:

- (MASConstraint * (^)(id))equalTo {
    return ^id(id attribute) {
        return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
    };
}
           

这里直接进入了

-equalToWithRelation

这个方法,这是一个抽象方法,由

MASConstraint

的两个子类来实现

MASViewConstraint

MASCompositeConstraint

Note:这里的抽象方法用一种比较有趣的方法来实现,Masonry定义了一个宏叫做MASMethodNotImplemented(),这个宏会抛出一个异常,如果错误的调用了这个抽象方法在运行时就是导致crash,OC不支持抽象方法,但是这里用了一种独特的方式实现抽象方法,还是挺值得学习的。

在这个方法中传入了一个relation的参数比如上面代码中传入的

NSLayoutRelationEqual

,这个参数在后面的布局中是会用到的。

调用不同的方法传入的参数就不一样,比如

-greaterThanOrEqualTo

传入的就是

NSLayoutRelationGreaterThanOrEqual

,而

lessThanOrEqualTo

传入的就是

NSLayoutRelationLessThanOrEqual

MASViewConstraint

先看一下

MASConstraint

这个相对简单的子类,我们关注这个子类是如何实现上述的

equalToWithRelation

这个方法的:

- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
    return ^id(id attribute, NSLayoutRelation relation) {
        if ([attribute isKindOfClass:NSArray.class]) {
            ...
        } else {
            NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
            self.layoutRelation = relation;
            self.secondViewAttribute = attribute;
            return self;
        }
    };
}
           

我暂时省略了第一个判断中的内容,在

else

分支中,首先断言这个

constraint

没有被重定义。

然后设置

layoutRelation

,在

setter

方法中将上面的

self.hasLayoutRelation

标记为

YES

,这里的

relation

在前面说过

最后设置

secondViewAttribute

,看到second自然会想到会不会有first,确实是有的,first就是当前view的attribute,其实这个理解起来不难,一个约束就是描述两个view之间的关系(尺寸约束除外),所以这个

MASViewConstraint

最重要的三个属性就是:

firstViewAttribute

secondViewAttribute

layoutRelation

这个

secondViewAttribute

setter

方法里内容很多:

- (void)setSecondViewAttribute:(id)secondViewAttribute {
    if ([secondViewAttribute isKindOfClass:NSValue.class]) {
        [self setLayoutConstantWithValue:secondViewAttribute];
    } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) {
        _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute];
    } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) {
        _secondViewAttribute = secondViewAttribute;
    } else {
        NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute);
    }
}
           

这里的

secondViewAttribute

有三种类型,分别是

NSValue

MAS_VIEW

MASViewAttribute

,我可以举三个例子对应这里的三种情况:

make.width.mas_equalTo();
make.left.equalTo(self.view);
make.left.equalTo(self.view.mas_left);
           

其中第二行和第三行是等价的,从

setter

的代码里可以看出为什么第二个例子和第三个例子是等价的,因为当传入的

secondViewAttribute

的类型是

MAS_VIEW

类型时,首先会实例化一个

MASViewAttribute

的对象,该对象使用传入的

View

firstView

layoutAttribute

进行配置,所以当传入

self.view

时会和当前

view

attribute

保持一致使用

left

第三行传入的

self.view.mas_left

直接就是一个

MASViewAttribute

对象,直接赋值即可。

MASViewAttribute

MASViewAttribute

保存三样东西:

MAS_VIEW

类型的

view

id

类型的

item

NSLayoutAttribute

类型的

layoutAttribute

其初始化方法有两个:

- (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [self initWithView:view item:view layoutAttribute:layoutAttribute];
    return self;
}

- (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute {
    self = [super init];
    if (!self) return nil;

    _view = view;
    _item = item;
    _layoutAttribute = layoutAttribute;

    return self;
}
           

第二个方法中的item在一般情况下和第一个view是同一个对象,当使用Masonry的VC相关的接口时是指

id<UILayoutSupport>

最后就是使用两个view的

MASViewAttribute

来构建

constraint

并添加到相关view上。

Install

当配置好maker后,就是install的步骤,直接看install中的部分源代码:

- (void)install {
    ...
    MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item;
    NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute;
    MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item;
    NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute;
    ...
    MASLayoutConstraint *layoutConstraint
        = [MASLayoutConstraint constraintWithItem:firstLayoutItem
                                        attribute:firstLayoutAttribute
                                        relatedBy:self.layoutRelation
                                           toItem:secondLayoutItem
                                        attribute:secondLayoutAttribute
                                       multiplier:self.layoutMultiplier
                                         constant:self.layoutConstant];
    ...
    MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view];
    self.installedView = closestCommonSuperview;
    ...
    [self.installedView addConstraint:layoutConstraint];
    self.layoutConstraint = layoutConstraint;
    [firstLayoutItem.mas_installedConstraints addObject:self];
}
           

这里只关注其主要的逻辑,首先根据两个

viewAttribute

用系统API生成一个

layoutConstraint

对象,然后调用

-mas_closestCommonSuperview:

方法获取两个view的最近父view,最后在这个父view添加刚才生成的约束。

Note:mas_closestCommonSuperview:的逻辑是先固定一个view,然后向上遍历另一个view的父view,如果找到相同view就退出,没找到再固定第一个view的父view,继续遍历第二个view的父view,直到找到或是遍历完全部。

这其中有很多判断,会分成很多种情况,我这里讲的是最通常的那一种情况。

最后会将该

constraint

保存起来,同时将自身加入第一个

view

installedConstraints

的数组中

至此整个约束的添加逻辑就完成了。

MASCompositeConstraint

前面在说到

make.left

这句话的执行情况时省略了一部分代码,这里就将其补回来。

在最开始的使用示例中有一句话

make.width.height.mas_equalTo(200);

,这句话最终会进入下面这个方法:

- (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;
    }
    ...
    return newConstraint;
}
           

当我们调用make.width时返回一个

MASConstraint

对象,这个对象也有

left

right

top

bottom

width

height

等方法,当对

make.width

调用

.height

时,就会生成一个

MASCompositeConstraint

对象

compositeConstraint

,这个对象持有一个

MASConstraint

类型对象的数组,同时用

compositeConstraint

替换原来的

constraint

,Masonry使用

MASCompositeConstraint

来支持在一句话中同时设置多个约束的行为。

我们再来看另一种情况

make.top.left.bottom.right.equalTo(self.view);

,在这句话中

make.top.left

返回的已经是一个

MASCompositeConstraint

对象了,这时调用

.bottom

时会进入

MASCompositeConstraint

-constraint: addConstraintWithLayoutAttribute:

方法,这个方法的实现如下:

- (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    id<MASConstraintDelegate> strongDelegate = self.delegate;
    MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    newConstraint.delegate = self;
    [self.childConstraints addObject:newConstraint];
    return newConstraint;
}
           

它首先拿到自己的代理,这个代理实际上就是

maker

,我们看前面生成

MASCompositeConstraint

的代码就可知道,然后调用

maker

-constraint: addConstraintWithLayoutAttribute:

方法,这个方法的作用在此刻就十分单纯,就是生成一个

newConstraint

- (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
    MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute];
    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    ...
    return newConstraint;
}
           

省略去的代码都是在此情况下不会执行的部分。

然后将

newConstraint

代理设为

self

,同时将其加入到

self.childConstraints

数组中,在后面安装时,对这个数组中每个约束都发送

install

消息即可。

总结

第一次阅读源代码选择了Masonry,因为其代码量不是很大,但其实跳坑里去了。Masonry的源码阅读起来真的很吃力,各种拥有类似名字的变量,各种block的嵌套,各种抽象方法给阅读带来了困难。不过这丝毫不影响这个库的优秀,它提供的接口如此简洁,使用起来是如此的丝滑,完美的阐释了那句:把复杂留给自己,把简单留给别人。

链式语法

Masonry通过使用大量的block提供了简洁的链式语法。

MASConstraint

这个类中的大部分方法的都返回一个block,而block的返回值都是

MASConstraint

,返回的

MASConstraint

对象又可以调用返回block的方法,正是通过这样的方式使链式语法能够工作。

抽象方法

通过定义宏:

#define MASMethodNotImplemented() \
    @throw [NSException exceptionWithName:NSInternalInconsistencyException \
                                   reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \
                                 userInfo:nil]
           

来实现抽象方法真的很有创意。

宏的自动补全

我们看下面这段代码:

#define mas_equalTo(...)                 equalTo(MASBoxValue((__VA_ARGS__)))
#define mas_greaterThanOrEqualTo(...)    greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__)))
#define mas_lessThanOrEqualTo(...)       lessThanOrEqualTo(MASBoxValue((__VA_ARGS__)))

#define mas_offset(...)                  valueOffset(MASBoxValue((__VA_ARGS__)))

@interface MASConstraint (AutoboxingSupport)

/**
 *  Aliases to corresponding relation methods (for shorthand macros)
 *  Also needed to aid autocompletion
 */
- (MASConstraint * (^)(id attr))mas_equalTo;
- (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo;
- (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo;

/**
 *  A dummy method to aid autocompletion
 */
- (MASConstraint * (^)(id offset))mas_offset;

@end
           

当我们在使用

mas_equalTo()

这个方法时,实际上使用的是上面的宏,但是Masonry仍然提供了方法,这样做的目的在注释中写的很清楚,为了使宏能够自动补全。

没有循环引用

使用block时最让人心烦的就是循环引用,Masonry使用block为我们提供优雅的使用方式,并没有带来循环引用的弊端,真的是优秀。