Masonry源码解析

作者: iDeveloper | 来源:发表于2018-01-01 22:40 被阅读245次

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

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

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

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

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

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

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

    系统接口:


    约束的添加和修改

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

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

    约束构建

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

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

    MASViewConstraint - 属于Masonry约束

    MASViewConstraint

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

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

    继续往下看,到父类MASConstraint。

    MASConstraint - 优雅的点链式语法

    • multiplier


      multiplier
    • constant


      constant
    • relation


      image.png
    • priority


      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有多个子类,它们各自的实现又不同。

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

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

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

    MASCompositeConstraint

    MASCompositeConstraint,定义了一组MASConstraint。

    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文件的类,肯定是无法访问的。


    MASConstraint+Private.h MASCompositeConstraint.m image.png

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

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

    MASConstraintMaker install View+MASAdditions.h
    • mas_makeConstraints:
      直接添加约束。

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


      删除原来的约束
    • mas_updateConstraints:
      正常修改constant


      修改constant
    MASViewConstraint+install

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

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


    添加外部引用likeButton2CenterYOffCn

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


    NSAssert

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

    我们继续看hasBeenInstalled方法


    hasBeenInstalled

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


    为空,崩溃

    查下原因:


    deactivate
    uninstall

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

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

    成功更新约束!

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


      Masonry priority修改

    改进Masonry

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

    Masonry支持priority的修改1 Masonry支持priority的修改2 Masonry支持priority的修改3

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


    Masonry兼容priority修改

    Demo在此,请戳我。

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

    获取约束
    mas_installedConstraints

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

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


    获取约束

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

    约束移除

    因为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源码解析到此,有疑问或指出错误,请留言。

    相关文章

      网友评论

      • 舒马赫:Masory写的很棒,但是不喜欢纯代码写界面,太慢了。推荐使用xml的布局库FlexLib,采用前端布局标准flexbox(不使用autolayout),支持热刷新,国际化等。可以到这里了解详细信息:

        https://github.com/zhenglibao/FlexLib

      本文标题:Masonry源码解析

      本文链接:https://www.haomeiwen.com/subject/jieagxtx.html