Masonry源码解析

Masonry一直是OC中优秀的Auto Layout框架,尤其是其优雅的点链式语法设计,为人津津乐道。

今天我们来看看Masonry的源码,看看给我们什么启示。

先从脑海简单想想,如果让我们自己打造一个Auto Layout框架,大概要怎么做?

  • 构建约束
    系统的构建约束方法,诉我直言,非常不优雅。VFL依然和Masonry比起来依然糟糕透了。(Masonry不是基于VFL的封装)
    Apple貌似无心去优化Auto Layout的代码语法,而是不遗余力去推广Storyboard。

  • 添加约束
    系统有,简单封装下,如下图。

  • 修改约束
    系统有,简单封装下,如下图。

  • 记录约束
    系统有,简单封装下,如下图。

系统接口:


Masonry源码解析_第1张图片
约束的添加和修改

看来最主要还是构建约束的方法,需要封装成一套更加优雅,简单好用的方法。
VFL显然不符合我们面向对象的习惯,像UI版的SQLite。

那只剩唯一的方法:
按照view1.attr1 = view2.attr2 * multiplier + constant来构建约束。

Masonry源码解析_第2张图片
约束构建

然后进一步抽象成我们约束,形成一套我们构建方法。

简单脑洞就到这吧,我们看看Masonry是怎么做的。

MASViewConstraint - 属于Masonry约束

Masonry源码解析_第3张图片
MASViewConstraint

view1.attr1 = view2.attr2 * multiplier + constant构建形式。
MASViewAttribute 将一个item/view 和 attribute封装成一个对象。
那么MASViewConstraint 含有 firstViewAttribute和secondViewAttribute。
当然也有可能只有firstViewAttribute,例如对宽或高等于某个具体的数值的约束。

还差multiplier,constant 和 relation(=),priority也不能忘记,尽管它不在式子里。

继续往下看,到父类MASConstraint。

MASConstraint - 优雅的点链式语法

  • multiplier


    Masonry源码解析_第4张图片
    multiplier
  • constant


    Masonry源码解析_第5张图片
    constant
  • relation


    Masonry源码解析_第6张图片
    image.png
  • priority


    Masonry源码解析_第7张图片
    priority

点链式语法,利用OC中get(即点)方法获取block,这个block执行后又能放回self对象本身。当这个类有多个这种方法时,就能形成点链式的语法。

Talk is Cheap,上代码。例如,改进UIButton的设置:

@interface UIButton (Chain)

- (UIButton *(^)(NSString *normalTitle))normalTitle;
- (UIButton *(^)(NSString *selectedTitle))selectedTitle;
- (UIButton *(^)(UIColor *normalTitleColor))normalTitleColor;
- (UIButton *(^)(UIColor *selectedTitleColor))selectedTitleColor;
- (UIButton *(^)(NSString *normalImageName))normalImageName;
- (UIButton *(^)(NSString *selectedImageName))selectedImageName;
- (UIButton *(^)(CGFloat fontSize))fontSize;

@end

或者我们还可以通过运行时或者继承,将state记录下来,进一步优化接口:

@interface YZChainButton : UIButton

- (YZChainButton *(^)(UIControlState state))yz_state;
- (YZChainButton *(^)(NSString *title))yz_title;
- (YZChainButton *(^)(UIColor *color))yz_titleColor;
- (YZChainButton *(^)(NSString *name))yz_imageName;
- (YZChainButton *(^)(CGFloat fontSize))yz_fontSize;

@end

那就这样实现:

@interface YZChainButton ()

@property (nonatomic, assign) UIControlState yz_controlState;

@end

@implementation YZChainButton

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
    // Drawing code
}
*/

- (YZChainButton *(^)(UIControlState state))yz_state {
    return ^id (UIControlState state) {
        self.yz_controlState = state;
        return self;
    };
}
- (YZChainButton *(^)(NSString *title))yz_title {
    return ^id (NSString *title) {
        [self setTitle:title
              forState:self.yz_controlState];
        return self;
    };
}

