美文网首页三方库分析
探究 Masonry 源码

探究 Masonry 源码

作者: jing37 | 来源:发表于2017-05-26 15:44 被阅读71次

    Masonry 是一个轻量级自动布局框架,开发者可以使用更简洁的链式语法为控件进行布局。Masonry 的使用可以参考官网,这里主要探究一下 Masonry 的实现。

    Masonry 是对 Auto Layout 的封装,最终还是通过 Auto Layout 来对控件添加约束。这里简单地介绍一下约束,view 与 view 之间的布局可以用一系列线性方程来表示,一个单独的方程就表示为一个约束。下图就是一个简单的线性方程:

    EE6ADF1B-E69F-41E0-AD0D-46E351BA61C3.png

    这条约束说明了红色 view 与 蓝色 view 左右之间的位置关系,布局中所有的关系都可以抽象成这样的方程,因此布局的过程就是创建一系列约束,也就是创建一系列线性方程的过程。使用 NSLayoutConstraint 来创建约束时,每条约束都要按照下面的方法来创建,这样当布局复杂的时候就需要大量的代码。Masonry 对 NSLayoutConstraint 进行了封装,使用起来更加简洁易懂。下面就来探究一下 Masonry 如何对 NSLayoutConstraint 进行封装。

    // 使用 NSLayoutConstraint 创建约束
    redView.translatesAutoresizingMaskIntoConstraints = NO
    [NSLayoutConstraint constraintWithItem:redView
                                     attribute:NSLayoutAttributeLeading
                                     relatedBy:NSLayoutRelationEqual
                                        toItem:blueView
                                     attribute:NSLayoutAttributeTrailing
                                    multiplier:1.0
                                      constant:8.0]
    
    // 使用 Masonry 
    [redView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.leading.equalTo(blueView.mas_trailing).with.offset(8.0); 
    }];
    

    Masonry 结构

    首先我们来先看一个 Masonry 中所包含的类,对其有个大致的了解


    101162FC-4D0D-4EE1-A3A8-B7BB9F005488.png

    其中 UIViewController+MASAdditions、UIView+MASAdditions、NSArray+MASAdditions 三个 category 包含布局所使用的方法,它们通过 MASConstraintMaker 工厂类来创建 MASContraints,并且为视图添加约束。MASViewConstraint 和 MASCompositeConstraint 是 MASConstraint 的子类,MASConstraint 是个抽象类,由其子类 MASViewConstraint 和 MASCompositeConstraint 来创建实例,另外 MASConstraint 里面提供对链式语法的支持,使用者可以使用链式语法来创建约束。
    在看具体的源码之前,我们先来了解一下 Masonry 是如何表示上述布局表达式的,之前说了一个表达式就表示一个 constraint,MASConstraint 就是 Masonry 中用来生成 constraint 对象的类,因其是个抽象类,具体由其子类来实现,先来看一下 MASViewConstraint 这个类,其主要包含了两个属性和一个初始化方法,其中 firstViewAttribute 和 secondViewAttribute 对应下图等式中的部分,如此我们只需要设置其 relationship,multiplier 和 constant 这个约束就完成了,这些都可以通过 MASConstraint 给定的方法来完成,具体内容下面会介绍。

    @interface MASViewConstraint : MASConstraint <NSCopying>
    @property (nonatomic, strong, readonly) MASViewAttribute *firstViewAttribute;
    @property (nonatomic, strong, readonly) MASViewAttribute *secondViewAttribute;
    - (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute;
    + (NSArray *)installedConstraintsForView:(MAS_VIEW *)view;
    @end
    
    E93D48F8-B0C4-4722-8748-A88CDA8C5EDD.png
    从 mas_makeConstraints 开始源码探究

    UIView+MASAdditions 依赖于 MASViewAttribute 和 MASConstraintMaker,该 category 包含类型为 MASViewAttribute 的成员属性,并且提供了我们布局最常用的几个方法。

    // 创建并为当前视图添加约束
    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block;
    // 更新当前视图已有的约束
    - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block;
    // 移除已有约束,重新布局
    - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block;
    

    首先来看一下创建约束的过程,以下代码是 mas_makeConstraints:方法的具体实现,在使用时我们是通过 block 回调来添加约束。

    • 首先将 translatesAutoresizingMaskIntoConstraints 属性设置为 NO,Auto Layout 与 Autoresizing 不能同时使用。
    • 创建 MASConstraintMaker 的实例,MASConstraintMaker 提供工厂方法来创建 MASConstraint
    • 通过 block 回调创建约束,并添加到 MASConstraintMaker 的私有数组 constraints 中
    • 执行 [constraintMaker install] 方法,最终通过 MASConstraint 的 install 的方法,对相应视图添加约束
    - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block {
        self.translatesAutoresizingMaskIntoConstraints = NO;
        MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
        block(constraintMaker);
        return [constraintMaker install];
    }
    

    实例化 MASConstraintMaker 的过程

    MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
    
    // 创建 MASConstraintMaker 实例,并初始化 constraints 和 view 私有属性,将 self.view 设置为当前 view
    - (id)initWithView:(MAS_VIEW *)view {
        self = [super init];
        if (!self) return nil;
        
        self.view = view;
        self.constraints = NSMutableArray.new;
        
        return self;
    }
    

    创建好 MASConstraintMaker 实例之后,来具体看一下 block 中 make.leading.equalTo(blueView.mas_trailing).with.offset(8.0); 的执行,执行结果最终生成一个类型为 MASViewConstraint 的对象。

    • make 即为 MASConstraintMaker 的实例
    • leading 为 MASConstraintMaker 的属性, make.leading 会执行 MASConstraintMaker 中 leading 的 getter 方法,返回一个 MASContraint 对象,实际会返回一个 MASViewConstraint 对象,并设置其 firstViewAttribute 属性。
    // MASConstraintMaker.m
    // leading getter 方法,返回 MASContraint 对象
    - (MASConstraint *)leading {
        return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading];
    }
    - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
        return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute];
    }
    // 该方法创建 MASContraint 对象
    - (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;
    }
    

    具体看 -(MASConstraint *)constraint: addConstraintWithLayoutAttribute: 方法
    1、创建一个 MASViewAttribute 对象,MASViewAttribute 是对 view + NSLayoutAttribute 的封装,用来存储 view 和 其相关的 NSLayoutAttribute,描述了上述方程式等号的一边,执行 make.leading 时,MASViewAttribute 对象初始化时的 view 即为 redView, NSLayoutAttribute 为 NSLayoutAttributeLeading


    D5125E33-DDD6-4FDA-8311-2A344973BEC7.png

    2、根据第一步中的 MASViewAttribute 对象实例化 MASViewConstraint 对象,对其 firstViewAttribute 进行赋值。下面的代码为初始化 MASViewConstraint 对象的过程

    MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute];
    
    // MASViewConstraint.m
    - (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute {
        self = [super init];
        if (!self) return nil;
        
        _firstViewAttribute = firstViewAttribute;
        self.layoutPriority = MASLayoutPriorityRequired;
        self.layoutMultiplier = 1;
        
        return self;
    }
    

    3、判断 constraint 是否存在,在当前过程中 constraint 是不存在的,因此此次执行如下过程,设置newConstraint 的 delegate,并将该对象添加到数组中。

    if (!constraint) {
            newConstraint.delegate = self;
            [self.constraints addObject:newConstraint];
        }
    

    MASViewConstraint 对象的 delegate 方法定义在 其父类 MASConstraint 的 MASConstraint + Private.h 分类中,这个delegate 是实现链式语法的重点。

    @protocol MASConstraintDelegate <NSObject>
    - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute;
    @end
    

    4、最后返回 newConstraint 对象
    至此,make.leading 执行完毕,返回了 newConstraint 对象,该对象的 firstViewAttribute 已经设置好了,即为方程式的左边部分,layoutMultiplier 也设置为了1,此时方程式只剩下右边的 item2 、Attribute2、和常数了,Masonry 已经将item 和 attribute 打包为了 MASViewAttribute。

    • 接着执行 equalTo 方法, make.leading.equalTo(blueView.mas_trailing),iOS的语法中没有方法后面跟着(参数)的,这里很明显是一个 block,利用 block 实现了链式语法,这里要注意 block 的返回类型要为 MASConstraint 类型。
    //MASViewConstraint.m
    - (MASConstraint * (^)(id))equalTo {
        return ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        };
    }
    // 返回值为block 
    - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation {
        return ^id(id attribute, NSLayoutRelation relation) {
            if ([attribute isKindOfClass:NSArray.class]) {
                NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation");
                NSMutableArray *children = NSMutableArray.new;
                for (id attr in attribute) {
                    MASViewConstraint *viewConstraint = [self copy];
                    viewConstraint.layoutRelation = relation;
                    viewConstraint.secondViewAttribute = attr;
                    [children addObject:viewConstraint];
                }
                MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
                compositeConstraint.delegate = self.delegate;
                [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint];
                return compositeConstraint;
            } else {
                NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation");
                self.layoutRelation = relation;
                self.secondViewAttribute = attribute;
                return self;
            }
        };
    }
    

    在执行 equalTo 方法之前,先看一下该方法 block 所需要的参数 (id attr),参数类型可以MASViewAttribute, UIView, NSValue, NSArray 中的任意一个,显然 blueView.mas_trailing 应该是 MASViewAttribute 类型的对象。

    self.layoutRelation = relation;
    self.secondViewAttribute = attribute;
    // 
    - (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 的 set 方法中在对不同类型的参数进行区分,如果是 NSValue,MASConstraint.m 中有对常量设置的方法 setLayoutConstantWithValue;如果是 NSView 类型,则需要实例化一个 MASViewAttribute 对象,此时 layoutAttribute 属性值设置为 self.firstViewAttribute.layoutAttribute;如果是 MASViewAttribute 类型,则直接赋值。

    // 参数类型为 NSView 类型
    make.leading.equalTo(redView); 
    // 等价于  
    make.leading.equalTo(redView.mas_leading);  // mas_leading 在此时即为 constraint.firstViewAttribute.layoutAttribute
    

    此处的 secondViewAttribute 就是 等式右侧


    69FDAB87-8071-4EAF-B0D8-6EB7CB8ED68A.png
    • 执行完 make.leading.equalTo(blueView.mas_trailing),布局等式除了 constant 之外的所有参数都已设置完毕,接下来执行 .with,该函数返回其本身,只是为了提高代码的可读性。
    - (MASConstraint *)with {
        return self;
    }
    
    • 接下来就是 .offset() , 修改 constant 为8.0
    // MASConstraint.m
    - (MASConstraint * (^)(CGFloat))offset {
        return ^id(CGFloat offset){
            self.offset = offset;
            return self;
        };
    }
    // MASViewConstraint.m
    - (void)setOffset:(CGFloat)offset {
        self.layoutConstant = offset;
    }
    

    到这里一个约束就建立好了,回到 mas_makeConstraints 方法中,接下来该执行 [constraintMaker install] 对约束进行安装,首先会判断是够需要移除当前约束重新添加,并判断是否需要更新现有约束,然后执行 [constraint install] 找到合适的 view 来添加约束。

    - (NSArray *)install {
        if (self.removeExisting) {
            NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view];
            for (MASConstraint *constraint in installedConstraints) {
                [constraint uninstall];
            }
        }
        NSArray *constraints = self.constraints.copy;
        for (MASConstraint *constraint in constraints) {
            constraint.updateExisting = self.updateExisting;
            [constraint install];
        }
        [self.constraints removeAllObjects];
        return constraints;
    }
    
    MASConstraintDelegate 的使用

    前面提到了一个 MASConstraintDelegate 的 delegate 方法,这个方法非常重要,对 MASConstraint 对象设置代理,才能支持链式调用

    make.width.and.height.equalTo(@10)
    

    make.width 的执行过程上面已经提过了,返回的是一个 MASViewConstraint 实例对象 newConstraint,并设置 newConstraint.delegate = self ,and 方法和 with 方法一样,都返回的是自身,此时调用 .height 方法就不在是 MASConstraintMaker 的方法,而是 MASViewConstraint 的 height 方法,然后调用代理方法添加约束,就完成链式调用

    // MASConstraint.m
    - (MASConstraint *)height {
        return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight];
    }
    // MASViewConstraint.m
    #pragma mark - attribute chaining
    - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute {
        NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation");
        return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute];
    }
    

    关于.equalTo() 的链式调用,可以用下面的一句话来说,具体可参考链接文章

    相关文章

      网友评论

        本文标题:探究 Masonry 源码

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