Demo在此,请戳我。

简单发散一下,我们回到Masonry来继续看,MASConstraint.m

单个到组的拓展 - 使用同一个基类,同一套接口

未实现的方法
Masonry源码解析_第8张图片
未实现的方法

这些方法都没有实现,由子类去实现,为什么?

最核心也最简单的原因,因为Masonry有多个子类,它们各自的实现又不同。

为什么这些接口要父类来定义,交给各子类各自去定义去实现不就好了?

因为外部使用的时候希望,使用统一(即父类)的接口来调用,而不用关心子类的类型。

来,我们来看MASConstraint的子类们:

Masonry源码解析_第9张图片
MASCompositeConstraint

MASCompositeConstraint,定义了一组MASConstraint。

Masonry源码解析_第10张图片
MASViewConstraint

MASViewConstraint,定义一个约束。

Masonry 一套构建接口,可以构建多个约束或者一个接口:

按照MASViewConstraint的构建,我们会这样写。

    [likeButton2 mas_remakeConstraints:^(MASConstraintMaker *make) {
        make.centerX.equalTo(self.view);
        make.centerY.equalTo(self.view).offset(150);
    }];

有了MASCompositeConstraint,我们可以这样:

    [likeButton2 mas_makeConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self.view).centerOffset(CGPointMake(0, 150));
    }];

实际上,在Masonry内部添加约束都是一组(MASCompositeConstraint)来添加的。 (哈哈,必须是啊,难道还if-else ?!?)

一句代码构建多个约束,一次大大简化,接口却和构建一个约束一毛一样,666。

我们可以在MASConstraintMaker.m文件中看到。

- (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs {
    __unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading
                                          | MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX
                                          | MASAttributeCenterY | MASAttributeBaseline
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
                                          | MASAttributeFirstBaseline | MASAttributeLastBaseline
#endif
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
                                          | MASAttributeLeftMargin | MASAttributeRightMargin | MASAttributeTopMargin | MASAttributeBottomMargin
                                          | MASAttributeLeadingMargin | MASAttributeTrailingMargin | MASAttributeCenterXWithinMargins
                                          | MASAttributeCenterYWithinMargins
#endif
                                          );
    
    NSAssert((attrs & anyAttribute) != 0, @"You didn't pass any attribute to make.attributes(...)");
    
    NSMutableArray *attributes = [NSMutableArray array];
    
    if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left];
    if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right];
    if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top];
    if (attrs & MASAttributeBottom) [attributes addObject:self.view.mas_bottom];
    if (attrs & MASAttributeLeading) [attributes addObject:self.view.mas_leading];
    if (attrs & MASAttributeTrailing) [attributes addObject:self.view.mas_trailing];
    if (attrs & MASAttributeWidth) [attributes addObject:self.view.mas_width];
    if (attrs & MASAttributeHeight) [attributes addObject:self.view.mas_height];
    if (attrs & MASAttributeCenterX) [attributes addObject:self.view.mas_centerX];
    if (attrs & MASAttributeCenterY) [attributes addObject:self.view.mas_centerY];
    if (attrs & MASAttributeBaseline) [attributes addObject:self.view.mas_baseline];
    
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
    
    if (attrs & MASAttributeFirstBaseline) [attributes addObject:self.view.mas_firstBaseline];
    if (attrs & MASAttributeLastBaseline) [attributes addObject:self.view.mas_lastBaseline];
    
#endif
    
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
    
    if (attrs & MASAttributeLeftMargin) [attributes addObject:self.view.mas_leftMargin];
    if (attrs & MASAttributeRightMargin) [attributes addObject:self.view.mas_rightMargin];
    if (attrs & MASAttributeTopMargin) [attributes addObject:self.view.mas_topMargin];
    if (attrs & MASAttributeBottomMargin) [attributes addObject:self.view.mas_bottomMargin];
    if (attrs & MASAttributeLeadingMargin) [attributes addObject:self.view.mas_leadingMargin];
    if (attrs & MASAttributeTrailingMargin) [attributes addObject:self.view.mas_trailingMargin];
    if (attrs & MASAttributeCenterXWithinMargins) [attributes addObject:self.view.mas_centerXWithinMargins];
    if (attrs & MASAttributeCenterYWithinMargins) [attributes addObject:self.view.mas_centerYWithinMargins];
    
#endif
    
    NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count];
    
    for (MASViewAttribute *a in attributes) {
        [children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]];
    }
    
    MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children];
    constraint.delegate = self;
    [self.constraints addObject:constraint];
    return constraint;
}
typedef NS_OPTIONS(NSInteger, MASAttribute) {
    MASAttributeLeft = 1 << NSLayoutAttributeLeft,
    MASAttributeRight = 1 << NSLayoutAttributeRight,
    MASAttributeTop = 1 << NSLayoutAttributeTop,
    MASAttributeBottom = 1 << NSLayoutAttributeBottom,
    MASAttributeLeading = 1 << NSLayoutAttributeLeading,
    MASAttributeTrailing = 1 << NSLayoutAttributeTrailing,
    MASAttributeWidth = 1 << NSLayoutAttributeWidth,
    MASAttributeHeight = 1 << NSLayoutAttributeHeight,
    MASAttributeCenterX = 1 << NSLayoutAttributeCenterX,
    MASAttributeCenterY = 1 << NSLayoutAttributeCenterY,
    MASAttributeBaseline = 1 << NSLayoutAttributeBaseline,
    
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100)
    
    MASAttributeFirstBaseline = 1 << NSLayoutAttributeFirstBaseline,
    MASAttributeLastBaseline = 1 << NSLayoutAttributeLastBaseline,
    
#endif
    
#if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000)
    
    MASAttributeLeftMargin = 1 << NSLayoutAttributeLeftMargin,
    MASAttributeRightMargin = 1 << NSLayoutAttributeRightMargin,
    MASAttributeTopMargin = 1 << NSLayoutAttributeTopMargin,
    MASAttributeBottomMargin = 1 << NSLayoutAttributeBottomMargin,
    MASAttributeLeadingMargin = 1 << NSLayoutAttributeLeadingMargin,
    MASAttributeTrailingMargin = 1 << NSLayoutAttributeTrailingMargin,
    MASAttributeCenterXWithinMargins = 1 << NSLayoutAttributeCenterXWithinMargins,
    MASAttributeCenterYWithinMargins = 1 << NSLayoutAttributeCenterYWithinMargins,

#endif
    
};

这种位移枚举值,使用|(或)来组合,然后&(与)来提取,然后遍历构建一组(MASCompositeConstraint)约束。

接口分离

曾经有人问,OC语言钟,想让父类的某个属性只让它的子类访问,而不让其他类访问,可以实现吗? 我回答:不可以。 虽然这个答案没毛病,但称不上优秀。

通过Extension,在一个新的.h文件中声明,仅供子类访问的接口(包括属性),然后在需要访问的子类中引入改.h文件,子类就能访问了,而其他未引入改.h文件的类,肯定是无法访问的。


Masonry源码解析_第11张图片
MASConstraint+Private.h
Masonry源码解析_第12张图片
MASCompositeConstraint.m
Masonry源码解析_第13张图片
image.png

Masonry 中的 MASConstraint+Private.h 就是这种方式。
Extension的作用,不仅仅在.m文件中声明私有接口吧。

免去外部引用的约束修改。

Masonry源码解析_第14张图片
MASConstraintMaker install
Masonry源码解析_第15张图片
View+MASAdditions.h
  • mas_makeConstraints:
    直接添加约束。

  • mas_updateConstraints:
    删除所有原来的约束,再添加新的约束。


    Masonry源码解析_第16张图片
    删除原来的约束
  • mas_updateConstraints:
    正常修改constant


    Masonry源码解析_第17张图片
    修改constant
Masonry源码解析_第18张图片
MASViewConstraint+install

更新约束,先找是否有相同的约束,有改constant;没有,则添加此约束。
怎样算相同的约束?
除了constant,其他都必须一样! priority,multiplier,relation都必须一样。意味着想用mas_updateConstraints改priority,那是没有可能的事!
只能改constant!只能改constant!只能改constant!

我就是想改某个约束的priority怎么办?


Masonry源码解析_第19张图片
添加外部引用likeButton2CenterYOffCn

运行后,进入断言了。Cannot modify constraint priority after it has been installed。


Masonry源码解析_第20张图片
NSAssert

可以看到有效的约束的priority,Masonry压根不允许修改。

我们继续看hasBeenInstalled方法


Masonry源码解析_第21张图片
hasBeenInstalled

activate了才有效,那么我们可以先deactivate,再activate。


Masonry源码解析_第22张图片
为空,崩溃

查下原因:


Masonry源码解析_第23张图片
deactivate

Masonry源码解析_第24张图片
uninstall

deactive后,本约束从数组中移除了,失去了引用就销毁了,所以外部引用必须为强引用。

//@property (nonatomic, weak) MASConstraint *likeButton2CenterYOffCn;
@property (nonatomic, strong) MASConstraint *likeButton2CenterYOffCn;

成功更新约束!

最终结论:Masonry的约束修改priority和constant不同!
  • 必须添加外部强引用,然后deactive之后,修改priority,再重新active。
  • 或者删除该约束,重新添加,如下图代码。


    Masonry源码解析_第25张图片
    Masonry priority修改

改进Masonry

这个是我对Masonry的槽点,于是我决定改进一下。

Masonry源码解析_第26张图片
Masonry支持priority的修改1
Masonry源码解析_第27张图片
Masonry支持priority的修改2
Masonry源码解析_第28张图片
Masonry支持priority的修改3

修改后兼容下面两种方法的修改priority:


Masonry源码解析_第29张图片
Masonry兼容priority修改

Demo在此,请戳我。

Masonry的约束持有思维,不能和其他方式添加的Auto Layout兼容:

Masonry源码解析_第30张图片
获取约束

Masonry源码解析_第31张图片
mas_installedConstraints

Masonry记录约束的方式,通过View动态绑定NSMutableSet的mas_installedConstraints实现的。
通过搜索发现,只有MASViewConstraint的install方法才有调用,意味着只有Masonry自己创建的约束才会被记录。所以呢,xib或者Storyboard创建的约束,Masonry是不会管的,改不动。

这个是不是可以改进一下呢?
毕竟view的约束我们本来就是可以获取到的:


获取约束

然后将NSLayoutConstraint转成MASViewConstraint !?!
细想一下使用这种方法来获取约束,无法知道某个约束是针对谁创建了。mas_remakeConstraints:会误移除了别的View的约束,不符合Masonry对View的约束布局思想。

Masonry源码解析_第32张图片
约束移除

因为make.centerX.equalTo(likeButton2)是两个View共有的约束,两个View的constraints都包含了这个约束。任意一个View移除了它,就两个都没了。

恩,系统constraints记录的约束,不符合Masonry人性化的思想。
Masonry的思想是:对哪个View创建的约束,由那个View持有。

换言之,make.centerX.equalTo(likeButton2)这个约束不会被likeButton2持有。这种思维更加自然。
一个显而易见的好处就是:只要没有约束冲突likeButton1总是和likeButton2保持水平中心对齐,不会被likeButton2的布局影响到。

至此,Masonry最核心的几个类都介绍过了,几个Category带过一下吧。
View+MASAdditions MAS_VIEW的Masonry支持,Masonry可能是UIView或者NSView。
ViewController+MASAdditions是topLayoutGuide和bottomLayoutGuide的支持。
NSArray+MASAdditions数组的支持,同时为多个View创建一样的约束。
View+MASShorthandAdditionsNSArray+MASShorthandAdditions都是去掉mas_的简写。

关于MASViewAttribute为什么重新isEqualhash方法,可参考文章。

Masonry源码解析到此,有疑问或指出错误,请留言。

你可能感兴趣的:(Masonry源码解析)